리액트에서 복잡한 UI를 만들다 보면 재사용성, 확장성, 그리고 명확한 책임 분리가 매우 중요해진다.
실제 자주 사용되는 세 가지 패턴을 정리해보았다.
- Compound Component Pattern
- Higher-Order Component(HOC)
- React Portal
컴파운드 패턴은 여러 개의 컴포넌트가 마치 하나의 컴포넌트처럼 함께 동작하도록 만드는 패턴이다.
React의 Context API와 주로 함께 사용된다.
<Select>, <Select.Option>처럼 개발자가 사용하기 쉽고 직관적인 API를 만들고 싶을 때import { createContext, useContext, useState } from "react";
const TabsContext = createContext<any>(null);
export function Tabs({ children, defaultValue }) {
const [active, setActive] = useState(defaultValue);
return (
<TabsContext.Provider value={{ active, setActive }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }) {
return <div>{children}</div>;
};
Tabs.Trigger = function TabsTrigger({ value, children }) {
const { active, setActive } = useContext(TabsContext);
return (
<button
onClick={() => setActive(value)}
style={{
fontWeight: active === value ? "bold" : "normal",
}}
>
{children}
</button>
);
};
Tabs.Content = function TabsContent({ value, children }) {
const { active } = useContext(TabsContext);
return active === value ? <div>{children}</div> : null;
};
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1">탭 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">탭 2</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">내용 1</Tabs.Content>
<Tabs.Content value="tab2">내용 2</Tabs.Content>
</Tabs>
HOC는 컴포넌트를 인자로 받아 새로운 기능을 덧붙인 컴포넌트를 반환하는 함수다.
A higher-order component is a function that takes a component and returns a new component
function withAuth(Component) {
return function ProtectedComponent(props) {
const isLogin = Boolean(localStorage.getItem("token"));
if (!isLogin) return <div>로그인이 필요합니다.</div>;
return <Component {...props} />;
};
}
function MyPage() {
return <div>마이페이지!</div>;
}
export default withAuth(MyPage);
Portal은 컴포넌트를 DOM 트리 외부로 렌더링하는 기능이다.
즉, 시각적으로는 화면 맨 위(z-index 최고)에 떠 있어야 하지만 컴포넌트 구조는 기존 트리 하위에 있을 때 매우 유용하다.
import { createPortal } from "react-dom";
export default function Modal({ children }) {
const modalRoot = document.getElementById("modal-root");
return createPortal(
<div className="overlay">
<div className="modal">{children}</div>
</div>,
modalRoot
);
}
<div id="root"></div>
<div id="modal-root"></div>
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>모달 열기</button>
{open && (
<Modal>
<h2>모달 콘텐츠</h2>
</Modal>
)}
</>
);
}
overflow: hidden이나 z-index 영향을 받지 않도록 하기 위함