main

TypeScript에서 React children 타입 정하기

Articles
6

어떤 타입을 써야할까?

타입스크립트와 리액트로 프로젝트를 개발하다 보면 컴포넌트에서 children prop으로 받아서 사용하고 이에 대한 인터페이스를 정해줘야하는 경우가 있습니다.

export default function Component({children}){
  return (
    <div>
      {children}
    </div>
  )
}

이럴때마다 항상 children에 어떤 타입을 명시해야하는 고민이 있었어서 이에 대한 것을 알아보고자 합니다.

React.ReactElement

function Parent({children: React.ReactElement}){
  return children
}
 
// 에러 발생
function Test(){
  return <Parent>{null}</Parent>
}

React.ReactElementReact 함수형 컴포넌트의 반환타입입니다.
해당 타입은 오직 JSX요소만 받도록 강제할 수 있기 때문에 위와같이 원시타입을 넣게 된다면 에러를 내뱉게 됩니다.
따라서 함수형 컴포넌트와 같이 JSX 요소만을 children으로 받고 싶을 때 사용하는 것이 좋을 것 같습니다.

JSX.Element

function Parent({children: React.ReactElement}){
  return children
}
 
// 에러 발생
function Test(){
  return <Parent>{null}</Parent>
}

JSX.ElementReactElement의 특정 타입이라고 생각하면 됩니다. JSX.Element라고 자식을 명시한다면 ReactElement와 같이 원시타입은 자식에 할당 될 수 없습니다.

const element = <div>Hello, React!</div>;
const element = React.createElement("div", null, "Hello, React!");

위와 같은 JSX도 결국 React.createElement 함수 호출로 변환되기때문에 JSX.ElementReactElement의 부분집합이라고 생각할 수 있습니다.

React.FC (!! Not to use)

const Example: React.FC = ({ children }) => <div>{children}</div>;

18버전 이전에는 FC를 사용해서 함수형 컴포넌트를 선언한다면 children을 암시적으로 사용할 수 있었습니다.
하지만 18 버전 이후에는 children관련 선언이 모두 제거되었습니다.

  • children이라는 암묵적이었던 일종의 키워드는 자식 노드가 필요한지 아닌지의 의도를 명확하게 드러낼 수 없다는 단점이 있었습니다. (FC으로 선언해버리는 경우, children이 필요하든 안필요하든 암시적으로 선언이 되어버리기 때문에... )
  • 이런점은 타입의 관점에서 크리티컬한 문제였습니다. 타입에 자식 노드가 꼭 필요한 컴포넌트에 자식 노드를 넣지 않았을 경우 혹은 그 반대의 경우를 타입으로 잡아내지 못한다는 단점이 있었습니다.
type Props = {
  children: React.ReactNode;
};
 
const Example: React.FC<Props> = ({ children }) => <div>{children}</div>;

따라서 앞으로 FC를 사용하게 된다면 위와 같이 명시적으로 선언해주어야합니다.

React.PropsWithChildren

import { PropsWithChildren } from 'react'
 
type FooProps = {
  name: 'foo'
}
 
export const Foo = (props: PropsWithChildren<FooProps>) => {
    return props.children
}

PropsWithChildrentype으로 명시해 제네릭으로 사용한다면, 내부적으로 children이 명시되어 사용할 수 있습니다.
보일러플레이트가 덜 필요하고, 프로퍼티가 암시적으로 입력되는 장점이 있습니다.

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

하지만 단점으로는 children이 옵셔널해서 컴포넌트에서 children이 필수인 값이라면 적합하지 않을 수도 있습니다.
또한 childrenReactNode 타입으로 지정되므로 JSX 요소만이 오게 강제하고 싶을 때는 유용하지 않습니다.

ReactNode

import { ReactNode } from 'react'
 
type FooProps = {
  name: 'foo'
  // look here 👇
  children: ReactNode
}
 
export const Foo = (props: FooProps) => {
    return props.children
}

가장 광범위하게 사용할 수 있는 React 컴포넌트 타입입니다. 또한 클래스형 리액트 컴포넌트의 반환 타입이기도 합니다.

type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | Boolean | null | undefined;

ReactNode의 타입명세를 보면 string, number, null, undefined 와 같은 원시타입과 ReactElement, ReactFragment등의 타입을 모두 포함하고 있습니다.
또한 children에 대한 타입을 ReactNode임을 매번 명시해야합니다.

React.ReactChild (‼️deprecated)

type ReactChild = ReactElement | string | number;

React 18부터 사용을 지양하는 타입입니다. 제 개인적인 의견으로는 타입 지정이 중구난방으로 되어 있어 지양하는 것 같습니다. (ReactNode처럼 다양한 타입을 받는 것도 아니고...)

Reference Doc


김다은 이모지
Daeun Kim
Junior Frontend Engineer