Learning
레슨 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 등)를 고려하세요.