React에서 Slot 패턴과 Compound Component 패턴을 사용하여 컴포넌트를 구성하고 재사용성을 높일 수 있습니다. 각각의 패턴을 이해하고 적용 방법을 살펴보겠습니다.
Slot 패턴은 주로 다른 프레임워크, 특히 Vue.js에서 사용되는 패턴입니다. 이 패턴은 컴포넌트 내부의 특정 위치에 자식 콘텐츠를 삽입할 수 있도록 합니다.
Slot 컴포넌트
특정 이름을 가진 슬롯에 자식 컴포넌트를 랜더링할 수 있습니다.
// Slot.tsx
import { ReactNode } from "react";
interface SlotProps {
name: string;
children: ReactNode;
}
const Slot = ({ children }: SlotProps) => {
return <>{children}</>;
};
export default Slot;
createSlots 함수
React의 children
을 특정 슬롯 이름과 매칭시켜 슬롯 맵을 생성하는 유틸리티 함수입니다. 이 함수는 ReactNode
타입의 자식 요소들과 슬롯 이름 목록을 입력받아, 각 슬록 이름에 해당하는 자식 콘텐츠를 매핑할 객체를 반환합니다.
// createSlots.tsx
import { ReactNode, Children, isValidElement } from "react";
interface SlotMap {
[key: string]: ReactNode;
}
const createSlots = (children: ReactNode, slots: string[]): SlotMap => {
const slotMap: SlotMap = {};
Children.forEach(children, (child) => {
if (isValidElement(child)) {
const slotName = child.props.name;
if (slotName && slots.includes(slotName)) {
slotMap[slotName] = child.props.children;
}
}
});
return slotMap;
};
export default createSlots;
Todo 컴포넌트
Slot 패턴을 사용하여 자식 컴포넌트를 특정 슬롯에 매핑하고 렌더링합니다.
// Todo.tsx
import { ReactNode } from "react";
import createSlots from "../utils/createSlots";
import TodoProvider from "../Todo/TodoProvider";
interface TodoProps {
children: ReactNode;
}
const Todo = ({ children }: TodoProps) => {
const slots = createSlots(children, ["Header", "Input", "ListArea"]);
return (
<TodoProvider>
{slots.Header}
{slots.Input}
<div className="mt-5">{slots.ListArea}</div>
</TodoProvider>
);
};
export default Todo;
App.tsx
App에서 하위 컴포넌트를 Todo
컴포넌트의 특정 슬롯에 배치합니다.
// App.tsx
import Divider from "./components/Divider/Divider";
import TodoHeader from "./components/Header/TodoHeader";
import TodoInput from "./components/Input/TodoInput";
import TodoList from "./components/List/TodoList";
import TodoListTools from "./components/Tools/TodoListTools";
import TodoListArea from "./components/List/TodoListArea";
import Todo from "./components/Todo";
import Slot from "./components/Slot";
function App() {
return (
<main className="min-w-[500px] p-8 mt-8 | bg-white rounded-2xl shadow-lg">
<Todo>
<Slot name="Header">
<TodoHeader />
</Slot>
<Slot name="Input">
<TodoInput className="mt-5" />
</Slot>
<Slot name="ListArea">
<TodoListArea className="mt-5">
<TodoListTools />
<Divider />
<TodoList />
</TodoListArea>
</Slot>
</Todo>
</main>
);
}
export default App;
Compound Component 패턴은 여러 개의 하위 컴포넌트가 함께 작동하여 하나의 부모 컴포넌트의 기능을 구현하는 패턴입니다.
children
의 요소를 React.Children.map
을 사용하여 순회하면서 각 자식 요소가 유효한 요소인지 확인합니다.// tabs.tsx
import React, { useState, ReactNode } from "react";
interface TabsProps {
children: ReactNode;
}
const Tabs = ({ children }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<div className="tabs">
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return (
<button onClick={() => setActiveIndex(index)}>
{child.props.label}
</button>
);
}
return null;
})}
</div>
<div className="content">
{React.Children.toArray(children)[activeIndex]}
</div>
</div>
);
};
interface TabProps {
label: string;
children: ReactNode;
}
Tabs.Tab = ({ children }: TabProps) => {
return <div>{children}</div>;
};
export default Tabs;
// App.tsx
import Tabs from "./Common/Tabs";
function App() {
return (
<div className="App">
<Tabs>
<Tabs.Tab label="Tab 1">Content 1</Tabs.Tab>
<Tabs.Tab label="Tab 2">Content 2</Tabs.Tab>
<Tabs.Tab label="Tab 3">Content 3</Tabs.Tab>
</Tabs>
</div>
);
}
export default App;
Slot 패턴과 Compound Component 패턴은 각각의 장단점이 있으며, 특정 상황에 따라 유용하게 사용할 수 있습니다. Slot 패턴은 자식 요소를 특정 슬롯에 배치하는 데 유용하며, Compound Component 패턴은 여러 하위 컴포넌트를 논리적으로 그룹화하여 부모 컴포넌트의 기능을 구현하는 데 유용합니다. 실무에서 이 두 패턴을 적용해보면서 각 패턴의 적합성을 경험하고, 프로젝트의 요구 사항에 맞게 적절한 패턴을 선택하여 사용하는 것이 중요하다 생각합니다.