-
Compound 패턴React/design patterns 2023. 12. 14. 12:57
컴파운드 컴포넌트 패턴은 여러 컴포넌트들이 모여 하나의 동작을 할 수 있게 해준다.
디자인시스템을 만들기 위해 래퍼런스를 찾는 중, Bootstrap이 해당 패턴을 사용하여 컴포넌트를 만들고 있었다.
장점: 컴파운드 패턴은 동작 구현에 필요한 상태를 내부적으로 가지고 있는데 이것을 사용하는 쪽에서는 드러나지 않아 걱정 없이 사용할 수 있다. 또 이 패턴을 사용하면 자식 컴포넌트들을 일일히 import할 필요 없이 기능을 사용할 수 있다.
Context API 를 활용해서 컴파운드 패턴을 구현했고,
사용자가 토글 버튼을 눌렀을 때 나타날 메뉴를 렌더링하는 컴포넌트를 만들어 보았다.
Toggle, List, Item 컴포넌트가 FlyOutContext Provider에 접근할 수 있도록 해당 컴포넌트는 FlyOut의 자식 컴포넌트로 렌더링해야 한다. Static property로 만들어 줬다.
import React, { useState } from 'react'; import { createContext, useContext } from 'react'; // Context Create const FlyOutContext = createContext(); // Provider export function FlyOut({ children }) { const [open, setOpen] = useState(false); const toggle = () => { setOpen((prev) => !prev); }; const providerValue = { open, toggle }; return ( <FlyOutContext.Provider value={providerValue}> {children} </FlyOutContext.Provider> ); } // Children Component function Toggle() { const { open, toggle } = useContext(FlyOutContext); return ( <div className="absolute p-[10px] h-[50px] border bg-black" onClick={() => toggle(!open)} > 토글 </div> ); } function List({ children }) { const { open } = useContext(FlyOutContext); return ( open && ( <ul className="absolute top-[50px] bg-[skyblue] px-[10px]">{children}</ul> ) ); } function Item({ children }) { return <li>{children}</li>; } // Toggle 컴포넌트가 FlyOutContext 프로바이더에 접근할 수 있도록 해당 컴포넌트는 FlyOut의 자식 컴포넌트로 렌더링 해야한다. FlyOut.Toggle = Toggle; FlyOut.List = List; FlyOut.Item = Item;
아래와 같이 FlyOut 컴포넌트만 import 해줘도 Toggle, List, Item 컴포넌트 등을 사용할 수 있게 된다.
import React from 'react'; import { useNavigate } from 'react-router-dom'; import classNames from 'classnames'; import formatAgo from '../../util/date'; import styles from './VideoCard.module.scss'; import { FlyOut } from '../FlyOut/FlyOut'; export default function VideoCard({ video, type, ...rest }) { const { title, channelTitle, publishedAt, thumbnails } = video.snippet; const navigate = useNavigate(); const isList = type === 'list'; return ( <li className={isList ? 'flex gap-1 m-2' : 'relative'} onClick={() => { navigate(`/videos/watch/${video.id}`, { state: { video } }); }} > <FlyOut> <FlyOut.Toggle /> <FlyOut.List> <FlyOut.Item>수정</FlyOut.Item> <FlyOut.Item>삭제</FlyOut.Item> </FlyOut.List> </FlyOut> <img className={isList ? 'w-60 mr-2' : 'w-full'} src={thumbnails.medium.url} alt={title} /> <div className={styles.content}> <h3 className={classNames(styles.title, 'line-clamp-3')}>{title}</h3> <p className={styles.channel}>{channelTitle}</p> <p className={styles.publishedAt}>{formatAgo(publishedAt, 'ko')}</p> </div> </li> ); }
결과)