본문으로 바로가기
윤창원uiwwsw · 작은 우주

개발과 기술

React Slot 패턴 — 자식에게 스타일과 클릭을 전달하는 규칙

2026-09-10: 설명과 코드 예시를 보완했습니다. 아래 예시는 글의 설계 의도를 전달하기 위한 것이며, 원 프로젝트에 반영된 변경 내역과는 구분합니다.

버튼처럼 보이는 UI를 링크로 사용하고 싶을 때, 바깥에 button을 하나 더 감싸면 인터랙티브 요소가 중첩될 수 있다. 자식 요소가 실제 DOM을 담당하고 공통 속성을 받도록 만드는 Slot 패턴을 살펴봤다.

먼저 정할 것은 병합 규칙

JSX 요소는 비교하고 복제할 수 있지만, 참조가 같다는 조건에 기대어 자식을 찾으면 사용 방식이 제한된다. 렌더링할 때마다 새 컴포넌트 함수를 만드는 방식은 컴포넌트 타입도 바뀌어 상태를 유지하기 어렵게 할 수 있다.

이 예시에서는 컴포넌트 타입을 고정하고 하나의 직접 자식에게 className과 onClick을 전달한다.

import { cloneElement, Fragment, type MouseEventHandler, type ReactElement } from 'react';

type ChildProps = {
  className?: string;
  onClick?: MouseEventHandler<HTMLElement>;
};

export function Slot({ children, className, onClick }: ChildProps & {
  children: ReactElement<ChildProps>;
}) {
  if (children.type === Fragment) {
    throw new Error('Slot requires a child that accepts DOM props.');
  }
  const childProps = children.props;
  return cloneElement(children, {
    className: [childProps.className, className].filter(Boolean).join(' ') || undefined,
    onClick: event => {
      childProps.onClick?.(event);
      if (!event.defaultPrevented) onClick?.(event);
    },
  });
}

자식의 클릭 핸들러를 먼저 실행하고, 그 핸들러가 기본 동작을 막았다면 Slot의 추가 클릭 처리를 생략하는 계약이다. 이 순서는 라이브러리마다 같다고 가정할 수 없으므로 인터페이스에 명시해야 한다.

<Slot className="button-style">
  <a href="https://velog.io/">블로그로 이동</a>
</Slot>

cloneElement는 props를 얕게 병합한다. className 결합이나 이벤트 합성이 자동으로 완성되는 것은 아니다. 데이터 흐름도 추적하기 어려워질 수 있어, 단순한 경우에는 명시적 props나 render prop을 사용하는 편이 나을 수 있다. React cloneElement

이 코드가 다루지 않는 것

이 예시는 하나의 자식, className, onClick만 다룬다. 전달받은 속성을 DOM에 반영하지 않는 사용자 컴포넌트에는 효과가 없다. style·여러 이벤트·외부 ref까지 합치려면 각각의 규칙이 더 필요하다.

링크에 버튼 스타일을 입혀도 버튼의 disabled 동작이 생기지는 않는다. 링크는 이동, 버튼은 동작이라는 의미를 유지해야 한다. 범용 UI 라이브러리라면 직접 구현한 범위와 검증된 Slot 구현의 범위를 비교할 필요가 있다.

내가 이 패턴에서 중요하게 보는 것은 JSX를 다룬다는 기술 자체보다, 누가 어떤 속성을 소유하고 충돌을 어떻게 처리하는지 드러내는 일이다.

Assisted by AI

윤창원이 벨로그에 남긴 글을 이 작은 우주에도 모았습니다. 사진은 누르면 원본 크기로 볼 수 있습니다. 원문의 전체 서식 보기 ↗

모든 글 둘러보기 →