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.Step
과Stepper.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 컴포넌트 구현
각 단계의 내용을 표시하는 컴포넌트. 현재 step
과 id
가 일치할 때만 렌더링된다.
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를 설계할 때 좋은 패턴이다.