들어가는 말
타입스크립트 환경에서 리액트 컴포넌트를 만들 때 가끔 가다 props로 다른 컴포넌트를 받아야 할 때가 옵니다. 자식은 사실 React.PropsWithChildren
이라는 친구를 쓰면 뚝딱입니다. 하지만 ErrorBoundary처럼 children이 아닌 fallback처럼 특수한 경우에는 무엇을 쓰는 게 좋을까요? 선택지는 크게 두 가지입니다. React.ReactNode
와 JSX.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.ReactNode
가 JSX.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.Element
는 jsx-runtime이라는 친구에서만 사용합니다. 이 친구는 jsx()
라는 함수의 결과물로 보여요. 하지만 이 함수는 일반적인 개발자를 위한 게 아닌 컴파일러를 위한 함수이므로 우리가 쓸 일은 없다고 봐도 무방합니다.
참고 자료
'호기심 천국' 카테고리의 다른 글
리액트 동시성이란 (4) | 2024.03.09 |
---|---|
CSS) border vs outline (0) | 2024.02.23 |
리액트 서버 컴포넌트 vs 서버 사이드 렌더링 (0) | 2024.01.27 |
타입스크립트는 자바스크립트의 상위집합(superset)일까? (4) | 2023.09.30 |
React) useState는 클로저로 동작할까? (4) | 2023.09.11 |