레슨 6 / 9·20분
컴포넌트 패턴과 Context
컴포넌트 합성 패턴
React에서는 상속보다 합성(Composition)을 선호합니다. children prop, 렌더 프롭, 컴파운드 컴포넌트 패턴 등으로 유연한 UI를 설계합니다.
tsx
// 레이아웃 컴포넌트 — children 활용
function PageLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen">
<header className="h-16 border-b">헤더</header>
<main className="p-8">{children}</main>
<footer className="h-16 border-t">푸터</footer>
</div>
);
}
// 슬롯 패턴 — 여러 영역을 prop으로 전달
function Dialog({
title,
content,
footer,
}: {
title: React.ReactNode;
content: React.ReactNode;
footer: React.ReactNode;
}) {
return (
<div className="dialog">
<div className="dialog-title">{title}</div>
<div className="dialog-content">{content}</div>
<div className="dialog-footer">{footer}</div>
</div>
);
}
// 사용
<Dialog
title={<h2>확인</h2>}
content={<p>정말 삭제하시겠습니까?</p>}
footer={
<>
<button>취소</button>
<button>삭제</button>
</>
}
/>Context API
Context는 prop drilling(깊은 컴포넌트까지 props를 전달하는 문제) 없이 데이터를 공유하는 방법입니다. 테마, 인증 정보, 언어 설정 등 전역 상태에 적합합니다.
tsx
import { createContext, useContext, useState } from 'react';
// 1. Context 생성
type Theme = 'light' | 'dark';
const ThemeContext = createContext<{
theme: Theme;
toggle: () => void;
}>({ theme: 'light', toggle: () => {} });
// 2. Provider 컴포넌트
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// 3. 커스텀 Hook으로 편리하게 사용
function useTheme() {
return useContext(ThemeContext);
}
// 4. 소비 컴포넌트 — 어디서든 사용 가능
function ThemeButton() {
const { theme, toggle } = useTheme();
return (
<button onClick={toggle}>
현재 테마: {theme === 'light' ? '라이트' : '다크'}
</button>
);
}
// 5. App에서 Provider로 감싸기
function App() {
return (
<ThemeProvider>
<div>
<h1>앱</h1>
<ThemeButton />
</div>
</ThemeProvider>
);
}useReducer
useReducer는 복잡한 상태 로직을 관리할 때 useState보다 적합합니다. Redux와 유사한 패턴으로, action을 dispatch하면 reducer 함수가 새 상태를 반환합니다.
tsx
import { useReducer } from 'react';
type State = { count: number };
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'set'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
case 'set': return { count: action.payload };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>카운트: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>초기화</button>
<button onClick={() => dispatch({ type: 'set', payload: 100 })}>
100으로 설정
</button>
</div>
);
}💡
Context + useReducer 조합은 소규모 앱에서 Redux를 대체할 수 있습니다. 하지만 Context 값이 변경되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링되므로, 대규모 앱에서는 상태 관리 라이브러리(Zustand, Jotai 등)를 고려하세요.