ReactElement vs ReactNode vs JSX.Element

OROSY·2022년 1월 9일
10

TypeScript

목록 보기
1/1

ReactElement vs ReactNode vs JSX.Element

부트캠프를 수료하고 현업에서 일을 하게된 지 이제 약 한 달이 지나갔습니다. 수료 후, 취업을 준비한 11월과 결국 문과생에서 개발자로 전향을 하게 된 12월, 두 달간의 짧은 기간 사이에 많은 일이 있었고 저의 심경에도 여러가지 많은 변화가 있던 시기였던 것 같습니다. 취업을 하게 된 이야기와 면접, 개발자의 업무 등에 대해서는 추후 자세히 이야기를 나눠보도록 하겠습니다.

현재 저는 회사에서 Next.js와 TypeScript 기반으로 프론트엔드 개발을 진행하고 있습니다. 두 가지 물론 React와 JS의 기반이지만, 모두 새롭게 배우게 된 프레임워크와 언어로서 부담이 되었던 것은 사실입니다.

특히, 현재 맡은 프로젝트를 진행하면서 TypeScript에서 Type 중에 ReactNode가 있었고, 이에 대해 사수님께서 위 3가지의 차이점에 대해 아시고 있냐는 물음에 대답을 하지 못했습니다. 이를 공부하라고 말해주신 덕분에 이렇게 오랜만에 블로그 글을 쓰게 되었습니다.

React Component의 Type을 정해주는 것

위에서 이야기한 3가지를 포함하여 ReactChild, ReactChildren 모두 타입스크립트에서 리액트 컴포넌트의 타입을 정해주는 것들입니다.

3가지의 관계를 그림으로 나타내면 위와 같이 나타낼 수 있으며, 위 3가지를 중점적으로 알아보도록 하겠습니다.

[배경지식] 1. Name Space

위에서 이야기하는 네임 스페이스란 세 가지의 개념을 이해하기 위해 알아야할 개념입니다.

  • 네임 스페이스 (Name Space): 구분이 가능하도록 정해놓은 범위나 영역. 이름을 한 곳에 모아 충돌을 방지하고 해당 이름으로 선언된 변수는 함수를 쉽게 가져다 쓸 수 있도록 만든 메커니즘.
    💡 네임 스페이스 패턴(Name Space Pattern)을 검색하면 자세한 개념을 알 수 있습니다.
    JavaScript: 네임스페이스 패턴 바로 알기

자세한 내용은 위 글을 참조하는 것을 추천드립니다.

[배경지식] 2. JSX

JSX는 JavaScript의 확장 문법이라고 보시면 됩니다. React에서 작성하시는 코드는 대부분 JSX 코드입니다.

클래스형 컴포넌트는 render 메소드에서 ReactNode를 리턴합니다. 함수형 컴포넌트는 ReactElement를 리턴합니다. JSX는 바벨에 의해서 React.createElement(component, props, ...children) 함수로 트랜스파일 됩니다.

html 처럼 생긴 문법을 리액트 라이브러리의 렌더링 함수로 변환하는 것입니다. 그래서 JSX를 사용하지 않고도 리액트를 사용할 수 있으나 이렇게 하면 매우 불편합니다.

//jsx
<div>Hello {this.props.toWhat}</div>
<Hello toWhat="World" />
// transpile 
React.createElement('div', null, `Hello ${this.props.toWhat}`); 
React.createElement(Hello, {toWhat: 'World'}, null);

React.createElement의 리턴 타입이 바로 ReactElement와 JSX.Element입니다.

1. ReactElement

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null; }

React.createElement를 호출하면 이런 타입의 객체가 리턴됩니다. 단순하게 리액트 컴포넌트를 JSON 형태로 표현해놨다고 생각하면 됩니다. ReactElement는 type과 props를 가진 객체입니다.

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

ReactNode는 ReactElement의 superset입니다. ReactNode는 ReactElement일 수 있고 ReactFragment, string, number, ReactNode의 Array, null, undefined, boolean 등의 좀 더 유연한 타입 정의라고 할 수 있습니다.

3. JSX.Element

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> {}
  }
}

JSX.Element는 ReactElement의 특정 타입이라고 생각하면 됩니다. JSX.Element는 props와 type이 any인 제네릭 타입을 가진 ReactElement입니다. JSX는 글로벌 네임 스페이스에 있기 때문에 다양한 라이브러리에서 자체 라이브러리의 방법대로 실행될 수 있습니다.

4. 예시

<p> // ReactElement = JSX.Element
  <Custom> // ReactElement = JSX.Element
    {true && 'test'} // ReactNode
  </Custom>
</p>

그렇다면 왜 클래스형 컴포넌트의 render()ReactNode를 리턴하고 함수형 컴포넌트ReactElement를 리턴할까요?

  • TS class component: returns ReactNode with render(), more permissive than React/JS

  • TS function component: returns JSX.Element | null, more restrictive than React/JS

In principle, render() in React/JS class components supports the same return types as a function component. With regard to TS, the different types are a type inconsistency still kept due to historical reasons and the need for backwards-compatibility.

위의 이유를 참고하면 되지만, 저도 읽었지만 아직 정확한 이유를 파악하지는 못한 것 같습니다. 혹시 해당 의미를 알고 계시다면, 댓글로 알려주시면 감사하겠습니다 😄😄😄

결국, 결론은 ReactNode 이외에는 null 타입을 가지지 않으므로 ReactElement를 리턴하는 함수형 컴포넌트에서는 아래와 같이 null을 union 해줘야 합니다.

const example = (): ReactElement | null => {
  if(/* true조건 */) return null;
    
  return <p>Hello World</p>;
};

이렇게 ReactElement, ReactNode, JSX.Element 3가지의 개념의 차이에 대해 알아보았습니다. 아직 타입스크립트에 온전한 이해를 바탕으로 글을 작성한 것이 아니기에 추후 더욱 공부를 하여 깊은 이해를 하고 싶은 내용이었습니다.

관련하여 덧붙이고 싶으신 내용은 언제든 댓글로 남겨주시기 바라겠습니다. 그럼 저는 또 다음 글에서 뵙도록 하겠습니다.

참고 사이트

Winney_Seo님 블로그
심심재님 블로그

profile
Life is a matter of a direction not a speed.

0개의 댓글