Children과 composition
HTML은 언제나 중첩을 통해 작동해왔습니다. <select>는 <option>을 감싸고, <ul>은 <li>를 감싸며, <button>은 레이블을 감쌉니다. 안쪽의 요소는 바깥쪽 요소의 자식이며, 둘이 함께 작동해 화면에 보이는 것을 만듭니다. React 컴포넌트도 같은 방식으로 작동할 수 있습니다. 이름이 있는 props로 모든 것을 전달하는 대신, 네이티브 요소처럼 컴포넌트가 콘텐츠를 감쌀 수 있으며, 바로 이 아이디어가 설정으로 사용하는 컴포넌트와 **composition**으로 만드는 컴포넌트를 구분 짓습니다.
이 챕터는 컴포넌트 라이브러리를 위한 Button을 통해 강의의 논리 구조를 따릅니다. 먼저 children을 다루고, 그 다음 prop spreading을 다룬 후, 마지막으로 컴포넌트를 실무에서 재사용 가능하게 만드는 마무리 작업을 살펴봅니다.
Children을 인터페이스로 활용하기
Props 챕터에서 children을 배웠습니다. 컴포넌트의 여는 태그와 닫는 태그 사이의 콘텐츠는 이름이 children인 prop으로 전달됩니다. 재사용성 측면에서 이것이 제공하는 이점은 생각보다 훨씬 큽니다.
function Button({ children }) {
return <button>{children}</button>
}
// 사용: <Button>지금 구매</Button>컴포넌트는 children이 어디에 렌더되는지 결정하고, 사용자는 children이 무엇인지 결정합니다. 이를 text prop과 비교하면, text prop은 사용자가 문자열 이상의 것을 원할 때까지만 작동합니다. Children을 사용하면 사용자가 문자열 이상의 것을 전달하는 것을 막을 것이 없습니다.
// 사용:
// <Button>
// <CartIcon />
// 지금 구매
// </Button>레이블 왼쪽에 아이콘이 들어가는데, icon prop도 없고, iconPosition="left" 옵션도 없으며, 컴포넌트 작성자의 예상도 전혀 없었습니다. 아이콘을 오른쪽에 놓고 싶으신가요? 텍스트의 다른 쪽으로 옮기면 됩니다. 작성자가 발명했을 모든 설정 prop이 사라지고, 사용자가 원하는 마크업을 직접 작성하게 됩니다.
나머지 props 전달하기
네이티브 <button>을 렌더하는 Button에는 두 번째 문제가 있습니다. 바로 이벤트입니다. 사용자가 <Button onClick={...}>라고 작성하면, onClick은 여러분의 컴포넌트에 도달하는 커스텀 prop이며, 컴포넌트가 이를 아래의 실제 <button>으로 전달하지 않으면 아무것도 일어나지 않습니다. onClick을 수동으로 전달할 수도 있고, onDoubleClick을 전달할 수도, style과 className을 전달할 수도 있지만, 네이티브 button이 허용하는 모든 것을 다룰 수는 없습니다. spread 문법을 사용하면 한 번에 모두 전달할 수 있습니다.
function Button({ children, ...rest }) {
return <button {...rest}>{children}</button>
}컴포넌트가 실제로 다루는 props는 구조 분해로 빼내고, 나머지를 rest 문법으로 수집한 후, rest를 기본 요소에 spread합니다. 사용자가 전달하는 모든 유효한 button 속성(이벤트 핸들러 포함)이 올바른 위치에 도달합니다. 컴포넌트가 자신을 위해 만든 props(예를 들어 variant)는 구조 분해로 빼내므로 DOM 요소에 절대 도달하지 않습니다.
자신의 API를 위한 공간 만들기
재사용 가능한 컴포넌트는 보통 네이티브 요소 위에 옵션을 추가합니다. 이 강의의 button은 size("sm" 또는 "lg")와 variant("success", "warning", "danger")를 받으며, 각각 CSS class로 매핑됩니다.
function Button({ children, size, variant, className, ...rest }) {
const sizeClass = size ? `button-${size}` : ''
const variantClass = variant ? `button-${variant}` : ''
const allClasses = [sizeClass, variantClass, className].filter(Boolean).join(' ')
return (
<button className={allClasses} {...rest}>
{children}
</button>
)
}이 병합 방식은 미묘한 버그를 피합니다. 컴포넌트가 className을 설정하고 사용자도 하나를 전달하면, 요소에 마지막에 도달하는 쪽이 이기게 됩니다. rest를 자신의 className 뒤에 spread하면 사용자의 class가 여러분의 것을 조용히 덮어쓰고, 그 전에 spread하면 여러분의 것이 사용자의 것을 덮어씁니다. className을 구조 분해로 빼내고 문자열을 직접 병합하는 것이 해결책입니다. clsx나 classnames 같은 라이브러리는 조건이 많아질 때 정확히 이 병합을 하기 위해 존재합니다.
children을 렌더하는 Button은 text prop, icon prop, icon-position prop이 필요하지 않습니다. 이를 사용하는 사람이 아이콘과 텍스트를 원하는 순서대로 태그 내에 작성하면, Button이 그 콘텐츠를 속한 곳에 배치합니다. 중첩, HTML이 언제나 해온 것, 바로 그것이 React의 재사용성에 대한 최고의 트릭입니다.
다음: Compound components, 하나의 컴포넌트가 함께 작동하는 여러 컴포넌트가 되는 경우입니다.

