관리 메뉴

JAN's History

ReactNode, ReactElement, JSX.Element 완전 정복 (feat. 바벨과 createElement까지) 본문

React

ReactNode, ReactElement, JSX.Element 완전 정복 (feat. 바벨과 createElement까지)

JANNNNNN 2025. 4. 22. 16:10

리액트 + 타입스크립트를 사용하다보면 ReactNode, ReactElement, JSX.Element 같은 타입이 자주 등장하죠..

이게 처음엔 꽤나 헷갈립니다.ㅠㅠ

이 글에서는 이 세 가지 타입이 각각 무엇인지, 어떤 상황에서 사용하는지, 그리고 실제 예시까지 함께 정리해보겠습니다.

이번 글에서는 초보자도 확실히 이해할 수 있도록 아래 내용을 순서대로 설명해볼게요

  • JSX가 어떻게 동작하는지 (React.createElement + Babel)
  • ReactNode / ReactElement / JSX.Element의 차이
  • 각각 언제 사용해야 하는지
  • 예제 가득한 실전 코드
  • 최종 비교 정리표

먼저, JSX는 어떻게 동작할까?

우리가 흔히 쓰는 JSX는 사실 자바스크립트 문법이 아닙니다.
브라우저는 JSX를 이해하지 못해요.

그래서 개발 도구는 Babel 같은 트랜스파일러(transpiler) 를 이용해 JSX를 자바스크립트 코드로 바꿔줍니다.

JSX → JS로 바뀌는 과정

예를 들어 이런 JSX가 있다면?

const element = <h1>Hello React</h1>;

Babel은 아래처럼 바꿔줍니다:

const element = React.createElement("h1", null, "Hello React");

즉, JSX는 결국 React.createElement라는 함수로 변환됩니다.

그럼 React.createElement가 뭐길래?

React.createElement는 React 컴포넌트나 태그(div, span, h1 등)를 JS 객체로 만들어주는 함수예요.
이 객체는 브라우저에 실제로 렌더링되기 전에 내부적으로 처리되는 React 요소(React Element) 입니다.

이제 타입 개념으로 넘어가자 !

1. ReactElement

 

  • 정의: React.createElement()로 만들어지는 리액트 요소 (JSX 하나)
  • 정확한 타입: ReactElement<P = any, T extends string | JSXElementConstructor<any> = string>
  • 특징: JSX 하나만 들어올 수 있음
const title: ReactElement = <h1>제목</h1>; //OK
const text: ReactElement = "hello"; //XX 오류

 

언제 쓰나요?

  • JSX 하나만 받는 상황 / 컴포넌트의 리턴 타입을 JSX 하나로 제한할 때

2. JSX.Element

  • 정의: TypeScript가 JSX 표현식에 부여하는 타입
  • 내부적으로: ReactElement<any, any>와 거의 동일
  • 쓰는 곳: 함수형 컴포넌트의 반환 타입 명시할 때
function Header(): JSX.Element {
  return <h1>헤더</h1>;
}

언제 쓰나요?

  • 컴포넌트가 JSX를 반환할 때 리턴 타입으로 명시

3. ReactNode

  • 정의: React에서 렌더링 가능한 모든 타입을 포함하는 가장 넓은 범위의 타입
  • 정확한 타입:
type ReactNode =
  | ReactElement
  | string
  | number
  | boolean
  | null
  | undefined
  | ReactNode[];
type Props = {
  children: ReactNode;
};

<Box>Hello</Box>               ✅ 문자열
<Box>{123}</Box>               ✅ 숫자
<Box>{false}</Box>             ✅ boolean
<Box>{null}</Box>              ✅ null
<Box><h1>태그</h1></Box>       ✅ JSX
<Box>{[<p>1</p>, <p>2</p>]}</Box> ✅ 배열

언제 쓰나요?

  • children prop으로 모든 걸 받아야 할 때 (텍스트, 숫자, JSX 등)

실제 예제 비교

🔹 ReactNode 사용 예시

type BoxProps = {
  children: ReactNode;
};

function Box({ children }: BoxProps) {
  return <div>{children}</div>;
}

<Box>안녕하세요</Box>           ✅ 문자열
<Box>{42}</Box>                ✅ 숫자
<Box>{false}</Box>             ✅ boolean
<Box>{null}</Box>              ✅ null
<Box><span>JSX</span></Box>    ✅ JSX

🔹 ReactElement 사용 예시

type CardProps = {
  children: ReactElement;
};

function Card({ children }: CardProps) {
  return <div className="card">{children}</div>;
}

<Card><h2>제목</h2></Card>     ✅ OK
<Card>제목</Card>             ❌ Error (문자열은 안 됨)

🔹 JSX.Element 사용 예시

function Title(): JSX.Element {
  return <h1>타이틀입니다</h1>;
}

render props 패턴과 함께 심층 파악해보기!

먼저, “render props 패턴”이란

render props 패턴은 컴포넌트에게 함수를 prop으로 넘겨서, 그 함수를 통해 어떻게 렌더링할지부모가 직접 정하는 방식입니다.

예시

❌ 일반적인 방식

function UserCard({ name }: { name: string }) {
  return <div>{name}</div>;
}

<UserCard name="철수" />

☑️ render props 방식:

type UserCardProps = {
  render: () => JSX.Element;
};

function UserCard({ render }: UserCardProps) {
  return <div className="user-card">{render()}</div>;
}

<UserCard render={() => <strong>철수</strong>} />

즉, 내부에서 어떤 UI를 렌더링할지는 부모가 정해주는 거예요!
(이런 걸 '렌더링 전략을 외부에서 주입한다'고도 표현해요)

왜 이게 유용할까?

  • 재사용성 증가: UserCard가 어떤 내용을 렌더링할지 전혀 몰라도 됨.
  • 유연한 UI 구성: 필요한 만큼 props.render()로 원하는 JSX를 렌더링 가능.
  • 상태 기반 제어가 쉬움: render 함수에 상태도 넘길 수 있음.

JSX.Element와 무슨 관계야?

우리가 넘기는 render 함수는 JSX를 반환해야 하죠!

그래서 이런 타입을 쓰는 거예요

type Props = {
  render: () => JSX.Element;
};

즉, render라는 prop은 JSX 하나를 반환하는 함수가 돼요.
그 결과는 결국 React.createElement(...)로 변환돼서 렌더링돼요.

 

예제 1: render props + JSX.Element

type ProfileCardProps = {
  title: string;
  renderDetails: () => JSX.Element;
};

function ProfileCard({ title, renderDetails }: ProfileCardProps) {
  return (
    <div>
      <h2>{title}</h2>
      <div>{renderDetails()}</div>
    </div>
  );
}

// 사용 예시
<ProfileCard
  title="홍길동"
  renderDetails={() => (
    <ul>
      <li>나이: 24</li>
      <li>직업: 프론트엔드</li>
    </ul>
  )}
/>

renderDetails는 () => JSX.Element 타입이므로 JSX를 하나 반환해야 해요.

예제 2: ReactNode로 더 유연하게

만약 함수가 아니라 그냥 JSX 자체를 넘기고 싶다면?

type ProfileCardProps = {
  title: string;
  details: ReactNode;
};

function ProfileCard({ title, details }: ProfileCardProps) {
  return (
    <div>
      <h2>{title}</h2>
      <div>{details}</div>
    </div>
  );
}

// 사용 예시
<ProfileCard
  title="홍길동"
  details={
    <ul>
      <li>나이: 24</li>
      <li>직업: 프론트엔드</li>
    </ul>
  }
/>

예제 3 : 동적인 예제

type ToggleProps = {
  on: boolean;
  render: (on: boolean) => JSX.Element;
};

function Toggle({ on, render }: ToggleProps) {
  return <div>{render(on)}</div>;
}

// 사용
<Toggle
  on={true}
  render={(on) =>
    on ? <span>켜짐</span> : <span>꺼짐</span>
  }
/>

요약 비교 정리표

구분 포함 내용 주요 사용처
ReactNode JSX, 문자열, 숫자, null, 배열, boolean 등 다양한 children을 받을 때
ReactElement JSX 하나 (React.createElement의 결과) JSX 하나만 받는 prop / 내부 로직
JSX.Element JSX 반환값 타입 (ReactElement와 유사) 함수형 컴포넌트 반환 타입

실무 팁 & 권장사항

  • children 받는 컴포넌트는 무조건 ReactNode로!
  • ReactElement는 여러 값 허용 X → 정확한 제어가 필요할 때만 사용
  • JSX.Element는 함수 컴포넌트의 반환 타입에 주로 사용

React에서 타입을 잘 이해하는 건 컴포넌트를 정확하게 설계하고, 실수를 줄이는 데 큰 도움이 되죠!
특히 TypeScript와 함께 쓰면 ReactNode, ReactElement, JSX.Element의 역할을 잘 구분해서 써야 하는데 꽤 헷갈리네요 .. 

이 글로 개념이 좀 더 또렷해졌다면, 꼭 직접 몇 가지 컴포넌트를 만들어 보면서 연습해보세요!