kecan0406

Compound Pattern을 사용한 React 컴포넌트 만들기


Compound Component는 부모와 자식 컴포넌트 간의 공통된 상태를 공유한다. 이 패턴은 컴포넌트에서 로직과 UI를 명시적으로 분리할 수 있어 관리가 쉽다는 장점이 있다.

대표적인 예로 <select><option>의 관계를 들 수 있다.

<select name="pets">
  <option value="dog">Dog</option>
  <option value="cat">Cat</option>
  <option value="hamster">Hamster</option>
  <option value="parrot">Parrot</option>
  <option value="spider">Spider</option>
  <option value="goldfish">Goldfish</option>
</select>
  • <select>부모 컴포넌트로서 공통 상태와 로직을 제공하고 관리한다.
  • <option>자식 컴포넌트로서 부모 컴포넌트가 관리하는 상태와 로직에 따라 동작한다.

React에서 이러한 Compound Pattern을 활용하면, 상태와 로직을 부모에서 관리하고 자식 컴포넌트는 UI 역할에 집중하도록 설계할 수 있다.

이번 글에서는 Stepper 컴포넌트를 예시로 Compound Pattern을 적용하는 방법을 살펴보자.

#Stepper Compound Component 구현하기

#Stepper란?

Stepper는 사용자가 특정 작업의 진행 상태를 단계별로 시각화할 수 있는 UI 요소입니다. 예를 들어, 쇼핑몰 결제 과정에서

  • 장바구니
  • 배송 정보 입력
  • 결제 정보 입력
  • 주문 확인

이러한 순차적인 단계를 나타낼 때 자주 사용된다.


#Stepper 구조 설계

Stepper 컴포넌트에는 기본적으로 다음 세가지의 구조로 구성된다:

  • Stepper: 공통 상태를 **Stepper.StepStepper.Navigation**같은 자식 컴포넌트에게 제공한다.

  • Stepper.Step: 특정 단계의 내용을 표시하는 자식 컴포넌트.

  • Stepper.Navigation: 특정 단계로 이동할 수 있는 네비게이션 UI를 제공하는 자식 컴포넌트.


#코드 구현

#1. Stepper 컨텍스트 생성

React의 Context API를 사용해 Stepper의 자식들에게 상태를 제공한다.

import { useContext } from 'react'
import { createContext, useState, type ReactNode } from 'react'
 
const StepperContext = createContext<{ ids: string[]; step: string; setStep: (step: string) => void }>(null!)
 
type StepperProps = { children: ReactNode; ids: string[]; defaultId?: string }
export default function Stepper({ children, ids, defaultId }: StepperProps) {
  const [step, setStep] = useState<string>(defaultId!)
 
  return <StepperContext.Provider value={{ ids, step, setStep }}>{children}</StepperContext.Provider>
}

#2. Step 컴포넌트 구현

각 단계의 내용을 표시하는 컴포넌트. 현재 stepid가 일치할 때만 렌더링된다.

function Step({ id, children }: { id: string; children: ReactNode }) {
  const { step } = useContext(StepperContext)
 
  return id === step && <>{children}</>
}
Stepper.Step = Step

#4. Navigation 컴포넌트 구현

사용자가 이전, 이후 단계를 선택할 수 있도록 구성한다.

function Navigation() {
  const { ids, step, setStep } = useContext(StepperContext)
 
  const prevStep = () => setStep(ids[ids.indexOf(step) - 1])
  const nextStep = () => setStep(ids[ids.indexOf(step) + 1])
 
  return (
    <div>
      <button disabled={ids.at(0) === step} onClick={prevStep}>
        이전
      </button>
      <button disabled={ids.at(-1) === step} onClick={nextStep}>
        다음
      </button>
    </div>
  )
}
Stepper.Navigation = Navigation

#사용 예시

위에서 구성한 Stepper를 활용하여 간단한 UI를 구성해보자.

import Stepper from './Stepper'
 
function App() {
  return (
    <Stepper defaultId="step1" ids={['step1', 'step2', 'step3']}>
      <Stepper.Step id="step1">
        <div>1단계: 사용자 정보 입력</div>
      </Stepper.Step>
      <Stepper.Step id="step2">
        <div>2단계: 배송 정보 입력</div>
      </Stepper.Step>
      <Stepper.Step id="step3">
        <div>3단계: 결제 정보 입력</div>
      </Stepper.Step>
      <Stepper.Navigation />
    </Stepper>
  )
}

#마무리

Compound Pattern은 Radix-UI와 같은 UI 라이브러리에서 자주 볼 수 있는 패턴이다. select. dialog, form, card 등 다양한 UI들을 Compound Pattern을 적용시키면 UI와 로직의 역할을 분리하고, 재사용성과 확장성을 높일 수 있다.

Stepper 예제처럼 상태와 로직을 부모에서 관리하며, 자식 컴포넌트는 자신의 역할에만 집중할 수 있도록 설계해보자. Compound Pattern은 React 애플리케이션에서 복잡한 UI를 설계할 때 좋은 패턴이다.


#References

patterns.dev - Compound Pattern react.dev - createContext