React.FC 를 사용하지 않는 이유, JSX.element 와 ReactNode
React.FC 를 사용하지 않는 이유, JSX.element 와 ReactNode
리액트 타입스크립트 로 함수형 컴포넌트를 작성할 때 이런식으로 React.FunctionalComponent 로 타입을 지정해서 사용하는 것을 많이 볼 수 있을 것이다.
React.FC란 typescript 에서 함수형 컴포넌트를 사용하기 위해 지원되는 인터페이스 중 하나이다.
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
React.FC를 사용하면 몇가지 문제점이 있어서 사용하지 않는 것으로 대부분 합의를 본다고 한다.
const TestComponent: React.FC = () => {
return <div> test component </div>;
};
🤔 React.FC (React.FunctionalComponent)를 잘 쓰지 않는 이유
1. children에 대한 암시적 정의 :
React.FC에는 children
이 있다고 가정한다, 내가 children을 의도하지 않았어도 넘겨주면 그대로 들어간다. 이렇게 작성하는 경우 런타임 에러가 나지 않는다.
const App: React.FC = () => { /*... */ };
const Example = () => {
<App><div>Unwanted children</div></App>
}
2. 제네릭을 지원하지 않음
이런식으로 컴포넌트를 만들 때 제네릭을 사용하는데,
type GenericComponentProps<T> = {
prop: T
callback: (t: T) => void
}
const GenericComponent = <T>(props: GenericComponentProps<T>) => {/*...*/}
React.FC에서는 이런 제네릭을 사용할 수 있는 방법이 없다.
const GenericComponent: React.FC</* ??? */> = <T>(props: GenericComponentProps<T>) => {/*...*/}
3. defaultProps가 제대로 작동하지 않는다
es6의 default arguments를 사용하는게 더 나을 수도 있다,,하지만,, defaultProps를 적용한 예시를 보면 이렇게 함수형 컴포넌트에 적용할 수 있다.
interface TestProps {
name: string;
age: number;
}
const TestComponent = ({ name, age }: TestProps) => {
return (
<div>
{name}, {age}
</div>
);
};
TestComponent.defaultProps = { age: 20 };
const Example = () => (<TestComponent name="hello" />)
이거를 그대로 React.FC로 바꾸면 에러가 난다.
interface TestProps {
name: string;
age: number;
}
const TestComponent: React.FC<TestProps> = ({ name, age }) => {
return (
<div>
{name}, {age}
</div>
);
};
TestComponent.defaultProps = { age: 20 };
const Example = () => (<TestComponent name="hello" />) //compile error
그렇다면 어떻게 써야할까? JSX.Element? React.ReactNode? ReactElement?
관련 커뮤니티나 react typescript cheatSheet 등을 보면 대부분 JSX.Element
를 리턴타입으로 지정해서 쓰라고 한다.
(하지만 JSX.Element를 사용하면 null은 반환이 안되기 때문에 )
interface AppProps = {
message: string;
};
// 컴포넌트를 정의하는 가장 쉬운 방법, 함수형 컴포넌트가 타입을 유추한다.
const App = ({ message }: AppProps) => <div>{message}</div>;
// 리턴 타입을 설정할 수 있고, 리턴타입이 아니면 에러가 난다.
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;
// 인라인으로 타입을 선언할 수 있다. interface 를 선언하지 않아도 되지만 반복되는 것 처럼 보인다.
const App = ({ message }: { message: string }) => <div>{message}</div>;
찬찬히 살펴보면
-
클래스형 컴포넌트는 ReactNode를 리턴한다.
- reactNode는 이런 타입이다
type ReactText = string | number; type ReactChild = ReactElement | ReactText; interface ReactNodeArray extends Array<ReactNode> {} type ReactFragment = {} | ReactNodeArray; type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
-
함수형 컴포넌트는 ReactElement를 리턴한다.
-
JSX는 바벨에 의해서 React.createElement(component, props, ...children) 함수로 트랜스파일된다
이런식으로 트랜스파일 되기 때문에 JSX와 ReactElement는 거의 동일하다고 보면 된다고 한다. 그래도 하나하나 뜯어보자
const jsx = <div>hello</div>
const ele = React.createElement("div", null, "hello"); //transpilation
- JSX.Element
- 리액트 코드를 열어보니 ReactElement의 props와 type이 any인 제네릭 타입을 가진 React.ReactElement 이다. 그리고 global 영역에 정의되어 있어 외부에서 변경이 가능하다.
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
- ReactElement
- reactElement는 type, props 를 가진 객체이다
type Key = string | number;
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
이런식으로 보면 이해하기 쉽다
<p> // <- ReactElement = JSX.Element
<Custom> // <- ReactElement = JSX.Element
{true && "test"} // <- ReactNode
</Custom>
</p>
사진출처: https://simsimjae.tistory.com/426 [104%]
😱 왜 클래스형 컴포넌트는 typescriptnode
를 리턴하고 함수형 컴포넌트는 typescriptElement
를 리턴할까 ,,,?
-
TS class : React/JS보다 더 큰 범위인 ReactNode를
render()
함수에서 반환render(): ReactNode; //이렇게 정의되어 있다
-
TS functional : React/JS보다 더 제한적인 JSX.Element | null 을 반환
🚦그래서 결론
위에서 나열한 FC의 문제가 있으니 JSX.Element를 리턴타입으로 사용하도록 하고 Null을 리턴해야 할 때에는 이렇게 쓰도록 하자
const TestComponent = ({ name, age }: TestProps): JSX.Element | null => {
return (
<div>
{name}, {age}
</div>
);
};
Return type을 선언하지 않았을 때도vscode가 JSX.Element로 추론해주는 것을 볼 수 있다