[React] Compound Component Pattern

김학재·2023년 8월 2일
0

리액트

목록 보기
6/8
post-thumbnail

최근 리액트로 프로젝트를 만드는 중인데, 혼자 진행하는 프로젝트이다보니 코드를 어떻게 더 이쁘게 짤 수 있을지, 더 나은 컴포넌트를 만들 수 있을지 고민을 지속하고 있었다.

그러던 중 Compound Component라는 개념에 대해 접하게 되었고 이를 활용하면 컴포넌트를 좀 더 우아하게 만들 수 있을 것 같아 여기에 공부한 내용을 정리하고 실제로 프로젝트에도 적용해보려고 한다


개념

정확히는 Compound Component가 아닌 Compound Component Pattern이 맞다. 즉, 특정 컴포넌트를 말하는 것이 아닌 컴포넌트를 작성하는 디자인 패턴을 말한다.

요지는, 두 개 이상의 컴포넌트(주로 부모와 자식 컴포넌트)가 특정 작업을 수행하기 위해 함께 동작하고 있을 때, 부모 컴포넌트에게는 자식 컴포넌트와 통신을 좀 더 용이하게 하면서, 로직과 UI를 분리하는 리액트 디자인 패턴이다.

즉, 여러 개의 작은 컴포넌트들이 각각의 역할을 분담하도록 하고 이를 조립해 하나의 큰 컴포넌트를 만드는 것이다.

예시 1

FlyOut 컴포넌트 만들기

링크의 예시를 기준으로 정리해보려고 한다

위와 같은 이미지 목록의 각 이미지마다 FlyOut컴포넌트를 추가해 유저가 이미지를 수정하고 삭제할 수 있도록 만드려고 한다.

FlyOut 컴포넌트는 다음과 같은 요소들을 가지고 있을 것이다.

  • FlyOut wrapper : 토글 버튼과 리스트를 포함하는 wrapper
  • Toggle button : List를 토글
  • List : 메뉴 아이템을 포함

위와 같은 동작을 하는 FlyOut 컴포넌트를 React의 Context API와 Compound component pattern을 활용해서 만들어보자

// FlyOut component
const FlyOutContext = createContext()

functoin FlyOut(props) {
  const [open, toggle] = useState(false)
  
  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      { props.children }
    </FlyOutConext.Provider>
  )
}

FlyOut 컴포넌트는 자식 컴포넌트에게 open, toggle value를 전달할 수 있다.

// Toggle component
function Toggle() {
  const { open, toggle } = useContext(FlyOutContext)
  
  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  )
}

Toggle 컴포넌트는 유저가 메뉴를 토글하기 위해 클릭할 수 있는 컴포넌트이다.

Toggle 컴포넌트가 FlyOutContext provider에 접근할 수 있도록 하려면, FlyOut 컴포넌트의 자식 컴포넌트로 렌더해야 한다.

그러나, 자식 컴포넌트로 렌더링하는 방식 대신, Toggle 컴포넌트를 FlyOut 컴포넌트의 속성 중 하나로 만들 수 있다.

const FlyOutContext = createContext()
 
function FlyOut(props) {
  const [open, toggle] = useState(false)
 
  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  )
}
 
function Toggle() {
  const { open, toggle } = useContext(FlyOutContext)
 
  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  )
}
 
FlyOut.Toggle = Toggle

이렇게 함으로써 FlyOut 을 import 해서 사용할 수 있게 된다

import React from "react"
import { FlyOut } from "./FlyOut"
 
export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
    </FlyOut>
  );
}

토글 동작을 통해 열리고 닫힐 List 컴포넌트 또한 필요하다

// List component
function List({ children }) {
  const { open } = React.useContext(FlyOutContext)
  return open && <ul>{ children }</ul>
}

function Item({ children }) {
  return <li>{ children }</li>
}

List 컴포넌트는 open값에 따라 자식 컴포넌트를 렌더링한다

Toggle 컴포넌트와 같이 List, Item 또한 FlyOut 컴포넌트의 속성으로 만든다

const FlyOutContext = createContext()
 
function FlyOut(props) {
  const [open, toggle] = useState(false)
 
  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  )
}
 
function Toggle() {
  const { open, toggle } = useContext(FlyOutContext)
 
  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  )
}

function List({ children }) {
  const { open } = React.useContext(FlyOutContext)
  return open && <ul>{ children }</ul>
}

function Item({ children }) {
  return <li>{ children }</li>
}
 
FlyOut.Toggle = Toggle
FlyOut.List = List
FlyOut.Item = Item

위와 같은 코드를 통해 FlyOut 컴포넌트의 properties들을 사요할 수 있다.

import React from "react"
import { FlyOut } from "./FlyOut"
 
export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
      <FlyOut.List>
        <FlyOut.Item>Edit</FlyOut.Item>
        <FlyOut.Item>Delete</FlyOut.Item>
      </FlyOut.List>
    </FlyOut>
  )
}

위와 같이 Compound Component Pattern과 React의 Context API를 함꼐 사용하여 컴포넌트 구조를 만들었다.

그러나 매번 context를 사용하기 보다, prop drilling을 피하기 위해 컴포넌트를 개선할 수 있는 상황에서 사용하면 좋은 방법이 될 것이다.

예시 2

Toggle 컴포넌트 만들기

출처 : React Hooks: Compound Components

// Toggle.jsx
import { useRef, useState, useEffect, useCallback, createContext, useMemo, useContext } from 'react'
import { Switch } from './Switch'

// 1. 새로운 컨텍스트를 생성한다
const ToggleContext = createContext()

// 2. 컴포넌트가 마운트 된 후에 useEffect 콜백을 호출한다
function useEffectAfterMount(cb, dependencies) {
  const justMounted = useRef(true)
  useEffect(() => {
    if (!justMounted.current) {
      return cb()
    }
    justMounted.current = false
  }, dependencies)
}

// 3. useState 훅을 사용하여 토글 상태를 관리한다.
// useCallback 훅을 사용하여 토글 함수를 메모이제이션한다
// 컴포넌트가 마운트 된 후에는 useEffectAfterMount 함수를 사용해
// 토글 상태가 변경될 때마다 props.onToggle 콜백을 호출한다
function Toggle(props) {
  const [on, setOn] = useState(false)
  const toggle = useCallback(() => setOn(oldOn => !oldOn), [])
  useEffectAfterMount(
    () => {
      props.onToggle(on)
    },
    [on],
  )
  const value = useMemo(() => ({on, toggle}), [on])
  return (
    <ToggleContext.Provider value={value}>
      {props.children}
    </ToggleContext.Provider>
  )
}

// 4. On, Off 컴포넌트는 Toggle 컴포넌트의 상태에 따라
// 자식 컴포넌트를 조건부로 렌더링한다
function On({children}) {
  const {on} = useContext(ToggleContext)
  return on ? children : null
}

function Off({children}) {
  const {on} = useContext(ToggleContext)
  return on ? null : children
}

function Button(props) {
  const {on, toggle} = useContext(ToggleContext)
  return <Switch on={on} onClick={toggle} {...props} />
}

Toggle.On = On
Toggle.Off = Off
Toggle.Button = Button

export default Toggle


정리

  • Compound Component Pattern 은 여러 컴포넌트가 하나의 동작에 관여하는 경우 각 컴포넌트를 분리해 확장 및 재사용에 유연한 컴포넌트를 만드는 디자인 패턴이다
  • Context API를 사용해 구현할 수 있다
  • Prop Drilling을 개선하기 위해 context를 사용할 수 있다

생소했지만 막상 정리해보니 어려운 개념은 아니었다. 이제 실제 프로젝트에도 적용해보고 내 것으로 만들 차례이다

참고 문서
React Hooks: Compound Components
react.dev - Passing Data Deeply with Context
카카오 엔터테인먼트 기술 블로그 - 아토믹 디자인을 활용한 디자인 시스템 도입기
patterns.dev - Compound Pattern

profile
YOU ARE BREATHTAKING

0개의 댓글