Slot 패턴
Slot 패턴은 React에서 부모 컴포넌트가 자식 요소를 직접 감싸지 않고, 자식 요소 자체가 부모의 역할을 하도록 만드는 기법이다. 이를 통해 불필요한 래퍼 태그를 제거하면서도 부모의 스타일이나 이벤트 핸들러를 그대로 자식에게 전달할 수 있다. 핵심 원리는 React.cloneElement()를 사용하여 자식 요소를 복사하고, 필요한 props를 추가하는 것이다.
import { cloneElement, isValidElement, ReactNode } from "react";
const Slot = ({ children, ...props }: { children?: ReactNode }) => {
if (!isValidElement(children)) {
throw new Error("Slot 컴포넌트는 하나의 React 요소만 가져야 합니다.");
}
return cloneElement(children, props);
};
이 패턴을 활용하면 컴포넌트의 재사용성과 유연성이 크게 향상된다. 특정 UI 요소를 감싸는 방식이 아닌, 그대로 유지하면서 추가적인 기능만 주입할 수 있기 때문에 다양한 상황에서 활용할 수 있다. 예를 들어, 버튼을 기본 <button> 태그로 렌더링하면서도 필요에 따라 <a>나 <span> 등으로 변경할 수 있어, 보다 직관적인 마크업을 구성할 수 있다. 또한, Slot 패턴은 상태 및 이벤트 핸들링을 부모가 직접 수행하는 것이 아니라, 자식 요소가 자연스럽게 이어받도록 설계할 수 있어, 보다 깔끔한 컴포넌트 구조를 유지하는 데 도움이 된다.
const Button = ({ asChild, ...props }: { asChild?: boolean } & ComponentPropsWithoutRef<"button">) => {
const Comp = asChild ? Slot : "button";
return <Comp {...props} className="bg-blue-500 text-white px-4 py-2 rounded" />;
};
export default function App() {
return (
<div>
<Button>기본 버튼</Button>
<Button asChild>
<Link href="/">링크 스타일 버튼</Link>
</Button>
</div>
);
}
하지만 나는 후술할 여러 이유로 인해 Slot 패턴을 아주 선호하지는 않는다. 우선 React.cloneElement는 자식 요소를 복사하고 새로운 props를 추가하는 방식으로 동작한다. 때문에 컴포넌트가 깊게 중첩되어 있을 때마다 복사가 이루어지므로, 렌더링 성능에 부담을 줄 수 있다. 또한, 부모 컴포넌트는 자식 컴포넌트에 대해 아는 바가 없기에 Slot이 의도대로 동작하지 않거나 예상치 못한 부작용을 일으킬 수 있다.
이걸 막기 위해서는 결국 문서화에 의존해야 하는데, 나는 코드가 그 자체의 의도를 (조금이라도) 드러내지 못하면 그 자체로 문제가 된다고 생각하는 사람이다. 때문에 이 패턴을 거의 사용하지 않고, 만에 하나 사용하더라도 오직 스타일 관련 로직만 처리하도록 하고 있다.
블로그의 정보
Ayden's journal
Beard Weard Ayden