컴포넌트 타입
클래스 컴포넌트의 render 메서드와 함수 컴포넌트의 반환 타입인 React.ReactElement, JSX.Element, React.ReactNode의 차이와 기본 HTML 요소를 감싸는 컴포넌트를 만들 때 DetailedHTMLProps와 ComponentPropsWithoutRef 중 어떤 타입을 사용하는 것이 적절한지 비교합니다.
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.Element가 React.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가 렌더링할 수 있는 모든 값을 포함하는 타입입니다.
ReactElement나 JSX.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.ReactElement | React가 JSX를 객체로 저장하기 위한 포맷 | React element 객체를 직접 다룰 때 |
JSX.Element | JSX 표현식의 결과 타입 | 컴포넌트가 JSX element 하나를 반환한다는 의도를 드러낼 때 |
React.ReactNode | React가 렌더링할 수 있는 모든 값 | children, 조건부 렌더링 결과처럼 넓은 값을 받을 때 |
기본 HTML 요소 사용
재사용 컴포넌트를 만들 때 기본 HTML 요소의 props를 확장해야 하는 경우가 많습니다.
예를 들어 button을 감싼 Button 컴포넌트를 만든다면 onClick, disabled, type 같은 기본 button 속성을 그대로 받을 수 있어야 합니다.
이때 자주 비교되는 타입이 DetailedHTMLProps와 ComponentPropsWithoutRef입니다.
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를 선호하는 이유는 무엇인가요?
DetailedHTMLProps는 ref까지 props에 들어옵니다. ref를 실제로 전달하지 않는 컴포넌트라면 사용하는 쪽에서 ref를 넘길 수 있다고 오해할 수 있습니다. 그래서 ref를 받지 않는 래퍼 컴포넌트는 ComponentPropsWithoutRef로 HTML 속성만 가져오고, ref가 필요할 때는 forwardRef와 함께 별도로 설계하는 편이 안전합니다.
공유 사항
- 우아한 타입스크립트 with 리액트 교재에서는 "React.FC는 암묵적으로 children을 포함하고 있다"라고 기술하고 있으나, React 18 타입에서는 이 동작이 제거됨 참고
keyof와typeof를 사용한 객체 속성 타입 추론
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