Composition

Composition

asChild prop으로 Radix의 기능을 다른 요소 타입이나 자신의 React 컴포넌트에 조합하는 방법이에요.

출처: 문서

본문

Radix의 모든 DOM 요소를 렌더링하는 primitive 파트는 asChild prop을 받아요. asChild가 true이면 Radix는 기본 DOM 요소를 렌더링하는 대신 파트의 자식을 클론하고, 기능적으로 동작하는 데 필요한 props와 동작을 전달해요.

요소 타입 바꾸기

대부분의 경우 Radix가 가장 적절한 기본값을 제공하도록 설계되었으므로 요소 타입을 수정할 필요가 없어요. 하지만 그렇게 하는 게 유용한 경우도 있어요.

좋은 예가 Tooltip.Trigger예요. 기본적으로 이 파트는 button으로 렌더링되지만, 링크(a 태그)에 툴팁을 추가하고 싶을 수도 있어요. asChild로 이렇게 구현할 수 있어요:

import * as React from "react";

import { Tooltip } from "radix-ui";

export default () => (
  <Tooltip.Root>
    <Tooltip.Trigger asChild>
      <a href="https://www.radix-ui.com/">Radix UI</a>
    </Tooltip.Trigger>

    <Tooltip.Portal>…</Tooltip.Portal>
  </Tooltip.Root>
);

기본 요소 타입을 바꾸기로 했다면, 접근 가능하고 기능적으로 유지되도록 하는 것은 여러분의 책임이에요. 예를 들어 Tooltip.Trigger의 경우 포인터·키보드 이벤트에 반응할 수 있는 포커스 가능한 요소여야 해요. div로 바꾸면 더 이상 접근 가능하지 않게 돼요.

실제로는 위처럼 기본 DOM 요소를 거의 수정하지 않아요. 대신 자신만의 React 컴포넌트를 사용하는 것이 더 흔하며, 특히 대부분의 Trigger 파트에서 그렇습니다. 보통 디자인 시스템의 커스텀 버튼/링크와 기능을 조합하고 싶기 때문이에요.

자신의 React 컴포넌트와 조합하기

위와 정확히 동일하게 동작해요. 파트에 asChild를 전달하고 자신의 컴포넌트를 감싸면 돼요. 다만 몇 가지 주의할 점이 있어요.

컴포넌트가 props를 전개(spread)해야 해요

Radix가 컴포넌트를 클론할 때 기능적이고 접근 가능하게 만들기 위해 자신의 props와 이벤트 핸들러를 전달해요. 컴포넌트가 그 props를 지원하지 않으면 깨질 수 있어요.

이는 모든 props를 기본 DOM 노드에 전개하는 방식으로 동작해요.

// before

const MyButton = () => <button />;

// after

const MyButton = (props) => <button {...props} />;

항상 이렇게 하기를 권장해요. 그래야 구현 세부사항(어떤 props/이벤트를 받아야 하는지 등)에 신경 쓰지 않아도 되고, 일반적으로 "leaf" 컴포넌트에서 좋은 습관이에요.

요소 타입을 직접 바꿀 때와 마찬가지로, 커스텀 컴포넌트가 렌더링하는 요소 타입을 접근 가능하고 기능적으로 유지하는 것은 여러분의 책임이에요.

컴포넌트가 ref를 전달(forward)해야 해요

또한 Radix는 때때로 컴포넌트에 ref를 붙여야 할 수 있어요(예: 크기를 측정하기 위해). 컴포넌트가 ref를 받지 않으면 깨질 수 있어요.

이는 React.forwardRef로 처리해요(react.dev 참고).

// before

const MyButton = (props) => <button {...props} />;

// after

const MyButton = React.forwardRef((props, forwardedRef) => (
  <button {...props} ref={forwardedRef} />
));

모든 파트에 꼭 필요한 건 아니지만, 구현 세부사항에 신경 쓰지 않도록 항상 하는 것을 권장해요. 일반적으로 leaf 컴포넌트에서도 좋은 습관이에요.

여러 primitive 조합하기

asChild는 필요할 만큼 깊이 중첩해서 사용할 수 있어요. 즉 여러 primitive의 동작을 함께 조합하는 좋은 방법이에요. Tooltip.Trigger와 Dialog.Trigger를 자신의 버튼과 함께 조합하는 예시:

import * as React from "react";

import { Dialog, Tooltip } from "radix-ui";

const MyButton = React.forwardRef((props, forwardedRef) => (
  <button {...props} ref={forwardedRef} />
));

export default () => {
  return (
    <Dialog.Root>
      <Tooltip.Root>
        <Tooltip.Trigger asChild>
          <Dialog.Trigger asChild>
            <MyButton>Open dialog</MyButton>
          </Dialog.Trigger>
        </Tooltip.Trigger>

        <Tooltip.Portal>…</Tooltip.Portal>
      </Tooltip.Root>

      <Dialog.Portal>...</Dialog.Portal>
    </Dialog.Root>
  );
};

더 알아보기 (Learn more)

  • asChild로 조합된 커스텀 컴포넌트는 반드시 props를 전개하고 ref를 forward해야 해요.
  • asChild는 깊이 중첩할 수 있어 여러 primitive의 동작(예: Tooltip.Trigger + Dialog.Trigger)을 하나의 요소에 조합할 수 있어요.