[React] 컴파운드 컴포넌트 패턴을 알아보자

강경서·2023년 11월 1일
1
post-thumbnail

Intro

React로 여러 컴포넌트를 만들다 보면 이를 재사용하기 props를 추가하면서 처음 계획가 달리 컴포넌트의 구조가 복잡해지는 경우가 있었습니다. 이로 인해 코드의 가독성이 떨어지거나 새로운 컴포넌트를 만들게 되었습니다.

위와 같은 상황에서 컴파운드 컴포넌트 패턴(Compound Components Pattern)를 사용하여 해결이 가능하여 해당 글을 기록합니다.


컴파운드 컴포넌트 패턴

컴파운드 컴포넌트은 headless component로 기능은 있지만 스타일이 없는 컴포넌트입니다.
prop를 사용하지 않고 내부에서 데이터를 처리할 수 있고, 비슷한 디자인의 컴포넌트가 필요할 때 새로운 컴포넌트를 만들지 않고 사용할 수 있습니다. 이를 통해 유연하고 재사용 가능한 컴포넌트를 설계할 수 있고, 가독성과 유지 보수성을 높일 수 있습니다.


Context API

컴포넌트 내부에서 데이터를 처리하기 위해 Context API를 사용합니다.


CheckBox Component

일반적인 체크박스를 만들게 된다면 아래와 같은 코드를 가지게됩니다.

import { useState } from 'react'

const Checkbox = () => {
  const [isChecked, setIsChecked] = useState(false)
  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={() => setIsChecked(!isChecked)}
      />
      <span>체크박스 만들기</span>
    </label>
  )
}

export default Checkbox

이를 컴포넌트로 만들어서 다른 곳에서 사용하기 위해서는 어떤 내용을 체크하는 지에 대한 라벨, 체크가 되었는지의 상태 값, 체크하는 로직을 props를 받아야 합니다. 수정한다면 아래와 같은 형태가 될 것입니다.

type CheckboxProps = {
  label: string
  isChecked: boolean
  onChange: () => void
}

const Checkbox = ({ label, isChecked, onChange }: CheckboxProps) => {
  return (
    <label>
      <input type="checkbox" checked={isChecked} onChange={onChange} />
      <span>{label}</span>
    </label>
  )
}

export default Checkbox
export default function App() {
  const [isChecked, setIsChecked] = useState(false)
  return (
    <Checkbox
      label="체크박스 만들기"
      isChecked={isChecked}
      onChange={() => setIsChecked(!isChecked)}
    />
  )
}

만약 체크박스를 사용하는 모든 곳에서 디자인과 기능이 동일하다면 이대로 사용해도 문제없지만,
특정 페이지들에서만 색상을 다르게 한다던지, 모바일에서는 체크박스를 오른쪽으로 옮겨야 한다던지 등의 레이아웃의 변경이 필요하다면 상황이 곤란해집니다.


Compound Component

import * as React from 'react'

type CheckboxContextProps = {
  id: string
  isChecked: boolean
  onChange: () => void
}

type CheckboxProps = CheckboxContextProps & React.PropsWithChildren<{}>

const CheckboxContext = React.createContext<CheckboxContextProps>({
  id: '',
  isChecked: false,
  onChange: () => {},
})

const CheckboxWrapper = ({
  id,
  isChecked,
  onChange,
  children,
}: CheckboxProps) => {
  const value = {
    id,
    isChecked,
    onChange,
  }
  return (
    <CheckboxContext.Provider value={value}>
      {children}
    </CheckboxContext.Provider>
  )
}

const useCheckboxContext = () => {
  const context = React.useContext(CheckboxContext)
  return context
}

const Checkbox = ({ ...props }) => {
  const { id, isChecked, onChange } = useCheckboxContext()
  return (
    <input
      type="checkbox"
      id={id}
      checked={isChecked}
      onChange={onChange}
      {...props}
    />
  )
}

const Label = ({ children, ...props }: React.PropsWithChildren<{}>) => {
  const { id } = useCheckboxContext()
  return (
    <label htmlFor={id} {...props}>
      {children}
    </label>
  )
}

CheckboxWrapper.Checkbox = Checkbox
CheckboxWrapper.Label = Label

export default CheckboxWrapper
import CheckboxWrapper from './CheckboxWrapper'

export default function App() {
  const [isChecked, setIsChecked] = useState(false)
  return (
    <CheckboxWrapper
      id="checkbox-1"
      isChecked={isChecked}
      onChange={() => setIsChecked(!isChecked)}
    >
      <CheckboxWrapper.Checkbox />
      <CheckboxWrapper.Label>체크박스 만들기</CheckboxWrapper.Label>
    </CheckboxWrapper>
  )
}

컴포넌트 내부에서 state를 공유하기 위해 Context API를 사용해서 처음에 작성해야 하는 코드가 꽤 많지만, 컴포넌트를 사용하는 곳에서는 하위에 어떤 컴포넌트가 있는지 볼 수 있고, 위치도 자유롭게 수정 가능합니다.


📝 후기

React에서의 컴포넌트는 재사용이 중요하다고 생각하여 지금까지 무분별하게 많은 props를 사용하거나 복잡하게 코드를 작성했었습니다. 어떻게든 코드를 집어넣으며 컴포넌트를 만들었더니 오히려 코드의 가독성을 가독성대로 떨어지고 컴포넌트를 사용할 때도 다른 사람들이 보기에 이해하기 어려운 조건으로 가득 찬 컴포넌트를 만들었습니다. 앞으론 적절한 상황에서 컴파운드 컴포넌트를 통해 코드를 작성한다면 React를 더욱 효과적으로 활용할 수 있을 거 같습니다.


🧾 Reference

profile
기록하고 배우고 시도하고

0개의 댓글