React에서 children은 컴포넌트의 여는 태그와 닫는 태그 사이에 작성한 내용을 담는 prop입니다. 출력의 어딘가에서 {children}을 렌더링하는 컴포넌트는 래퍼가 됩니다. 컴포넌트는 틀을 제공하고, 호출하는 쪽은 내용을 제공합니다.
두 카드는 같은 테두리, 같은 패딩, 같은 제목 스타일을 공유하지만 각각 완전히 다른 내용을 담고 있습니다. 안에 링크를 넣은 세 번째 <Card title="Help">를 추가하면 같은 틀이 그대로 적용됩니다.
children이 컴포넌트에 도착하는 방식
JSX는 중첩된 내용을 prop으로 바꿉니다. 다음 두 줄은 같은 요소를 만듭니다.
<Card title="Profile"><p>Ada</p></Card>
<Card title="Profile" children={<p>Ada</p>} />
즉 children은 넘기는 방식만 특별한 일반 prop입니다. 다른 prop처럼 구조 분해하거나(function Card({ children })) props.children으로 읽습니다. props 페이지에서 props 전반을 다룹니다.
children에 무엇이 들어오는지는 태그 사이에 무엇을 썼는지에 따라 달라집니다. 자식이 하나면 그 자식이 그대로 들어옵니다. 자식이 여러 개면 배열로 들어옵니다. 텍스트는 문자열로 들어오고, 아무것도 없으면 undefined가 들어옵니다.
콘솔에는 object, array of 2, string, undefined가 표시됩니다. 대부분은 신경 쓸 필요가 없습니다. {children}을 렌더링하면 이 모든 경우를 처리하고, null, undefined, true, false는 아무것도 렌더링하지 않습니다. 형태가 중요해지는 것은 children을 검사하거나 바꾸려고 할 때뿐이며, 마지막 섹션에서 바로 그것을 경고합니다.
레이아웃 컴포넌트
children이 가장 흔하게 쓰이는 곳은 레이아웃 컴포넌트입니다. 페이지 셸, 사이드바 레이아웃, 가운데 정렬된 칼럼 같은 것들입니다. 컴포넌트는 구조와 간격을 맡고, 그것을 사용하는 각 페이지는 자기 내용을 넘깁니다.
function PageLayout({ children }) {
return (
<div className="page">
<Header />
<main className="page-content">{children}</main>
<Footer />
</div>
);
}
function AboutPage() {
return (
<PageLayout>
<h1>About us</h1>
<p>We teach people to code.</p>
</PageLayout>
);
}
PageLayout은 소개 페이지에 무엇이 들어 있는지 알지도 못하고 신경 쓰지도 않습니다. 바로 그 분리가 핵심입니다. 헤더를 한 번 바꾸면 레이아웃으로 감싼 모든 페이지에 반영됩니다.
이름 있는 props로 여러 슬롯 만들기
children은 슬롯 하나입니다. 컴포넌트가 여러 곳에 내용을 받아야 한다면 추가 조각을 이름 있는 props로 넘기세요. 어떤 prop이든 JSX를 담을 수 있으므로, 모달은 title, footer, 그리고 본문인 children을 받을 수 있습니다.
모달 셸은 세 영역의 테두리, 패딩, 순서를 제어하고, 호출하는 쪽은 각 영역을 채웁니다. 다른 프레임워크에서는 이것을 슬롯이라고 부르지만, React에서는 그냥 props입니다. 두 버튼을 감싼 <>...</>는 프래그먼트로, 요소를 추가하지 않고 버튼들을 묶습니다. 실제 대화상자라면 페이지 위에 놓이도록 포털로 document.body에 렌더링할 것입니다.
<p>를 title prop으로 옮기면 본문 텍스트가 헤더에 나타납니다. 각 prop이 어디로 갈지는 컴포넌트가 정하고, 호출하는 쪽은 그 안에 무엇을 넣을지만 정합니다.
함수로서의 children
때로는 래퍼가 어떤 상태를 가지고 있고, 그것을 어떻게 그릴지는 호출하는 쪽이 정해야 합니다. 이때 호출하는 쪽은 요소 대신 함수를 children으로 넘기고, 래퍼는 값을 넣어 그 함수를 호출합니다. 이 패턴을 render prop이라고 합니다.
같은 로직으로 하나의 Toggle은 버튼을, 다른 하나는 체크박스를 그립니다. 훅이 나오기 전에는 render prop이 상태를 가진 로직을 공유하는 주된 방법이었습니다. 지금은 보통 useToggle() 같은 커스텀 훅이 더 읽기 쉽지만, 목록, 폼, 애니메이션 라이브러리에서는 여전히 render prop을 보게 됩니다.
Children API와 cloneElement
React는 children을 순회하는 도우미를 담은 Children 객체를 export합니다: Children.map, Children.forEach, Children.count, Children.toArray, Children.only. 새 props로 요소를 복사하는 cloneElement와 함께 쓰면 부모가 받은 것을 검사하고 바꿀 수 있습니다. React 팀은 이것들을 레거시 API로 분류합니다. 여전히 동작하지만 새 코드에서는 피해야 합니다.
그 이유는 이 API들이 태그 사이에 직접 작성한 요소만 보기 때문입니다. 그 요소들이 렌더링하는 것은 볼 수 없습니다.
목록에는 항목이 네 개 보이지만 콘솔에는 Children.count = 3이라고 나옵니다. TwoMore는 항목을 몇 개 렌더링하든 자식 하나이기 때문입니다. cloneElement는 style prop도 TwoMore에 넘기는데, TwoMore는 이를 무시하므로 세 번째와 네 번째 항목에는 줄무늬가 없습니다. 누군가 <li> 요소 몇 개를 컴포넌트로 리팩터링하면, 부모를 건드리지 않았는데도 부모가 깨집니다.
해결 방법은 children 안으로 손을 뻗지 말고 대신 데이터를 넘기는 것입니다.
function NumberedList({ items }) {
return (
<ol>
{items.map((item, i) => (
<li key={item.id} style={{ color: i % 2 ? 'gray' : 'black' }}>
{item.label}
</li>
))}
</ol>
);
}
이제 목록이 각 행의 렌더링을 맡고, 호출하는 쪽이 JSX를 어떻게 썼는지에 의존하는 것은 없습니다. 부모가 깊이 중첩된 자식과 값(선택된 탭, 테마)을 공유해야 한다면, props를 복제해서 붙이지 말고 컨텍스트를 사용하세요.
TypeScript에서 children 타입 지정하기
TypeScript에서는 children의 타입을 React.ReactNode로 지정하세요. 요소, 문자열, 숫자, 그것들의 배열, null, undefined, 불리언 등 React가 렌더링할 수 있는 모든 것을 포함합니다.
import type { ReactNode } from 'react';
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<section>
<h3>{title}</h3>
{children}
</section>
);
}
render prop이라면 실제 모습 그대로 함수 타입으로 지정하세요: children: (on: boolean, toggle: () => void) => ReactNode. 컴포넌트가 비어 있어도 말이 된다면 children을 선택적으로(children?: ReactNode) 만드세요.
자주 하는 실수
children 렌더링을 잊는 것. 컴포넌트가 children을 받지만 출력에 {children}을 넣지 않으면 내용이 조용히 사라집니다. 아무 경고도 나오지 않습니다.
함수가 아닌 children을 함수처럼 호출하는 것. children()은 호출하는 쪽이 함수를 넘겼을 때만 동작합니다. 컴포넌트가 render prop을 기대한다면 이름이나 문서에 그렇다고 밝히거나, 의도가 분명하도록 render 같은 이름 있는 prop을 받으세요.
children을 직접 변경하는 것. 요소는 읽기 전용입니다. React는 개발 환경에서 요소를 동결하므로 children.props에 값을 할당하면 오류가 발생합니다. 대신 새 출력을 만드세요.
자주 묻는 질문
React에서 props.children이란 무엇인가요?
컴포넌트의 여는 태그와 닫는 태그 사이에 작성한 내용입니다. <Card><p>Hi</p></Card>에서 Card 함수는 <p>를 props.children으로 받고, 어디에 렌더링할지 직접 정합니다.
children은 특별한 prop인가요?
넘기는 방식만 특별합니다. JSX가 중첩된 내용을 children이라는 prop에 넣지만, 컴포넌트 안에서는 일반 prop입니다. <Card children={<p>Hi</p>} />처럼 명시적으로 넘겨도 똑같이 동작합니다.
컴포넌트에 여러 개의 내용 블록을 넘기려면 어떻게 하나요?
추가 블록은 이름 있는 props로 넘기세요. prop은 children과 마찬가지로 JSX를 담을 수 있으므로 <Modal title={<h2>Delete?</h2>} footer={<button>OK</button>}>Body</Modal>은 컴포넌트에 슬롯 세 개를 줍니다.
Children.map과 cloneElement를 써야 하나요?
새 코드에서는 피하세요. 이 API들은 태그 사이에 직접 작성한 요소만 보고, 그 요소가 렌더링하는 것은 보지 못하므로, 누군가 자식을 다른 컴포넌트로 감싸는 순간 깨집니다. 대신 데이터 배열을 prop으로 넘기거나 컨텍스트를 사용하세요.
TypeScript에서 children의 타입은 무엇인가요?
React.ReactNode를 사용하세요. 요소, 문자열, 숫자, 배열, null, undefined, 불리언 등 React가 렌더링할 수 있는 모든 것을 포함합니다.