ReactNode, ReactElement, JSXElement

Y·2022년 4월 22일
2
post-thumbnail

개발을 하면서, 리액트 컴포넌트의 타입에 ReactNode, ReactElement..를 정확히 알지 못하고
중구난방으로 사용하고 있었다.
이번 기회에 정확한 차이점을 알아보고, 어느 경우에 어떠한 타입을 사용할지 정리해보고자 한다.


@types/react는 리액트 컴포넌트의 리턴 타입을 정의할때
크게 3가지를 사용한다

1. ReactElement
2. JSX.Element
3. ReactNode

1. ReactElement

type Key = string | number;

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    type: T;
    props: P;
    key: Key | null;
}
  • JSX에서 컴포넌트를 작성하면 react.createElement를 사용하여 코드를 변환하는데, react.createElement의 리턴 타입이 ReactElementJSX.Element이다
    class Hello extends React.Component {
      render() {
        return <div>Hello {this.props.name}</div>;
      }
    }
    
    ReactDOM.render(
      // React.createElement(Hello, {name: '홍길동'}, null)
      <Hello name="홍길동" />,
      document.getElementById('root')
    );
  • ReactNode 와는 달리 원시 타입을 허용하지 않고 완성된 jsx 요소만을 허용하는데, 아래의 ReactNode를 보면 ReactNode 타입이 ReactElement 타입을 포함하고 있는 관계임을 알 수 있습니다

2. JSX.Element

declare global {
	namespace JSX {
		interface Element extends React.ReactElement<any, any> {
	}
}
  • JSX.Element 는 ReactElement 를 상속하는 개념으로, types와 props를 any로 하는 지네릭 타입을 가진 ReactElement로 볼 수 있다
  • JSX는 React의 global namespace에 있기 때문에, 다양한 라이브러리들이 자체적으로 JSX를 구현하여 사용할 수 있다

3. ReactNode

type ReactChild = ReactElement | ReactText;

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
  • ReactNode 타입은 jsx 내에서 사용할 수 있는 모든 요소의 타입을 의미하는데, 즉 stringnullundefined 등을 포함하는 가장 넓은 범위를 갖는 타입이다
  • 가장 “느슨한" 타입으로, React 클래스 컴포넌트의 render()로 리턴되는 모든 것들이 해당될 수 있다

무엇을 언제 사용할까?

  • 다른 리액트 컴포넌트를 children으로 받는 대부분의 상황에서 ReactNode가 가장 유연하게 사용될 수 있을 것 같습니다.
  • ReactElementJSX.ElementReactNode보다 더 "strict" 하다고 표현하는데, 위에서 봤듯이 이들은 null, string 과 같은 원시값을 리턴할 수 없습니다. 따라서 이를 사용하고 있다면 null타입을 union 해줘야한다.
    const Example = (): JSX.Element | null => {
      if(/* true조건 */) return null;
        
      return <p>Hello World</p>;
    };
  • definitelyTyped 에서 사용예시를 확인해보면,
    interface FunctionComponent<P = {}> {
      (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
      propTypes?: WeakValidationMap<P> | undefined;
      contextTypes?: ValidationMap<any> | undefined;
      defaultProps?: Partial<P> | undefined;
      displayName?: string | undefined;
    }
    ReactElement<any, any> | null 이렇게도 사용하고 있다
  • ReactElementJSX.Element는 거의 동일하다고 보는데, ReactNode보다 더 strict하게, 예를 들어 자식 요소로 하나의 컴포넌트를 받는 것을 강제하고 싶은 상황 등에서 사용할 수 있을 것 같다.

+ 추가내용

1. React.FunctionComponent (React.FC)

React.FC는 암묵적으로 children을 가지고 있습니다.

따라서 children prop을 따로 명시하지 않아도 오류가 나지 않는다.

export const Greeting:FC<GreetingProps> = ({ name }) => {
  // name is string!
  return <h1>Hello {name}</h1>
};

// 사용
const App = () => <>
  <Greeting name="Stefan">
    <span>{"I can set this element but it doesn't do anything"}</span>
  </Greeting>
</>

단점이자 장점으로 작용할 것 같은데, children이 암묵적으로 사용되는 것보단 필요할 때 명시적으로 선언해서 사용하는 것이 좋을 것 같다.

이 외에도 React.FC 사용을 지양하자는 내용이 있어서 참고해보면 좋을 것 같다 (하단의 첫번째 링크)

2. React.PropsWithChildren

현재 ReactNode를 children으로 갖는 경우에 대해서 따로 WithChildren이라는 헬퍼 타입을 만들어서 사용하고 있었다

export type WithChildren<T = Record<string, unknown>> = T & {
  children?: React.ReactNode
}

그러나 이미 react에 PropsWithChildren이 존재한다.

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

따라서 기존의 코드를 변경해볼 수 있겠다.

// components > PopupModal > index.tsx
import { PropsWithChildren } from 'react'

interface Props {
  message: string
}

export const PopupModal = ({ children, message }: PropsWithChildren<Props>) => {

다만 children만 prop으로 가지는 경우에는 PropsWithChildren<unknown> 으로 사용해야한다.


[References]

profile
기록중

0개의 댓글