children prop으로 컴포넌트 조합하기
React children prop의 작동 원리, 받을 수 있는 값의 종류, 레이아웃 컴포넌트·슬롯 패턴·render prop 등 실전 컴포지션 패턴을 설명합니다.
지난 글에서 props로 데이터를 전달하는 방법을 살펴봤습니다. props 중에서도 가장 특별한 children은 컴포넌트 태그 사이에 넣은 모든 내용을 받는 통로입니다. React가 상속 대신 컴포지션을 권장하는 이유가 바로 이 children prop에 있습니다.
children이란
children은 컴포넌트 여는 태그와 닫는 태그 사이의 내용을 담는 특별히 이름 붙여진 prop입니다.
// 사용 측 — 태그 사이의 내용이 children
<Card title="공지">
<p>내용입니다.</p>
<button>닫기</button>
</Card>
// 정의 측 — props.children으로 수신
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
{children} {/* 여기서 렌더링 */}
</div>
);
}
children은 다른 prop과 똑같이 props.children으로 접근하거나 구조 분해로 꺼낼 수 있습니다. React가 마법처럼 처리하는 것이 아니라, 단지 이름이 children인 일반 prop입니다.
children이 받을 수 있는 값
children에는 다양한 타입이 올 수 있습니다.
// 문자열
<Label>이름</Label> // children = "이름"
// 단일 React 요소
<Box><p>단락</p></Box> // children = <p>단락</p>
// 여러 요소 (배열)
<Box><a /><b /><c /></Box> // children = [<a/>, <b/>, <c/>]
// 표현식
<Box>{user.name}</Box> // children = user.name 값
// 함수 (render prop 패턴)
<Loader>{data => <List items={data} />}</Loader>
TypeScript에서 children의 타입은 React.ReactNode로 선언합니다.
interface CardProps {
title: string;
children: React.ReactNode; // string | number | JSX.Element | null | undefined | ...
}
레이아웃 컴포넌트
페이지 전체 구조를 children으로 조합하는 패턴입니다. 헤더·사이드바·푸터는 고정하고 본문 영역만 children으로 교체합니다.
function AppLayout({ children }) {
return (
<div className="app">
<Header />
<aside className="sidebar">
<Nav />
</aside>
<main className="content">
{children}
</main>
<Footer />
</div>
);
}
// 사용
function HomePage() {
return (
<AppLayout>
<HeroBanner />
<ArticleList />
</AppLayout>
);
}
슬롯 패턴: 여러 삽입 지점
children 외에 추가 prop으로 다른 영역도 외부에서 주입할 수 있습니다.
function Dialog({ header, children, footer }) {
return (
<div className="dialog" role="dialog">
{header && <div className="dialog-header">{header}</div>}
<div className="dialog-body">{children}</div>
{footer && <div className="dialog-footer">{footer}</div>}
</div>
);
}
// 사용
<Dialog
header={<h2>알림</h2>}
footer={<button onClick={onClose}>닫기</button>}
>
<p>작업이 완료되었습니다.</p>
</Dialog>
이 패턴은 Angular의 ng-content, Vue의 <slot>과 동일한 개념입니다.
함수 children (Render Prop)
children이 함수인 경우, 컴포넌트가 데이터를 인자로 넘기며 함수를 호출합니다.
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData);
}, [url]);
if (!data) return <Spinner />;
return children(data); // 데이터를 넘기며 children(함수)를 호출
}
// 사용
<DataFetcher url="/api/posts">
{posts => (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)}
</DataFetcher>
로직을 컴포넌트 안에 캡슐화하면서, 렌더링 결과는 외부에서 제어할 수 있습니다.
children 다루기 시 주의사항
// children이 없을 수 있으므로 방어적으로 처리
function Panel({ children }) {
if (!children) return null;
return <div className="panel">{children}</div>;
}
// 또는 기본값 설정
function Panel({ children = <p>내용 없음</p> }) {
return <div className="panel">{children}</div>;
}
React.Children API(React.Children.count, React.Children.map 등)는 children을 배열처럼 다루는 유틸리티입니다. 하지만 단일 자식인 경우 children이 배열이 아니기 때문에, 직접 .length를 호출하면 오류가 발생합니다. React.Children.count(children)을 쓰면 단일·복수·null을 일관되게 처리할 수 있습니다.
function List({ children }) {
const count = React.Children.count(children); // 0, 1, 또는 n
return (
<div>
<span>{count}개 항목</span>
<ul>{children}</ul>
</div>
);
}
지난 글: props로 데이터 전달하기
다음 글: Props 스프레딩과 전달 패턴
읽어주셔서 감사합니다. 😊