본문으로 건너뛰기

컴포넌트 타입

클래스 컴포넌트의 render 메서드와 함수 컴포넌트의 반환 타입인 React.ReactElement, JSX.Element, React.ReactNode의 차이와 기본 HTML 요소를 감싸는 컴포넌트를 만들 때 DetailedHTMLPropsComponentPropsWithoutRef 중 어떤 타입을 사용하는 것이 적절한지 비교합니다.

render 메서드와 함수 컴포넌트의 반환 타입

React.ReactElement

React.ReactElement는 JSX가 변환된 결과를 표현하는 객체 타입입니다.

React 타입 정의를 단순화하면 다음과 같습니다.

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

예를 들어 아래 JSX는

const element = <button disabled>저장</button>;

개념적으로 다음과 같은 객체 형태로 볼 수 있습니다.

const element = {
type: 'button',
props: {
disabled: true,
children: '저장',
},
key: null,
};

ReactElement는 실제 DOM이 아니라, React가 어떤 UI를 렌더링해야 하는지 저장해 둔 객체 포맷입니다.

JSX.Element

JSX.Element는 TypeScript가 JSX 표현식의 결과로 보는 타입입니다.

React 타입 정의에서는 다음처럼 JSX.ElementReact.ReactElement<any, any>를 확장합니다.

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

그래서 React 환경에서는 ReactElement<any, any>JSX.Element가 거의 같은 타입처럼 동작합니다.

const element: JSX.Element = <div />;
const sameElement: React.ReactElement = <div />;

다만 의미는 조금 다릅니다. ReactElement는 React element 객체의 구조를 설명하는 타입이고, JSX.Element는 JSX 문법의 결과 타입이라는 역할을 가집니다.

React.ReactNode

React.ReactNode는 React가 렌더링할 수 있는 모든 값을 포함하는 타입입니다.

ReactElementJSX.Element보다 훨씬 넓은 타입입니다.

type ReactNode =
| ReactElement
| string
| number
| bigint
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;

아래 값들은 모두 ReactNode가 될 수 있습니다.

const elementNode: React.ReactNode = <div />;
const textNode: React.ReactNode = 'hello';
const numberNode: React.ReactNode = 123;
const emptyNode: React.ReactNode = null;

이 차이 때문에 children 타입에는 보통 ReactNode를 사용합니다.

type LayoutProps = {
children: React.ReactNode;
};

function Layout({ children }: LayoutProps) {
return <main>{children}</main>;
}

children에는 JSX뿐 아니라 문자열, 숫자, null, 배열 등도 들어올 수 있기 때문입니다.

반환 타입 비교

타입의미주로 쓰는 곳
React.ReactElementReact가 JSX를 객체로 저장하기 위한 포맷React element 객체를 직접 다룰 때
JSX.ElementJSX 표현식의 결과 타입컴포넌트가 JSX element 하나를 반환한다는 의도를 드러낼 때
React.ReactNodeReact가 렌더링할 수 있는 모든 값children, 조건부 렌더링 결과처럼 넓은 값을 받을 때

기본 HTML 요소 사용

재사용 컴포넌트를 만들 때 기본 HTML 요소의 props를 확장해야 하는 경우가 많습니다.

예를 들어 button을 감싼 Button 컴포넌트를 만든다면 onClick, disabled, type 같은 기본 button 속성을 그대로 받을 수 있어야 합니다.

이때 자주 비교되는 타입이 DetailedHTMLPropsComponentPropsWithoutRef입니다.

DetailedHTMLProps

DetailedHTMLProps는 특정 HTML 요소가 받을 수 있는 속성을 자세하게 표현할 때 사용하는 타입입니다.

이 타입을 사용하면 기본 HTML 속성과 함께 ref까지 props에 포함됩니다.

type ButtonProps = React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;

function Button(props: ButtonProps) {
return <button {...props} />;
}

이 방식은 어떤 HTML 속성 타입을 조합하는지 직접 드러나지만, ref를 실제로 전달하지 않는 컴포넌트에서도 props에 ref가 포함된다는 점을 주의해야 합니다.

ComponentPropsWithoutRef

ComponentPropsWithoutRef는 특정 HTML 요소나 컴포넌트가 받을 수 있는 props에서 ref를 제외한 타입입니다.

type ComponentPropsWithoutRef<T extends ElementType> =
PropsWithoutRef<ComponentProps<T>>;

기본 HTML 요소를 감싸지만 ref를 외부로 전달하지 않는 컴포넌트라면 이 타입이 더 안전합니다.

type ButtonProps = React.ComponentPropsWithoutRef<'button'>;

function Button({ children, ...props }: ButtonProps) {
return <button {...props}>{children}</button>;
}

이렇게 하면 button이 받을 수 있는 기본 속성은 그대로 가져오되, ref는 props에 포함하지 않습니다.

DetailedHTMLProps와 ComponentPropsWithoutRef 비교

타입가져오는 속성ref 포함 여부사용하기 좋은 경우
DetailedHTMLProps지정한 HTML 요소의 속성포함ref까지 포함한 HTML props를 직접 조합해야 할 때
ComponentPropsWithoutRef<'button'>JSX의 button props에서 ref를 제거한 타입미포함ref를 전달하지 않는 일반 래퍼 컴포넌트

ref를 실제로 전달하지 않는 컴포넌트에서 props 타입에 ref가 포함되어 있으면, 사용하는 쪽에서는 ref를 넘길 수 있다고 기대할 수 있습니다.

하지만 컴포넌트 내부에서 forwardRef를 사용하지 않는다면 그 ref는 실제 DOM 요소까지 전달되지 않습니다. 타입은 허용하는데 동작은 기대와 달라지는 상황이 생길 수 있습니다.

그래서 기본 HTML 속성을 확장하는 props를 설계할 때는 보통 ComponentPropsWithoutRef를 먼저 고려하는 것이 안전합니다.

type InputProps = React.ComponentPropsWithoutRef<'input'> & {
label: string;
};

function Input({ label, id, ...props }: InputProps) {
return (
<label>
{label}
<input id={id} {...props} />
</label>
);
}

반대로 ref를 외부에서 받아 실제 DOM 요소로 넘겨야 한다면 forwardRef와 함께 ref 타입을 명시합니다.

type InputProps = React.ComponentPropsWithoutRef<'input'> & {
label: string;
};

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
},
);

정리하면 ref를 받지 않는 컴포넌트는 ComponentPropsWithoutRef, ref를 실제로 전달하는 컴포넌트는 forwardRef와 함께 ref 타입을 설계하는 식으로 구분할 수 있습니다.

예상 질문

ReactElement, JSX.Element, ReactNode는 어떻게 다른가요?

ReactElement는 JSX가 변환된 React element 객체 타입입니다. JSX.Element는 JSX 표현식의 결과 타입이며, React 타입 정의에서는 ReactElement<any, any>를 확장합니다. ReactNode는 이보다 더 넓어서 ReactElement뿐 아니라 문자열, 숫자, null, undefined, 배열처럼 React가 렌더링할 수 있는 값을 포함합니다.

HTML props를 확장할 때 DetailedHTMLProps보다 ComponentPropsWithoutRef를 선호하는 이유는 무엇인가요?

DetailedHTMLPropsref까지 props에 들어옵니다. ref를 실제로 전달하지 않는 컴포넌트라면 사용하는 쪽에서 ref를 넘길 수 있다고 오해할 수 있습니다. 그래서 ref를 받지 않는 래퍼 컴포넌트는 ComponentPropsWithoutRef로 HTML 속성만 가져오고, ref가 필요할 때는 forwardRef와 함께 별도로 설계하는 편이 안전합니다.

공유 사항

  1. 우아한 타입스크립트 with 리액트 교재에서는 "React.FC는 암묵적으로 children을 포함하고 있다"라고 기술하고 있으나, React 18 타입에서는 이 동작이 제거됨 참고
  2. keyoftypeof 를 사용한 객체 속성 타입 추론

as const를 사용하지 않고 typeof fruits를 사용하면 string으로 추론됩니다.

// keyof
const fruits = {apple: "사과", banana : "바나나", blueberry: "블루베리"};

type FruitsKeys = keyof typeof fruits // 'apple' | 'banana' | 'blueberry'

type FruitsValues = typeof fruits[FruitsKeys] // string