상태 끌어올리기 패턴으로 형제 컴포넌트 간 상태를 공유하는 방법과 컴포넌트 합성(Composition) 패턴의 실전 활용을 배웁니다.
🤝 왜 상태를 끌어올려야 하는가
두 컴포넌트가 동일한 데이터를 공유해야 할 때 어떻게 해야 할까요? 리액트에서 데이터는 항상 위에서 아래로(부모 → 자식) 흐릅니다. 형제 컴포넌트끼리 직접 데이터를 주고받을 수 없습니다. 이 문제를 해결하는 패턴이 상태 끌어올리기(Lifting State Up)입니다.
핵심 아이디어: 두 컴포넌트가 공유해야 하는 상태를 가장 가까운 공통 부모로 이동시킵니다.
📌 문제 상황 — 온도 변환기
섭씨와 화씨 입력창이 있고, 하나를 바꾸면 다른 것도 자동으로 업데이트됩니다:
// ❌ 각자 state를 가지면 동기화 불가
function CelsiusInput() {
const [value, setValue] = useState('')
return <input value={value} onChange={(e) => setValue(e.target.value)} />
}
function FahrenheitInput() {
const [value, setValue] = useState('') // 완전히 별개의 상태!
return <input value={value} onChange={(e) => setValue(e.target.value)} />
}
이렇게 만들면 두 입력창의 값이 서로 연동되지 않습니다. 해결책은 상태를 부모로 끌어올리는 것입니다.
✅ 해결 — 상태를 공통 부모로 이동
// 자식 컴포넌트들: 값과 콜백을 Props로 받음
function TemperatureInput({
label,
value,
onChange,
}: {
label: string
value: string
onChange: (val: string) => void
}) {
return (
<label className="block">
<span className="text-sm font-medium">{label}</span>
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
className="mt-1 block w-full border rounded px-3 py-2"
/>
</label>
)
}
// 부모: 상태를 가지고 두 자식에 내려줌
function TemperatureConverter() {
const [celsius, setCelsius] = useState('')
const fahrenheit = celsius !== ''
? String(Math.round(parseFloat(celsius) * 9 / 5 + 32))
: ''
function handleCelsiusChange(val: string) {
setCelsius(val)
}
function handleFahrenheitChange(val: string) {
const f = parseFloat(val)
if (!isNaN(f)) {
setCelsius(String(Math.round((f - 32) * 5 / 9)))
} else {
setCelsius('')
}
}
return (
<div className="space-y-4 p-6 max-w-sm">
<h2 className="text-xl font-bold">온도 변환기</h2>
<TemperatureInput label="섭씨 (°C)" value={celsius} onChange={handleCelsiusChange} />
<TemperatureInput label="화씨 (°F)" value={fahrenheit} onChange={handleFahrenheitChange} />
</div>
)
}
이제 섭씨 입력창을 바꾸면 부모의 celsius state가 업데이트되고, fahrenheit 값이 자동으로 계산되어 두 번째 입력창에 반영됩니다.
🧩 컴포넌트 합성 (Composition)
상태 끌어올리기와 함께 중요한 패턴이 합성(Composition)입니다. children이나 render props를 활용해 컴포넌트를 유연하게 조합합니다.
// 레이아웃 컴포넌트 — children으로 내용을 유연하게 받음
function Panel({
title,
children,
action,
}: {
title: string
children: React.ReactNode
action?: React.ReactNode // 헤더 오른쪽에 올 요소 (선택)
}) {
return (
<div className="rounded-2xl border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b bg-gray-50">
<h3 className="font-bold text-gray-900">{title}</h3>
{action}
</div>
<div className="p-6">{children}</div>
</div>
)
}
// 사용: 완전히 다른 내용을 같은 Panel에 담을 수 있음
function Dashboard() {
return (
<div className="space-y-4">
<Panel title="최근 주문" action={<button className="text-sm text-indigo-600">전체 보기</button>}>
<OrderList />
</Panel>
<Panel title="알림" action={<span className="text-xs bg-red-100 text-red-600 px-2 py-0.5 rounded-full">3개 미읽음</span>}>
<NotificationList />
</Panel>
</div>
)
}
🗂️ 언제 상태를 끌어올릴까
항상 가장 가까운 공통 부모까지만 올립니다. 불필요하게 높이 올리면 prop drilling(깊은 곳까지 props를 전달하는 것)이 생깁니다. 규칙:
- 형제 컴포넌트가 같은 데이터를 필요로 할 때 → 부모로 끌어올리기
- 한 컴포넌트만 사용하는 상태 → 그 컴포넌트 안에 유지
- 많은 컴포넌트가 공유해야 할 때 → Context(뒤에서 다룸)
✅ 정리
- 리액트 데이터는 위→아래 단방향. 형제 컴포넌트는 직접 통신 불가.
- 공유 상태는 가장 가까운 공통 부모로 끌어올립니다.
- 자식은 값(Props)과 콜백(함수 Props)을 받아 표시하고 변경 요청만 합니다.
children과 선택적 Props(action?)로 유연한 합성 컴포넌트를 만듭니다.
관련 주제
- lifting state up
- 공통 부모 컴포넌트
- 콜백으로 상태 변경 전달
- 형제 컴포넌트 상태 공유
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
