처음 기획할때 하위 컴포넌트들과 상태를 공유하는 컴포넌트의 경우 Compound Components 패턴을 통해 구현할 계획이었다. 하지만 여러 변형을 가진 컴포넌트를 만났을때 문제가 발생했다.
// Compound Components 예제 코드
interface NavHeadlessProps {
redirect: () => void;
onChange: () => void;
openMenu: () => void;
onUpload: () => void;
}
// 컨텍스트 생성
const NavWrapperContext = createContext<NavHeadlessProps>({
redirect: () => {},
onChange: () => {},
openMenu: () => {},
onUpload: () => {},
});
// NavWrapper 컴포넌트의 자식요소에게 Context 공유
const NavWrapper = ({ children }: PropsWithChildren) => {
const value = {
redirect: () => {},
onChange: () => {},
openMenu: () => {},
onUpload: () => {},
};
return <NavWrapperContext.Provider value={value}>{children}</NavWrapperContext.Provider>;
};
// NavWrapper 하위 컴포넌트
const Container = ({ children }: { children: JSX.Element[] }) => {
return (
<div className="w-[390px] flex justify-between items-center px-4 py-3 border-b border-gray-250 bg-white">
{children}
</div>
);
};
// NavWrapper 하위 컴포넌트
const BackButton = ({ href, ...props }: { href: string }) => {
const { redirect } = useContext(NavWrapperContext);
return <ImgButton icon="leftArrow" onClick={() => redirect(href)} {...props} />;
};
// NavWrapper 하위 컴포넌트
const MoreButton = ({ ...props }) => {
const { openMenu } = useContext(NavWrapperContext);
return <ImgButton icon="moreVertical" onClick={openMenu} {...props} />;
};
// NavWrapper 컴포넌트 객체의 property로 저장
NavWrapper.Container = Container;
NavWrapper.BackButton = BackButton;
NavWrapper.MoreButton = MoreButton;
export default NavWrapper;
// 사용된 컴포넌트
export default function Home() {
return (
<main>
<NavWrapper>
<NavWrapper.Container>
<NavWrapper.BackButton href="/feed" />
<NavWrapper.MoreButton />
</NavWrapper.Container>
</NavWrapper>
</main>
);
}
// ImgButton 컴포넌트를 랩핑하여 상태를 전달한다.
const BackButton = ({ href, ...props }: { href: string }) => {
const { redirect } = useContext(NavWrapperContext);
return <ImgButton icon="leftArrow" onClick={() => redirect(href)} {...props} />;
};
NavWrapper.BackButton = BackButton;
// NavWrapper 하위 컴포넌트
const BackButton = ({ href, ...props }: { href: string }) => {
const { redirect } = useContext(NavWrapperContext); // useContext
// (...)
};
// NavWrapper 하위 컴포넌트
const MoreButton = ({ ...props }) => {
const { openMenu } = useContext(NavWrapperContext); // useContext
// (...)
};
// Headless UI 예제 코드
interface NavHeadlessProps {
redirect: () => void;
onChange: () => void;
openMenu: () => void;
onUpload: () => void;
}
// Headless UI 생성
const NavHeadless =({
children,
}: {
children: (args: NavHeadlessProps) => JSX.Element;
}) => {
const redirect = () => { };
const onChange = () => { };
const openMenu = () => { };
const onUpload = () => { };
// children이 없거나 함수가 아니라면 렌더링하지 않는다.
if (!children || typeof children !== "function") return null;
// 함수인 children에게 함수 파라미터로 값을 내려준다.
return children({ onChange, redirect, openMenu, onUpload });
}
export default NavHeadless;
// 기본 Nav Bar
const BasicNavBar = () => {
return (
<NavHeadless>
// 함수로 된 children을 생성해서 파라미터로 상태값을 받아 렌더링한다.
{({ openMenu, redirect }) => {
return (
<div className="w-[390px] flex justify-between items-center px-4 py-3 border-b border-gray-250 bg-white">
<ImgButton size="2xs" icon="leftArrow" onClick={() => redirect("/feed")}>
메인으로
</ImgButton>
<ImgButton size="xs" icon="moreVertical" onClick={openMenu}>
메뉴
</ImgButton>
</div>
);
}}
</NavHeadless>
);
}
// Feed 페이지 NavBar
const FeedNavBar = () => {
return (
<NavHeadless>
{({ redirect }) => {
return (
<div className="w-[390px] flex justify-between items-center px-4 py-3 border-b border-gray-250 bg-white">
<Text size="lg">우주마켓 피드</Text>
<ImgButton size="xs" icon="search" onClick={() => redirect("/search")} />
</div>
);
}}
</NavHeadless>
);
}
Headless UI 컴포넌트를 만들어서 사용하게 된다면 상태값을 받아서 이미 구현된 Atom 컴포넌트에 랩핑할필요 없이 전달해주면 된다.
Compound Components 패턴에 비해 간략화 되어있기 때문에 가독성이 좋아졌다.
Headless UI 컴포넌트는 로직만을 가지고 UI를 가지고 있지 않아 같은 기능을 가진 다른 스타일의 컴포넌트에 유연하게 대응할 수 있다.
마지막으로 디자인시스템에 적용하기 위해 variant 변수를 통해 렌더링할 NavBar를 선택할 수 있게 하였다.
interface IProps {
variant?: "basic" | "search" | "feed" | "upload" | "chat";
onClickRightIcon?: MouseEventHandler;
username?: string;
}
// 컴포넌트들을 객체화
const variants = {
basic: BasicNavBar,
search: SearchNavBar,
feed: FeedNavBar,
upload: UploadNavBar,
chat: ChatNavBar,
};
// 랜더러 함수를 만들어 variant를 통해 NavBar들을 선택할 수 있게 하였다.
export default function NavBar({ variant = "basic", username }: IProps) {
const renderer = variants[variant];
// 컴포넌트 중 Props를 받아야하는 경우 객체로 넘겨주었다.
return renderer({ username });
}
디자인 패턴은 상황에 맞게 알맞은 패턴을 사용해야한다는 점을 다시 한번 느꼈다. 사실 Compound Components
패턴을 직접 구현하면서 그냥 사용해도 됬지만 근거를 가지고 선택할 수 있게 되어서 학습에 큰 도움이 되었다.