지금은 사용하지 않는 CRA에서는 React.FC를 템플릿에서 제거한 PR을 확인할 수 있다. 그렇다면 왜 React.FC 사용을 지양해야 할까? 무조건 쓰지 말아야 하는 걸까?
// ❌ 지양
const Button: React.FC<ButtonProps> = ({ children }) => {
return <button>{children}</button>;
};
// ✅ 권장
const Button = ({ children }: ButtonProps) => {
return <button>{children}</button>;
};
children 이 암묵적으로 포함된다.React.FC는 자동으로 children을 props에 추가하므로, children을 받지 않는 컴포넌트에서도 children을 넘길 수 있어서 버그 가능성이 생긴다. (React 18에서는 제거됐지만 레거시에서 고려할 필요)
// ❌ React.FC로는 불가능
const List: React.FC<ListProps<T>> = ... // T를 어디에?
// ✅ 직접 선언하면 가능
const List = <T,>(props: ListProps<T>) => { ... };
예시: Dropdown
// ❌ React.FC — sub-component마다 타입 선언에 다 적어야 함
const Dropdown: React.FC<DropdownProps> & {
Trigger: React.FC<TriggerProps>;
Menu: React.FC<MenuProps>;
Item: React.FC<ItemProps>;
} = ({ children }) => {
return <div className="dropdown">{children}</div>;
};
Dropdown.Trigger = ({ children }) => <button>{children}</button>;
Dropdown.Menu = ({ children }) => <ul>{children}</ul>;
Dropdown.Item = ({ label, onClick }) => <li onClick={onClick}>{label}</li>;
React.FC<DropdownProps>로 타입을 선언하면, TypeScript는 Dropdown이 정확히 그 타입만 가진다고 본다.
// TypeScript가 보는 관점:
const Dropdown: React.FC<DropdownProps> = ...
// → Dropdown의 타입 = (props: DropdownProps) => ReactElement | null
// → 그 외 프로퍼티? 없음
Dropdown.Trigger는 React.FC에 없는 프로퍼티기에, 미리 “이 함수에는 Trigger, Menu, Item이 있어.”라고 intersection(&) 으로 알려줘야 한다.
const Dropdown: React.FC<DropdownProps> & {
Trigger: React.FC<TriggerProps>; // ← "Trigger도 있을 거야"
Menu: React.FC<MenuProps>; // ← "Menu도 있을 거야"
Item: React.FC<ItemProps>; // ← "Item도 있을 거야"
} = ({ children }) => { ... };
하지만 서브 컴포넌트가 많아질수록 React.FC로 타입 선언을 일일이 해 줘야 하기에 매우 번거롭고, 코드도 복잡해진다.
반면 FC 없이는 TS가 타입 추론만 하고 고정하지는 않는다. 그래서 프로퍼티를 붙이면 자동으로 타입을 추론한다.
const Dropdown = ({ children }: DropdownProps) => { ... };
Dropdown.Trigger = ... // 자동 추론해서 반영
FC를 쓰면 타입이 잠겨서 미리 다 열거해야 하고, 안 쓰면 타입이 열려있어서 그냥 붙이기만 하면 되는 것으로 이해할 수 있겠다.
const TRIGGER_TYPE_ICON_MAP: Record<
TriggerType,
React.FC<{ color?: string; size?: string }>
>
여기서 React.FC를 쓴 이유는 compound component가 아니라 Record의 value 타입을 지정하는 용도이기 때문이다.
⇒ “이 Record의 value는 color와 size props를 받는 React 컴포넌트다.”라는 타입 제약을 뜻한다.
만약 React.FC없이 같은 걸 표현하려면
const TRIGGER_TYPE_ICON_MAP: Record<
TriggerType,
(props: { color?: string; size?: string }) => React.ReactElement | null
>
더 길고 번거롭다. 그래서 컴포넌트 타입을 참조용으로 쓸 때는 React.FC가 간결하다.
예시는 타입 참조기 때문에 children을 암묵적으로 포함하지 않으며, 타입 파라미터로 넘기면 되니까 제네릭과 상관이 없고, compound와도 무관하기 때문에 가독성과 간결함을 위해 사용한 것이다.
ex. 타입 참조 시 - FC로 제네릭 문제는 없음
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
// 타입 참조에서는 T를 구체적으로 지정하니까 문제없음
type Props = {
userList: React.FC<ListProps<User>>;
productList: React.FC<ListProps<Product>>;
};
선언 시에는 를 함수 앞에 붙여야 하는데 FC 문법 구조상 넣을 곳이 없고, 타입 참조 시에는 이미 <User> 같이 구체적인 타입을 넣기 때문에 문제가 생기지 않는다.