본문 바로가기

호기심 천국

React.ReactNode vs JSX.Element

참새의 모래목욕. Unsplash 에 Nagara Oyodo 님이 올림.

들어가는 말

타입스크립트 환경에서 리액트 컴포넌트를 만들 때 가끔 가다 props로 다른 컴포넌트를 받아야 할 때가 옵니다. 자식은 사실 React.PropsWithChildren 이라는 친구를 쓰면 뚝딱입니다. 하지만 ErrorBoundary처럼 children이 아닌 fallback처럼 특수한 경우에는 무엇을 쓰는 게 좋을까요? 선택지는 크게 두 가지입니다. React.ReactNodeJSX.Element죠. 제가 알기론 많은 분들이 React.ReactNode를 사용합니다. 과연 그 이유는 무엇일까요?

React.ReactNode

React.ReactNode의 정의는 간단합니다.

Represents all of the things React can render.

 

리액트가 렌더링할 수 있는 모든 것들이라고 하네요. 우리가 보통은 JSX만을 사용하지만 return null 으로 특정 조건에서는 컴포넌트가 안 보이게 할 수도 있는 것처럼 JSX 말고 다른 친구들도 전부 포함합니다.

JSX.Element

JSX.Element는 단순하게 React.ReactElement<any, any>를 extend한 친구입니다. 그러면  React.ReactElement는 누구인지 알아봐야겠죠? 얘는 평범한 객체입니다.

 

DefinitelyTyped에서의 정의
React에서의 정의

 

그리고 이렇게 적혀있죠.

Where ReactNode represents everything that can be rendered, ReactElement only represents JSX.

 

React.ReactElement는 JSX를 표현하는 방식이었습니다.

둘 사이의 포함관계

쉽게 보면 React.ReactNodeJSX.Element를 포함합니다. 이건 타입스크립트로도 살펴볼 수 있는데요. 그 방법도 소개하고자 합니다.

npm init
npm install typescript @types/react
// index.ts

const reactNodeNeeded = (param: React.ReactNode) => {};
const jsxElementNeeded = (param: JSX.Element) => {};
const reactJsxNeeded = (param: React.JSX.Element) => {};
const reactElementNeeded = (param: React.ReactElement) => {};

let testReactNode!: React.ReactNode;
reactNodeNeeded(testReactNode);
jsxElementNeeded(testReactNode);
reactJsxNeeded(testReactNode);
reactElementNeeded(testReactNode);

let testJsxElement!: JSX.Element;
reactNodeNeeded(testJsxElement);
jsxElementNeeded(testJsxElement);
reactJsxNeeded(testJsxElement);
reactElementNeeded(testJsxElement);

let testReactJsxElement!: React.JSX.Element;
reactNodeNeeded(testReactJsxElement);
jsxElementNeeded(testReactJsxElement);
reactJsxNeeded(testReactJsxElement);
reactElementNeeded(testReactJsxElement);

let testReactElement!: React.ReactElement;
reactNodeNeeded(testReactElement);
jsxElementNeeded(testReactElement);
reactJsxNeeded(testReactElement);
reactElementNeeded(testReactElement);

 

먼저 네 개의 함수를 만들어 줍니다. 각 함수의 param으로 React.ReactNode, JSX.Element를 받을 수 있게 해 줍니다. global JSX namespace의 경우 "@deprecated — Use React.JSX instead of the global JSX namespace." 라는 안내 문구가 있기 때문에 React.JSX.Element도 추가했구요. 마지막으로 원형이라고 할 수도 있는 React.ReactElement까지 추가했습니다.

 

그리고 밑에서 각 타입을 갖는 변수를 선언하고 함수에 넣어보면 뭐가 되고 뭐가 안 되는지를 확인할 수 있습니다. 직접 해 보기에는 귀찮으니 결과 스크린샷도 첨부합니다.

 

코드 작성 결과. 빨간 줄에 주목하세요!

 

React.ReactNode는 사실 null, undefined, string, iterable 등 JSX 말고도 다양한 친구들이 들어올 수 있어서 호환이 안 되는 게 당연합니다. 하지만 신기하게도 나머지 JSX.Element, React.JSX.Element , React.ReactElement끼리는 호환이 되는데요. 실제로 타입 정의를 살펴 보면 나머지 세 친구들은 React.ReactElement를 열심히 extend 하고는 있지만, 그 과정에서 별다른 제약이 추가되고 있지는 않습니다. 따라서 타입스크립트의 구조적 타이핑 으로 인해 셋 모두가 같은 멤버만을 공유해서 타입의 이름은 달라도 서로 상호 호환되는 타입이 된 것이죠.

 

결론은 React.ReactNode가 더 넓은 범위입니다.

결론: 그럼 뭘 써야 할까요?

사실 이쯤 되면 결론은 나온 것 같긴 해요. 웬만한 상황에서는 React.ReactNode를 쓰는 게 더 좋을 것입니다. 저희가 JSX만을 받아야만 하는 경우가 아니라면 말이죠.

 

직관 말고도 리액트를 살펴보면 객관적인 근거들이 몇 개 있습니다.

React.ReactNode

React.PropsWithChildren
함수형 컴포넌트의 반환값
클래스형 컴포넌트의 render() 메서드의 반환값

 

이 친구들 모두 React.ReactNode를 사용합니다.

JSX.Element

jsx-runtime

 

JSX.Element는 jsx-runtime이라는 친구에서만 사용합니다. 이 친구는 jsx()라는 함수의 결과물로 보여요. 하지만 이 함수는 일반적인 개발자를 위한 게 아닌 컴파일러를 위한 함수이므로 우리가 쓸 일은 없다고 봐도 무방합니다.

참고 자료

@types/react
react/ReactElementType.js
Type Compatibility