Skip to content
This page has been auto-translated and may contain errors.View in English

Children과 composition

HTML은 언제나 중첩을 통해 작동해왔습니다. <select><option>을 감싸고, <ul><li>를 감싸며, <button>은 레이블을 감쌉니다. 안쪽의 요소는 바깥쪽 요소의 자식이며, 둘이 함께 작동해 화면에 보이는 것을 만듭니다. React 컴포넌트도 같은 방식으로 작동할 수 있습니다. 이름이 있는 props로 모든 것을 전달하는 대신, 네이티브 요소처럼 컴포넌트가 콘텐츠를 감쌀 수 있으며, 바로 이 아이디어가 설정으로 사용하는 컴포넌트와 **composition**으로 만드는 컴포넌트를 구분 짓습니다.

이 챕터는 컴포넌트 라이브러리를 위한 Button을 통해 강의의 논리 구조를 따릅니다. 먼저 children을 다루고, 그 다음 prop spreading을 다룬 후, 마지막으로 컴포넌트를 실무에서 재사용 가능하게 만드는 마무리 작업을 살펴봅니다.

Children을 인터페이스로 활용하기

Props 챕터에서 children을 배웠습니다. 컴포넌트의 여는 태그와 닫는 태그 사이의 콘텐츠는 이름이 children인 prop으로 전달됩니다. 재사용성 측면에서 이것이 제공하는 이점은 생각보다 훨씬 큽니다.

jsx
function Button({ children }) {
  return <button>{children}</button>
}

// 사용: <Button>지금 구매</Button>

컴포넌트는 children이 어디에 렌더되는지 결정하고, 사용자는 children이 무엇인지 결정합니다. 이를 text prop과 비교하면, text prop은 사용자가 문자열 이상의 것을 원할 때까지만 작동합니다. Children을 사용하면 사용자가 문자열 이상의 것을 전달하는 것을 막을 것이 없습니다.

jsx
// 사용:
// <Button>
//   <CartIcon />
//   지금 구매
// </Button>

레이블 왼쪽에 아이콘이 들어가는데, icon prop도 없고, iconPosition="left" 옵션도 없으며, 컴포넌트 작성자의 예상도 전혀 없었습니다. 아이콘을 오른쪽에 놓고 싶으신가요? 텍스트의 다른 쪽으로 옮기면 됩니다. 작성자가 발명했을 모든 설정 prop이 사라지고, 사용자가 원하는 마크업을 직접 작성하게 됩니다.

나머지 props 전달하기

네이티브 <button>을 렌더하는 Button에는 두 번째 문제가 있습니다. 바로 이벤트입니다. 사용자가 <Button onClick={...}>라고 작성하면, onClick은 여러분의 컴포넌트에 도달하는 커스텀 prop이며, 컴포넌트가 이를 아래의 실제 <button>으로 전달하지 않으면 아무것도 일어나지 않습니다. onClick을 수동으로 전달할 수도 있고, onDoubleClick을 전달할 수도, styleclassName을 전달할 수도 있지만, 네이티브 button이 허용하는 모든 것을 다룰 수는 없습니다. spread 문법을 사용하면 한 번에 모두 전달할 수 있습니다.

jsx
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로 매핑됩니다.

jsx
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을 구조 분해로 빼내고 문자열을 직접 병합하는 것이 해결책입니다. clsxclassnames 같은 라이브러리는 조건이 많아질 때 정확히 이 병합을 하기 위해 존재합니다.

rest props를 spread하는 것은 계약이며, 날카로운 모서리를 가지고 있습니다. 섹션에서 이미 다룬 두 가지, spread 순서와 leaked props는 기본적인 것들이지만, leak은 대부분의 사람들이 예상하는 것보다 조용합니다.

새어나간 prop이 실제로 하는 일

variant 같은 소문자 string prop은 경고 없이 DOM 노드에 유효하지 않은 HTML 속성으로 도착하는데, 이것이 문제를 야기하는 경우입니다. React는 iconPosition 같은 camelCase prop에 대해 경고하고 여전히 렌더합니다. boolean 값이나 onclick 같은 모든 소문자 핸들러 이름에 대해서는 경고하고 아무것도 렌더하지 않습니다. onClik 같은 camelCase 오타는 속성도 경고도 받지 않습니다.

ref는 반대 방향입니다. React 19에서는 일반 prop이므로 ...rest에 탑승하고 함수 컴포넌트의 기본 요소에 도달하며 forwardRef를 필요로 하지 않습니다. 비용이 큰 모서리는 API 표면입니다. 호출자가 임의의 네이티브 props를 전달할 수 있게 되면, 그들이 사용하는 모든 것이 다음 버전에서 여러분이 보장해야 할 것이 되며, 나중에 passthrough를 줄이는 것은 문서화하지 않았더라도 breaking change입니다.

이 강의의 Avatar "mega challenge"는 composition의 다른 측면을 보여줍니다. src가 제공되면 사진을, children이 있으면 initials을, 그 외에는 아이콘을 렌더하는, 세 가지 다른 방식으로 렌더하는 하나의 컴포넌트입니다. 이러한 오버로딩은 문서화된 컴포넌트 라이브러리에 적합하며, 세 개의 비슷한 컴포넌트보다 하나의 유연한 Avatar가 낫습니다. 애플리케이션 코드에서는 이를 일찍 사용하는 것이 보통 컴포넌트가 너무 많은 일을 하고 있다는 신호입니다. 두 개의 작은 컴포넌트가 하나의 영리한 컴포넌트를 이깁니다. prop을 추가할지 외부에서 composition할지에 대한 이 줄다리기는 이 섹션의 나머지를 통해, 그리고 다음 패턴들을 통해 실행되며, compound componentsrender props는 이에 대한 구조화된 답변입니다.

Juno호출자가 내부에 콘텐츠를 넣도록 하기children을 렌더하는 Button은 text prop, icon prop, icon-position prop이 필요하지 않습니다. 이를 사용하는 사람이 아이콘과 텍스트를 원하는 순서대로 태그 내에 작성하면, Button이 그 콘텐츠를 속한 곳에 배치합니다.

중첩, HTML이 언제나 해온 것, 바로 그것이 React의 재사용성에 대한 최고의 트릭입니다.

Juno호출자가 내부에 콘텐츠를 넣도록 하기 두 가지 습관으로 재사용 가능한 컴포넌트를 만드세요. 콘텐츠가 들어갈 곳에 children을 렌더하고, 여러분이 다루는 props는 구조 분해로 빼내되 ...rest를 기본 요소에 spread하여 네이티브 속성과 이벤트 핸들러가 통과하도록 합니다.

className을 직접 병합하여 spread 순서가 어느 것을 유지할지 결정하지 않도록 하고, variant 같은 발명된 props를 rest 객체 밖에 두어 DOM에 절대 도달하지 않도록 합니다.

Juno호출자가 내부에 콘텐츠를 넣도록 하기 rest-prop forwarding을 컴포넌트의 API 표면의 일부로 취급합니다. 충돌은 spread 순서로 결정되고, 구조 분해되지 않은 커스텀 props는 DOM에 leak되며, 호출자가 passthrough에 의존할 것입니다.

configuration prop이 그 자리를 차지할 때까지 children을 통한 composition을 선호하고, 하나의 유연한 조각이 정말로 여러 개를 대체하는 문서화된 라이브러리 코드를 위해 오버로딩된 컴포넌트를 남겨두세요.

다음: Compound components, 하나의 컴포넌트가 함께 작동하는 여러 컴포넌트가 되는 경우입니다.