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)를 잘 쓰지 않는 이유

출처: Facebook React FC issue

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>;

출처 - typescript cheatsheets

찬찬히 살펴보면

  1. 클래스형 컴포넌트는 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;
  2. 함수형 컴포넌트는 ReactElement를 리턴한다.

  3. 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>

image

사진출처: 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로 추론해주는 것을 볼 수 있다

image

출처:https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement?rq=1

https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement/59840095#59840095