React에서 children은 문법만 보면 특별한 값처럼 보입니다. <Card>내용</Card>처럼 태그 사이에 작성하기 때문에 일반 props와 별개의 영역처럼 느껴지기 쉽습니다. 하지만 TypeScript로 컴포넌트를 작성할 때는 결국 props 객체 안에 들어오는 값입니다. 컴포넌트가 태그 사이의 내용을 받을 수 있다면, props 타입에도 children을 받을 자리를 만들어야 합니다.
일반 props 타입을 정리한 뒤 children에서 다시 막히는 이유는 타입 이름이 여러 개 나오기 때문입니다. ReactNode, PropsWithChildren, ReactElement, JSX.Element가 한꺼번에 보이면 어떤 타입이 정답인지 찾게 됩니다. 이 글에서는 타입 이름을 외우는 방식이 아니라, 컴포넌트가 어떤 자식 값을 허용해야 하는지부터 판단하는 방식으로 정리합니다.
children은 props인데 왜 따로 헷갈릴까

아래처럼 작성한 JSX를 보면 Card 컴포넌트는 title만 props로 받는 것처럼 보입니다. 하지만 태그 사이에 들어간 문장도 함께 전달됩니다.
import type { ReactNode } from "react";
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
export default function ProfilePage() {
return (
<Card title="프로필">
<p>사용자 정보를 확인하는 영역입니다.</p>
</Card>
);
}
여기서 <p>사용자 정보를 확인하는 영역입니다.</p>는 Card의 children으로 들어갑니다. title과 children은 작성 위치만 다를 뿐, 둘 다 컴포넌트가 받는 props입니다.
TypeScript 입장에서는 JSX 태그 사이에 값이 들어왔다는 사실보다, CardProps에 그 값을 받을 타입이 있는지가 더 중요합니다. children을 타입에 적지 않으면 컴포넌트 사용부에서 내용을 넣었을 때 타입 오류가 날 수 있습니다.
type CardProps = {
title: string;
};
function Card({ title }: CardProps) {
return <section>{title}</section>;
}
function ProfilePage() {
return (
<Card title="프로필">
<p>이 문장은 받을 타입이 없습니다.</p>
</Card>
);
}
이런 코드는 화면 구조만 보면 자연스럽지만, 타입 기준으로는 Card가 children을 받겠다고 선언하지 않은 상태입니다. 그래서 children 타입 지정의 출발점은 “React가 알아서 처리해주겠지”가 아니라 “이 컴포넌트가 태그 사이 값을 받을 수 있어야 하는가”입니다.
가장 많이 쓰는 타입은 ReactNode
일반적인 UI 컴포넌트의 children 타입은 대부분 ReactNode로 시작하면 됩니다. ReactNode는 React가 렌더링할 수 있는 값을 넓게 포함합니다. 문자열, 숫자, JSX 요소, 여러 JSX 요소, null, undefined처럼 조건부 렌더링에서 자주 나오는 값까지 받을 수 있습니다.
import type { ReactNode } from "react";
type PageLayoutProps = {
children: ReactNode;
};
function PageLayout({ children }: PageLayoutProps) {
return (
<main className="page-layout">
{children}
</main>
);
}
PageLayout처럼 내부 콘텐츠가 없으면 의미가 약해지는 컴포넌트는 children: ReactNode처럼 필수 속성으로 잡는 쪽이 읽기 쉽습니다. 이 타입은 “이 레이아웃은 반드시 감싸는 콘텐츠를 받아야 한다”는 의도를 드러냅니다.
import type { ReactNode } from "react";
type CardProps = {
title: string;
children?: ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<article className="card">
<h3>{title}</h3>
{children ? <div className="card-content">{children}</div> : null}
</article>
);
}
반대로 Card처럼 제목만으로도 화면에 존재할 수 있고, 추가 설명 영역은 있어도 되고 없어도 된다면 children?: ReactNode가 자연스럽습니다. 기준은 단순합니다. children이 없어도 컴포넌트가 정상적인 UI로 보이면 선택으로 두고, children이 빠졌을 때 컴포넌트 의미가 깨지면 필수로 둡니다.
다만 children?: ReactNode로 열어두면 사용부에서 태그 사이 값을 넣지 않아도 타입 오류가 나지 않습니다. 이 점이 장점이 될 때도 있지만, 모달 본문이나 페이지 레이아웃처럼 내용이 빠지면 안 되는 컴포넌트에서는 오히려 누락을 늦게 발견하게 만들 수 있습니다.
PropsWithChildren은 언제 편하고 언제 애매할까
PropsWithChildren은 기존 props 타입에 children을 더해주는 편의 타입입니다. 반복되는 children?: ReactNode를 직접 쓰지 않아도 되기 때문에 간단한 래퍼 컴포넌트에서는 코드가 짧아집니다.
import type { PropsWithChildren } from "react";
type NoticeBoxProps = PropsWithChildren<{
tone: "info" | "warning";
}>;
function NoticeBox({ tone, children }: NoticeBoxProps) {
return <div className={`notice notice-${tone}`}>{children}</div>;
}
이 방식은 NoticeBox처럼 “내용을 감싸는 작은 UI 박스”를 만들 때 간결합니다. props가 많지 않고, children이 있어도 되고 없어도 되는 구조라면 타입 선언이 깔끔해집니다.
하지만 편의 타입이라는 이유만으로 모든 컴포넌트에 붙이면 의도가 흐려질 수 있습니다. PropsWithChildren의 children은 기본적으로 선택 속성처럼 다뤄집니다. 그래서 children이 반드시 있어야 하는 ModalBody, PageLayout, FormSection 같은 컴포넌트라면 직접 children: ReactNode를 적는 방식이 더 명확합니다.
import type { ReactNode } from "react";
type ModalBodyProps = {
children: ReactNode;
};
function ModalBody({ children }: ModalBodyProps) {
return <div className="modal-body">{children}</div>;
}
실무에서는 짧은 코드보다 타입을 읽었을 때 컴포넌트의 사용 조건이 바로 보이는지가 중요할 때가 많습니다. children이 선택인지 필수인지가 UI 구조에 영향을 준다면 PropsWithChildren보다 직접 명시하는 편이 유지보수에 유리합니다.
React.FC를 사용할 때도 비슷한 판단이 필요합니다. 오래된 예제에서는 React.FC가 children을 자연스럽게 포함하는 것처럼 설명되는 경우가 있지만, 현재 프로젝트에서는 children을 받을 컴포넌트라면 props 타입에 직접 드러내는 방식이 읽기 쉽습니다. 팀 안에서 React.FC 사용 여부가 정해져 있더라도, children의 필수 여부까지 자동으로 해결된다고 기대하지 않는 것이 좋습니다.
ReactNode, ReactElement, JSX.Element를 구분하는 기준

children 타입을 검색하다 보면 ReactNode, ReactElement, JSX.Element가 함께 나옵니다. 이름이 비슷해서 같은 역할처럼 보이지만, 실제 사용 위치는 다릅니다.
| 타입 | 주로 쓰는 위치 | 판단 기준 |
|---|---|---|
ReactNode |
children, 렌더링 가능한 콘텐츠 | 문자열, 숫자, JSX, 조건부 값까지 넓게 받을 때 |
ReactElement |
하나의 React 요소를 기대하는 props | 텍스트가 아니라 JSX 요소 하나를 기대할 때 |
JSX.Element |
JSX 표현식의 결과, 컴포넌트 반환 타입 설명 | children 타입보다 반환 결과 쪽에서 주로 마주칠 때 |
일반적인 children에는 ReactNode가 가장 자연스럽습니다. 버튼 안에 텍스트만 들어갈 수도 있고, 아이콘과 텍스트가 같이 들어갈 수도 있으며, 조건에 따라 아무것도 렌더링하지 않을 수도 있기 때문입니다.
import type { ReactNode } from "react";
type ButtonProps = {
children: ReactNode;
onClick: () => void;
};
function Button({ children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>;
}
반면 특정 위치에 JSX 요소 하나만 받고 싶다면 ReactElement를 검토할 수 있습니다. 예를 들어 패널 상단에 들어갈 아이콘을 문자열이 아니라 실제 아이콘 컴포넌트로 제한하고 싶을 때입니다.
import type { ReactElement, ReactNode } from "react";
type EmptyStateProps = {
icon: ReactElement;
title: string;
children?: ReactNode;
};
function EmptyState({ icon, title, children }: EmptyStateProps) {
return (
<section className="empty-state">
<div className="empty-state-icon">{icon}</div>
<h2>{title}</h2>
{children ? <p>{children}</p> : null}
</section>
);
}
JSX.Element는 children 타입을 고를 때의 첫 번째 선택지가 아닙니다. 프로젝트의 React 타입 설정에 따라 React.JSX.Element처럼 보일 수도 있는데, 실무에서는 컴포넌트가 반환하는 JSX 결과를 설명할 때 더 자주 마주칩니다. children은 더 넓은 값을 받을 일이 많기 때문에 ReactNode와 구분해서 보는 것이 좋습니다.
특히 children: JSX.Element처럼 좁게 잡으면 문자열이나 숫자, 여러 요소를 자연스럽게 받기 어렵습니다. 버튼이나 카드처럼 내부 구성이 바뀔 수 있는 컴포넌트라면 처음부터 지나치게 좁은 타입을 쓰기보다, 실제로 제한이 필요한지 먼저 확인하는 것이 좋습니다.
children을 직접 다루려 할 때 주의할 점
children 타입을 지정하는 것과 children 내부를 직접 다루는 것은 다른 문제입니다. 타입을 ReactNode로 잡아도, 그 값을 항상 배열처럼 사용할 수 있다는 뜻은 아닙니다.
import { Children, type ReactNode } from "react";
type ListGroupProps = {
children: ReactNode;
};
function ListGroup({ children }: ListGroupProps) {
const items = Children.toArray(children);
return (
<ul className="list-group">
{items.map((item, index) => (
<li key={index} className="list-group-item">
{item}
</li>
))}
</ul>
);
}
여기서 중요한 부분은 children.map처럼 직접 접근하지 않는다는 점입니다. children은 하나일 수도 있고, 여러 개일 수도 있고, 없을 수도 있습니다. 화면에서는 여러 요소처럼 보여도 내부 값의 형태를 배열이라고 단정하면 타입 오류와 런타임 오류가 섞일 수 있습니다.
자식 요소를 순회하거나 개수를 세거나 감싸야 한다면 Children.toArray, Children.map 같은 React의 children 처리 API를 사용합니다. 다만 이런 처리가 많아진다면 children 구조를 억지로 분석하는 컴포넌트가 된 것은 아닌지도 같이 확인해야 합니다.
예를 들어 탭 컴포넌트에서 <Tabs><Tab /><Tab /></Tabs> 구조만 허용하고 싶을 수 있습니다. 이때 타입만으로 “반드시 Tab 컴포넌트만 children으로 받는다”를 완벽하게 강제하려고 하면 코드가 복잡해집니다. 이런 경우에는 children을 분석하기보다 items 배열 props로 구조를 넘기는 방식이 더 단순할 수 있습니다.
type TabItem = {
id: string;
label: string;
content: ReactNode;
};
type TabsProps = {
items: TabItem[];
activeId: string;
};
function Tabs({ items, activeId }: TabsProps) {
const activeItem = items.find((item) => item.id === activeId);
return (
<section className="tabs">
<div className="tab-list">
{items.map((item) => (
<button key={item.id} type="button">
{item.label}
</button>
))}
</div>
<div className="tab-panel">{activeItem?.content}</div>
</section>
);
}
children은 컴포넌트 조합을 자연스럽게 만들 때 좋습니다. 반대로 순서, 개수, 식별자, 활성 상태처럼 데이터 구조가 중요해지는 순간에는 children보다 명시적인 props가 더 다루기 쉽습니다. children 타입 지정은 넓게 받을지 좁게 받을지의 문제가 아니라, 컴포넌트 사용 방식을 어떻게 설계할지와 연결됩니다.
실무에서 정리하는 children 타입 선택 기준
children 타입은 다음 기준으로 정리하면 대부분의 상황에서 크게 흔들리지 않습니다.
- 일반적인 UI 조합을 허용하는 컴포넌트라면
children: ReactNode를 사용합니다. - children이 없어도 자연스러운 컴포넌트라면
children?: ReactNode로 둡니다. - children이 반드시 있어야 컴포넌트 의미가 완성된다면 optional로 만들지 않습니다.
- 기존 props에 children을 가볍게 더하고 싶다면
PropsWithChildren을 사용할 수 있습니다. - 하나의 JSX 요소만 기대하는 별도 prop에는
ReactElement를 검토합니다. - children을 배열처럼 다뤄야 한다면 직접 배열로 단정하지 말고 React의 Children API를 사용합니다.
- children 구조를 강하게 제한해야 한다면 배열 props나 설정 객체로 바꾸는 설계도 함께 검토합니다.
초보 단계에서는 ReactNode와 PropsWithChildren 중 무엇을 써야 하는지가 가장 크게 보입니다. 하지만 실제 판단 기준은 타입 이름이 아니라 컴포넌트의 의도입니다. 레이아웃처럼 반드시 콘텐츠를 감싸야 한다면 직접 children: ReactNode를 적고, 단순 래퍼처럼 children이 있어도 되고 없어도 되는 구조라면 PropsWithChildren이나 children?: ReactNode를 선택할 수 있습니다.
정리하면, children 타입은 넓게 받는 것이 편할 때와 좁게 잡아야 하는 때를 구분하는 문제입니다. 화면 안에 무엇이 들어올지 자유롭게 열어두고 싶다면 ReactNode가 적합하고, 특정 요소 하나만 받아야 한다면 ReactElement를 검토합니다. children이 필수라면 optional로 만들지 않는 것만으로도 타입 의도가 훨씬 선명해집니다.
다음 흐름에서는 children?: ReactNode처럼 선택 속성으로 열어두는 판단을 일반 props로 넓혀 볼 수 있습니다. optional props와 default value를 같이 이해하면, 컴포넌트가 받는 값의 필수 여부를 더 안정적으로 설계할 수 있습니다.