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

복합 컴포넌트

메뉴 컴포넌트를 하나의 블랙박스로 작성할 수 있습니다. buttonTextitems 배열을 넘기면 나머지는 모두 자체적으로 렌더링합니다. 이 방식은 충분할 때까지만 작동합니다. 호출자는 아이템 렌더링 방식을 바꿀 수 없고, 조각들을 재정렬할 수 없으며, 버튼을 다른 곳에 재사용할 수 없습니다. 그리고 Menu가 사용하지 않는 모든 prop도 통과시켜야 합니다. 복합 컴포넌트는 반대의 접근을 취합니다. 함께 작동하도록 설계된 여러 작은 컴포넌트를 호출자가 마크업처럼 조합하는 것입니다.

HTML은 처음부터 이런 방식으로 작동했습니다. <select><option>이 없으면 무의미하고, <ul>은 내부의 <li>들을 스타일링하며, <form>은 감싸는 입력 요소들을 추적하고, 테이블 계열은 함께만 의미를 갖는 요소들의 생태계입니다. 복합 컴포넌트는 이런 부모와 자식 사이의 계약을 여러분의 React 컴포넌트로 가져옵니다.

prop 드릴링 문제

단일형 메뉴는 prop 드릴링을 작은 규모로 보여줍니다. MenubuttonTextitems를 받지만 오직 MenuButtonMenuDropdown으로 전달하기 위해서만 필요합니다. Menu 자체는 이들을 신경 쓰지 않습니다. 이런 릴레이가 바로 prop 드릴링이고, 상태 끌어올리기에서는 한두 단계의 드릴링이 정상인 시점과 다른 도구가 필요할 만큼 확대되는 시점을 다룹니다. 여러 계층에 걸치면 컴포넌트가 택배업체가 되고, 중간 계층을 리팩터링하면 전체 연쇄를 다시 엮어야 합니다.

때로는 아무것도 하지 않는 것이 정답입니다. 한두 단계의 드릴링은 성급한 추상화보다 저렴하고, 서두른 추상화는 제거하려던 반복 자체보다 비용이 큽니다. 아래 패턴들은 드릴링이 실제 문제일 때 가치를 발휘합니다.

구조 평탄화

복합 버전은 Menu를 자식들을 렌더링하는 래퍼로 바꾸고, 내부 조각들을 호출자 수준으로 끌어올립니다.

jsx
const sports = ['테니스', '피클볼', '라켓볼', '스쿼시']

<Menu>
  <MenuButton>스포츠</MenuButton>
  <MenuDropdown>
    {sports.map(sport => (
      <MenuItem key={sport}>{sport}</MenuItem>
    ))}
  </MenuDropdown>
</Menu>

각 조각은 작게 유지되고 하나의 일만 합니다. 마크업 뒤의 본체들은 거의 한두 줄입니다.

jsx
function Menu({ children }) {
  return <div className="menu">{children}</div>
}

function MenuButton({ children }) {
  return <Button>{children}</Button>
}

function MenuDropdown({ children }) {
  return <div className="menu-dropdown">{children}</div>
}

function MenuItem({ children }) {
  return <div className="menu-item">{children}</div>
}

MenuButton조합 챕터의 Button을 재사용합니다. Menu 내부에 숨겨져 있던 구조가 이제 호출 위치에서 보입니다. 데이터는 곧바로 그것을 사용하는 컴포넌트로 흘러갑니다. 배열은 필요한 정확한 자리에서 매핑되고 Menu를 통과할 필요가 없습니다. 각 아이템을 텍스트 대신 링크로 표시하고 싶나요? MenuItem 안에 <a>를 넣으면 됩니다. 컴포넌트 작성자는 그런 요청을 미리 예상할 필요가 없습니다.

투명성과 장황함이 서로 바뀝니다. 호출자는 더 많은 JSX를 작성하지만 모든 계층을 세밀하게 제어합니다. 블랙박스는 양쪽 의미에서 사라집니다.

조각들은 별도의 컴포넌트이므로 호출자는 각각을 import해야 합니다. 라이브러리는 보통 부모에 subcomponent를 첨부한 후 export하는 방식으로 이를 간결하게 만듭니다. 이는 평범한 JavaScript입니다. 함수는 객체이므로 속성을 가질 수 있습니다.

jsx
Menu.Button = MenuButton
Menu.Dropdown = MenuDropdown
Menu.Item = MenuItem
export default Menu

하나의 import로 전체 패밀리가 옵니다. 호출 위치에서 <Menu.Button>은 이 조각들이 함께 속한다는 것을 광고합니다. 점 표기법은 이름 지정일 뿐입니다. 조각들을 함께 작동하게 하는 것은 다른 곳에서 와야 하는데, 이것이 다음 섹션이 다루는 격차입니다.

빠진 조각: 암시적 상태

평탄화된 메뉴는 구멍이 있습니다. 버튼이 드롭다운을 더 이상 열지 않습니다. 열림/닫힘 불린 값과 그 토글 함수는 Menu에 살고 있고, Menu는 래핑 div와 {children}만 렌더링합니다. MenuButton에 전달되는 prop이 없으므로 토글을 걸어둘 곳이 없습니다. 조각들이 호출자가 매 태그마다 와이어링하지 않고도 상태를 공유해야 합니다. 커뮤니티는 이를 암시적 상태라고 부릅니다. 복합 조각들이 뒤에서 조정하면서 호출자는 그것들을 조합합니다.

React는 이를 흉내 낼 수 있는 API를 제공합니다. Children.map은 컴포넌트의 직접 자식들을 순회합니다. (props.children은 보장된 형태가 없습니다. 여러 자식은 배열이고 단일 자식은 요소이며 없으면 undefined이므로 .map()을 호출하는 것은 결국 작동하지 않을 때까지만 작동합니다). cloneElement는 요소를 복사하면서 추가 prop을 주입합니다.

jsx
import { Children, cloneElement, useState } from 'react'

function Menu({ children }) {
  const [open, setOpen] = useState(false)
  const toggle = () => setOpen(prevOpen => !prevOpen)

  return (
    <div className="menu">
      {Children.map(children, child =>
        cloneElement(child, { open, toggle })
      )}
    </div>
  )
}

이제 Menu의 모든 직접 자식이 암묵적으로 opentoggle을 받고 버튼이 다시 작동합니다. 하지만 누군가 건드리자마자 깨집니다. 이것이 진짜 교훈입니다.

Children.map의 부족한 점

두 가지 부족한 점이 있고, 둘 다 구조적입니다. 첫째, 취약합니다. 매핑은 직접 자식들만 건드립니다. 호출자가 MenuDropdown을 스타일링 <div>로 감싸면, div가 toggleopen을 받고, React는 콘솔에 함수가 DOM 속성의 유효한 값이 아니라고 불평하며, 메뉴가 작동 중단됩니다. 조각들의 래핑을 금지하는 복합 컴포넌트는 그것을 정당화했던 유연성을 포기한 것입니다.

둘째, 깊이 제한이 있습니다. MenuItem은 손자이므로 아무것도 받지 않습니다. 여기에 상태를 전달하려면 MenuDropdown 내부에서도 Children.mapcloneElement 춤을 반복해야 하고, 계층당 한 번씩의 릴레이인데, 이는 의상을 바꾼 prop 드릴링입니다.

패턴이 실제로 필요한 것은 Menu에서 아무리 깊고 아무리 감싸져 있든 모든 descendant로 상태를 '순간이동'시키는 방법입니다. 이는 정확히 context가 하는 일이고, context 챕터는 이 같은 메뉴를 집어 완성합니다. Menu{ open, toggle }을 감싼 모든 것에 제공하고, MenuButton은 그 사이에 무엇이 있든 useContext로 토글을 읽습니다.

버전 참고

React 문서는 이제 ChildrencloneElement를 레거시 API로 분류하고 새 코드에서는 context를 포함한 대안을 권장합니다. 여전히 작동하고 기존 코드에 많이 있으므로 코드를 보고 인식할 가치가 있습니다. 과정 레슨과 대부분의 기존 코드는 기본 import로 React.ChildrenReact.cloneElement로 작성합니다.

컴포넌트가 복합이어야 하는지 결정하는 것은 누가 중간의 제어권을 필요로 하는가에 달렸습니다. 내부 레이아웃이 고정되고 바뀔 가능성이 낮은 컴포넌트는 몇 개의 prop이 있는 단일형이 낫습니다. 복합 패턴은 호출자가 계속 렌더링 옵션을 하나 더 요청하고 prop 목록이 설정 언어로 변할 때 대가를 치릅니다. 공개 위젯, 메뉴, 탭, 아코디언이 그런 영역에 있으므로 모든 주류 headless UI 라이브러리가 복합 형태로 제공합니다.

패밀리를 제공하는 것은 가격 매기기 가치가 있는 두 가지 비용을 가집니다. 조각들을 부모에 첨부하면 번들러는 그것들을 함께 묶으므로, Menu를 import하면 페이지가 렌더링하지 않든 모든 subcomponent를 끌어옵니다. 드물게 사용하는 조각도 청크에 탑승합니다.

두 번째 비용은 복합 API가 기대하는 구조를 설명할 수 있지만 절대 강제하지 않습니다. 호출자가 위의 MenuButton 없이 MenuDropdown을 렌더링하는 것을 중지할 것은 없습니다. 자식의 type을 컴포넌트 항등성과 비교해 경찰하는 것은 래퍼가 들어가는 순간이나 하나의 빌드에 모듈 두 사본이 들어가는 순간 깨집니다. 지속되는 버전은 의도한 형태를 문서화하고 형태가 깨졌을 때 각 조각이 자체적으로 합리적으로 작동하게 합니다.

Juno함께 작동하는 작은 조각들 많은 prop을 받고 모든 것을 렌더링하는 하나의 큰 Menu 컴포넌트 대신, 복합 컴포넌트는 여러 작은 태그, Menu, MenuButton, MenuDropdown, MenuItem을 줍니다. 이들을 HTML의 select와 그 option들을 배열하는 방식대로 직접 배열합니다. 전체 구조가 작성하는 곳에 보이고, 각 조각이 콘텐츠를 직접 받습니다.

문제는 조각들이 여전히 "메뉴가 열렸는가"와 같은 것을 공유해야 한다는 점이고, context 챕터가 그들이 깔끔하게 하는 방법을 보여줍니다.

Juno함께 작동하는 작은 조각들 단일 컴포넌트의 prop이 설정 언어로 변할 때 복합 컴포넌트에 도달합니다. 조각들을 노출하고, 호출자가 조합하게 하며, 호출 위치에서 데이터를 매핑하고 래퍼를 통과시키지 않습니다.

Children.mapcloneElement는 공유 상태를 직접 자식들에 주입할 수 있지만, 래핑 div 아래서 깨지고 손자에게 절대 도달하지 않으므로 계단돌로 취급하고 실제 조정에는 context를 사용합니다.

Juno함께 작동하는 작은 조각들 복합 컴포넌트는 불투명한 단일 컴포넌트 API를 투명한 조합 가능한 API로 바꿉니다. 호출자가 중간 계층을 제어해야 할 때 바꾸고, 레이아웃이 고정되었을 때는 건너뜁니다.

스코프된 context 제공자를 통해 조각들을 조정합니다. Children.mapcloneElement는 레거시 API이고, 래핑 아래서 취약하며, 설계상 깊이 제한이 있습니다.

조각들을 패밀리로 제공하는 것은 가격이 있습니다. 부모에 첨부하면 번들러가 페이지가 절대 렌더링하지 않는 것을 드롭할 수 없고, API는 기대하는 구조를 설명할 수 있지만 절대 강제하지 않습니다.

다음: Context, 이 조각들이 아무리 깊숙이 있든 상태를 공유하게 하는 도구입니다.