포스트

[이제와서 시작하는 React 마스터하기 #7] Context API - Props 지옥에서 탈출하기

[이제와서 시작하는 React 마스터하기 #7] Context API - Props 지옥에서 탈출하기

React Day 7 - Props를 5단계 깊이로 전달하느라 지치셨나요? Context API가 여러분을 구해드릴게요! 오늘은 전역 상태를 쉽게 관리하는 방법을 배워봅시다.

오늘 배울 내용

  • ✅ Props Drilling 문제: 왜 불편한가?
  • ✅ Context API란 무엇인가?
  • ✅ createContext, Provider, useContext 사용법
  • ✅ 실전 예제: 테마 전환, 사용자 인증 상태

시작하기 전에

이전에 배운 내용을 기억하시나요?

하지만 Props를 계속계속 전달하다 보면 코드가 너무 복잡해져요. 오늘은 이 문제를 해결하는 Context API를 배워봅시다!


Props Drilling 문제

Props Drilling은 Props를 여러 단계의 컴포넌트를 거쳐 전달해야 하는 상황을 말해요.

일상 비유 🏢

회사에서 최고경영자(CEO)가 말단 직원에게 메시지를 전달한다고 생각해보세요:

1
2
3
CEO → 부사장 → 팀장 → 과장 → 대리 → 사원

"사원에게: 내일 회의실 예약해주세요"

중간에 있는 부사장, 팀장, 과장, 대리는 이 메시지를 전달만 하고, 실제로 사용하지는 않아요. 번거롭죠?

React도 똑같아요!

코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 😰 Props Drilling 문제
function App() {
  const [user, setUser] = useState({ name: '홍길동' });

  return <Layout user={user} />;
}

function Layout({ user }) {
  // Layout은 user를 사용하지 않지만 전달만 함
  return (
    <div>
      <Header user={user} />
    </div>
  );
}

function Header({ user }) {
  // Header도 user를 사용하지 않지만 전달만 함
  return (
    <header>
      <Navigation user={user} />
    </header>
  );
}

function Navigation({ user }) {
  // Navigation도 user를 사용하지 않지만 전달만 함
  return (
    <nav>
      <UserMenu user={user} />
    </nav>
  );
}

function UserMenu({ user }) {
  // 드디어 여기서 user를 사용!
  return <div>안녕하세요, {user.name}님!</div>;
}

문제점:

  • user가 필요한 곳은 UserMenu뿐인데, 모든 중간 컴포넌트가 props를 받아서 전달해야 해요
  • 코드가 복잡하고 유지보수가 어려워요
  • 나중에 user 이름을 바꾸면? 모든 컴포넌트를 수정해야 해요!

Context API가 뭔가요?

Context API는 Props를 거치지 않고, 필요한 컴포넌트에서 직접 데이터를 가져올 수 있게 해주는 기능이에요.

일상 비유 📻

Props Drilling은 전화 릴레이처럼 메시지를 하나씩 전달하는 거예요.

Context API는 라디오 방송처럼 한 곳에서 방송하면, 듣고 싶은 사람만 라디오를 켜서 들으면 돼요!

1
2
3
4
5
📻 Context (방송국)
     ↓
🔊 "안녕하세요, 홍길동님!"
     ↓
📱 듣고 싶은 컴포넌트들만 수신!

Context API 사용 방법

Context API는 3단계로 사용해요:

  1. createContext(): Context 만들기
  2. Provider: 데이터 방송하기
  3. useContext(): 데이터 수신하기

실습 1: 테마 전환 (다크모드)

가장 많이 사용하는 예제예요! 웹사이트 전체의 테마(밝은 모드/어두운 모드)를 관리해봅시다.

Step 1: Context 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { createContext, useContext, useState } from 'react';

// 1. Context 생성
const ThemeContext = createContext();

// 2. Provider 컴포넌트 만들기
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light'); // 'light' 또는 'dark'

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  // 제공할 값
  const value = {
    theme,
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. useTheme Hook (간편하게 사용하기 위해)
export function useTheme() {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error('useTheme은 ThemeProvider 안에서만 사용 가능합니다!');
  }

  return context;
}

Step 2: Provider로 감싸기

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ThemeProvider } from './ThemeContext';

function App() {
  return (
    <ThemeProvider>
      <div>
        <Header />
        <Main />
        <Footer />
      </div>
    </ThemeProvider>
  );
}

Step 3: 필요한 곳에서 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useTheme } from './ThemeContext';

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header
      style=
    >
      <h1>내 웹사이트</h1>
      <button onClick={toggleTheme}>
        {theme === 'light' ? '🌙 다크모드' : '☀️ 라이트모드'}
      </button>
    </header>
  );
}

function Main() {
  const { theme } = useTheme();

  return (
    <main
      style=
    >
      <h2>메인 컨텐츠</h2>
      <p>현재 테마: {theme}</p>
    </main>
  );
}

function Footer() {
  const { theme } = useTheme();

  return (
    <footer
      style=
    >
      <p>© 2025 My Website</p>
    </footer>
  );
}

결과:

  • Props를 전달하지 않아도 돼요!
  • useTheme()을 호출하면 어디서든 테마를 사용할 수 있어요!
  • 버튼을 클릭하면 전체 사이트의 테마가 바뀌어요!

전체 코드 (한 파일로 보기)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import React, { createContext, useContext, useState } from 'react';

// 1. Context 생성
const ThemeContext = createContext();

// 2. Provider 컴포넌트
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. useTheme Hook
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme은 ThemeProvider 안에서만 사용 가능합니다!');
  }
  return context;
}

// 컴포넌트들
function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header
      style={{
        padding: '20px',
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        borderBottom: `2px solid ${theme === 'light' ? '#ddd' : '#555'}`
      }}
    >
      <h1>🎨 테마 전환 데모</h1>
      <button
        onClick={toggleTheme}
        style={{
          padding: '10px 20px',
          fontSize: '16px',
          backgroundColor: theme === 'light' ? '#333' : '#fff',
          color: theme === 'light' ? '#fff' : '#333',
          border: 'none',
          borderRadius: '5px',
          cursor: 'pointer'
        }}
      >
        {theme === 'light' ? '🌙 다크모드' : '☀️ 라이트모드'}
      </button>
    </header>
  );
}

function Main() {
  const { theme } = useTheme();

  return (
    <main
      style={{
        padding: '40px',
        minHeight: '400px',
        backgroundColor: theme === 'light' ? '#f5f5f5' : '#1a1a1a',
        color: theme === 'light' ? '#333' : '#fff'
      }}
    >
      <h2>메인 컨텐츠</h2>
      <p>현재 테마: <strong>{theme === 'light' ? '라이트 모드' : '다크 모드'}</strong></p>
      <p>Context API를 사용하면 Props 전달 없이도 모든 컴포넌트에서 테마에 접근할 수 있어요!</p>
    </main>
  );
}

function Footer() {
  const { theme } = useTheme();

  return (
    <footer
      style={{
        padding: '20px',
        textAlign: 'center',
        backgroundColor: theme === 'light' ? '#e0e0e0' : '#222',
        color: theme === 'light' ? '#666' : '#ccc',
        borderTop: `2px solid ${theme === 'light' ? '#ccc' : '#444'}`
      }}
    >
      <p>© 2025 My Website | Powered by React Context API</p>
    </footer>
  );
}

// App 컴포넌트
function App() {
  return (
    <ThemeProvider>
      <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
        <Header />
        <Main />
        <Footer />
      </div>
    </ThemeProvider>
  );
}

export default App;

해보기:

  1. 위 코드를 복사해서 실행해보세요
  2. 테마 전환 버튼을 클릭해보세요
  3. Header, Main, Footer의 색상이 모두 바뀌는지 확인하세요
  4. Props를 전달하지 않았는데도 작동하는 것을 확인하세요!

실습 2: 사용자 인증 상태 관리

로그인/로그아웃 상태를 전역으로 관리해봅시다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import React, { createContext, useContext, useState } from 'react';

// 1. Context 생성
const AuthContext = createContext();

// 2. Provider
function AuthProvider({ children }) {
  const [user, setUser] = useState(null); // null = 로그아웃 상태

  const login = (username, password) => {
    // 실제로는 서버에 요청을 보내야 하지만, 여기서는 간단히 구현
    if (username && password) {
      setUser({
        id: 1,
        name: username,
        email: `${username}@example.com`
      });
      return true;
    }
    return false;
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. useAuth Hook
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth는 AuthProvider 안에서만 사용 가능합니다!');
  }
  return context;
}

// 컴포넌트들
function LoginForm() {
  const { login } = useAuth();
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    const success = login(username, password);
    if (success) {
      alert('로그인 성공!');
    } else {
      alert('아이디와 비밀번호를 입력해주세요');
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ padding: '20px' }}>
      <h2>로그인</h2>
      <div style={{ marginBottom: '10px' }}>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="아이디"
          style={{ padding: '10px', width: '200px', fontSize: '16px' }}
        />
      </div>
      <div style={{ marginBottom: '10px' }}>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="비밀번호"
          style={{ padding: '10px', width: '200px', fontSize: '16px' }}
        />
      </div>
      <button
        type="submit"
        style={{
          padding: '10px 30px',
          fontSize: '16px',
          backgroundColor: '#4caf50',
          color: 'white',
          border: 'none',
          borderRadius: '5px',
          cursor: 'pointer'
        }}
      >
        로그인
      </button>
    </form>
  );
}

function UserProfile() {
  const { user, logout } = useAuth();

  return (
    <div style={{ padding: '20px' }}>
      <h2>사용자 프로필</h2>
      <div
        style={{
          padding: '20px',
          backgroundColor: '#e3f2fd',
          borderRadius: '8px',
          marginBottom: '10px'
        }}
      >
        <p><strong>이름:</strong> {user.name}</p>
        <p><strong>이메일:</strong> {user.email}</p>
      </div>
      <button
        onClick={logout}
        style={{
          padding: '10px 30px',
          fontSize: '16px',
          backgroundColor: '#f44336',
          color: 'white',
          border: 'none',
          borderRadius: '5px',
          cursor: 'pointer'
        }}
      >
        로그아웃
      </button>
    </div>
  );
}

function App() {
  const { user } = useAuth();

  return (
    <div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>🔐 인증 데모</h1>

      {user ? (
        <UserProfile />
      ) : (
        <LoginForm />
      )}

      <div
        style={{
          marginTop: '20px',
          padding: '15px',
          backgroundColor: '#f5f5f5',
          borderRadius: '5px'
        }}
      >
        <p><strong>현재 상태:</strong> {user ? '로그인됨' : '로그아웃됨'}</p>
      </div>
    </div>
  );
}

function AuthApp() {
  return (
    <AuthProvider>
      <App />
    </AuthProvider>
  );
}

export default AuthApp;

해보기:

  1. 아이디와 비밀번호를 입력하고 로그인해보세요 (아무 값이나 입력하면 돼요)
  2. 로그인 후 프로필이 표시되는지 확인하세요
  3. 로그아웃 버튼을 클릭해보세요
  4. 여러 컴포넌트에서 useAuth()로 user 정보에 접근할 수 있어요!

실습 3: 장바구니 (Shopping Cart)

실전에서 많이 사용하는 패턴이에요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import React, { createContext, useContext, useState } from 'react';

// Context 생성
const CartContext = createContext();

// Provider
function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  const addToCart = (product) => {
    setItems(prev => {
      // 이미 있는 상품이면 수량만 증가
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      // 새 상품이면 추가
      return [...prev, { ...product, quantity: 1 }];
    });
  };

  const removeFromCart = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId));
  };

  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    setItems(prev =>
      prev.map(item =>
        item.id === productId ? { ...item, quantity } : item
      )
    );
  };

  const getTotalPrice = () => {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  };

  const getTotalItems = () => {
    return items.reduce((sum, item) => sum + item.quantity, 0);
  };

  return (
    <CartContext.Provider
      value={{
        items,
        addToCart,
        removeFromCart,
        updateQuantity,
        getTotalPrice,
        getTotalItems
      }}
    >
      {children}
    </CartContext.Provider>
  );
}

// useCart Hook
function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart는 CartProvider 안에서만 사용 가능합니다!');
  }
  return context;
}

// 컴포넌트들
function ProductList() {
  const { addToCart } = useCart();

  const products = [
    { id: 1, name: 'React 책', price: 35000 },
    { id: 2, name: 'JavaScript 책', price: 30000 },
    { id: 3, name: '키보드', price: 120000 },
    { id: 4, name: '마우스', price: 50000 }
  ];

  return (
    <div>
      <h2>📚 상품 목록</h2>
      <div style={{ display: 'grid', gap: '15px' }}>
        {products.map(product => (
          <div
            key={product.id}
            style={{
              padding: '15px',
              border: '2px solid #ddd',
              borderRadius: '8px',
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center'
            }}
          >
            <div>
              <h3 style={{ margin: '0 0 5px 0' }}>{product.name}</h3>
              <p style={{ margin: 0, color: '#666' }}>
                {product.price.toLocaleString()}</p>
            </div>
            <button
              onClick={() => addToCart(product)}
              style={{
                padding: '8px 16px',
                backgroundColor: '#4caf50',
                color: 'white',
                border: 'none',
                borderRadius: '5px',
                cursor: 'pointer'
              }}
            >
              장바구니 담기
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

function CartSidebar() {
  const { items, removeFromCart, updateQuantity, getTotalPrice, getTotalItems } = useCart();

  if (items.length === 0) {
    return (
      <div
        style={{
          padding: '20px',
          backgroundColor: '#f5f5f5',
          borderRadius: '8px',
          textAlign: 'center'
        }}
      >
        <p>🛒 장바구니가 비어있습니다</p>
      </div>
    );
  }

  return (
    <div>
      <h2>🛒 장바구니 ({getTotalItems()}개)</h2>

      {items.map(item => (
        <div
          key={item.id}
          style={{
            padding: '15px',
            marginBottom: '10px',
            backgroundColor: '#fff',
            border: '1px solid #ddd',
            borderRadius: '8px'
          }}
        >
          <h4 style={{ margin: '0 0 10px 0' }}>{item.name}</h4>
          <p style={{ margin: '0 0 10px 0', color: '#666' }}>
            {item.price.toLocaleString()}</p>

          <div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
            <button
              onClick={() => updateQuantity(item.id, item.quantity - 1)}
              style={{
                padding: '5px 10px',
                backgroundColor: '#ff9800',
                color: 'white',
                border: 'none',
                borderRadius: '3px',
                cursor: 'pointer'
              }}
            >
              -
            </button>

            <span style={{ minWidth: '30px', textAlign: 'center' }}>
              {item.quantity}
            </span>

            <button
              onClick={() => updateQuantity(item.id, item.quantity + 1)}
              style={{
                padding: '5px 10px',
                backgroundColor: '#4caf50',
                color: 'white',
                border: 'none',
                borderRadius: '3px',
                cursor: 'pointer'
              }}
            >
              +
            </button>

            <button
              onClick={() => removeFromCart(item.id)}
              style={{
                padding: '5px 10px',
                marginLeft: 'auto',
                backgroundColor: '#f44336',
                color: 'white',
                border: 'none',
                borderRadius: '3px',
                cursor: 'pointer'
              }}
            >
              삭제
            </button>
          </div>

          <p style={{ margin: '10px 0 0 0', fontWeight: 'bold' }}>
            소계: {(item.price * item.quantity).toLocaleString()}</p>
        </div>
      ))}

      <div
        style={{
          padding: '20px',
          marginTop: '20px',
          backgroundColor: '#e3f2fd',
          borderRadius: '8px',
          textAlign: 'center'
        }}
      >
        <h3 style={{ margin: '0 0 10px 0' }}>총 금액</h3>
        <p style={{ fontSize: '24px', fontWeight: 'bold', margin: 0, color: '#2196f3' }}>
          {getTotalPrice().toLocaleString()}</p>
      </div>
    </div>
  );
}

function ShopApp() {
  return (
    <div style={{ padding: '40px', maxWidth: '1200px', margin: '0 auto' }}>
      <h1>🛍️ 온라인 쇼핑몰</h1>

      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '40px' }}>
        <ProductList />
        <CartSidebar />
      </div>
    </div>
  );
}

function App() {
  return (
    <CartProvider>
      <ShopApp />
    </CartProvider>
  );
}

export default App;

해보기:

  1. 상품을 장바구니에 담아보세요
  2. 수량을 증가/감소시켜보세요
  3. 상품을 삭제해보세요
  4. 총 금액이 자동으로 계산되는지 확인하세요

Context API 사용 패턴 정리

언제 Context를 사용할까요?

사용하기 좋은 경우:

  • 테마 (다크모드/라이트모드)
  • 사용자 인증 정보 (로그인 상태)
  • 언어 설정 (다국어 지원)
  • 장바구니 상태
  • 전역 알림/토스트 메시지

피해야 할 경우:

  • 자주 변경되는 데이터 (성능 문제)
  • 부모→자식 1단계 전달 (그냥 Props 쓰세요!)
  • 너무 많은 데이터를 하나의 Context에 넣기

Context 만들 때 체크리스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ✅ 좋은 패턴
import { createContext, useContext, useState } from 'react';

// 1. Context 생성
const MyContext = createContext();

// 2. Provider 컴포넌트
export function MyProvider({ children }) {
  const [state, setState] = useState(initialValue);

  // 필요한 함수들
  const someFunction = () => {
    // ...
  };

  // 제공할 값
  const value = {
    state,
    setState,
    someFunction
  };

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}

// 3. custom hook
export function useMyContext() {
  const context = useContext(MyContext);

  if (!context) {
    throw new Error('useMyContext는 MyProvider 안에서만 사용 가능!');
  }

  return context;
}

흔한 실수와 해결 방법

실수 1: Provider 없이 사용

잘못된 코드:

1
2
3
4
5
6
7
8
function App() {
  return <Header />; // Provider로 감싸지 않음!
}

function Header() {
  const { theme } = useTheme(); // 에러!
  return <header>{theme}</header>;
}

⚠️ 에러: “useTheme은 ThemeProvider 안에서만 사용 가능합니다!”

올바른 코드:

1
2
3
4
5
6
7
function App() {
  return (
    <ThemeProvider>
      <Header />
    </ThemeProvider>
  );
}

실수 2: value 객체를 매번 새로 생성

성능 문제 발생:

1
2
3
4
5
6
7
8
9
10
function MyProvider({ children }) {
  const [state, setState] = useState(0);

  return (
    // 렌더링될 때마다 새 객체 생성 → 모든 consumer 리렌더링!
    <MyContext.Provider value=>
      {children}
    </MyContext.Provider>
  );
}

개선된 코드:

1
2
3
4
5
6
7
8
9
10
11
12
function MyProvider({ children }) {
  const [state, setState] = useState(0);

  // value를 미리 만들어두기
  const value = { state, setState };

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}

또는 useMemo 사용:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useMemo } from 'react';

function MyProvider({ children }) {
  const [state, setState] = useState(0);

  const value = useMemo(
    () => ({ state, setState }),
    [state] // state가 바뀔 때만 새 객체 생성
  );

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}

실수 3: Context를 남용

모든 것을 Context로:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 나쁜 예: 너무 많은 Context
<ThemeProvider>
  <AuthProvider>
    <UserProvider>
      <CartProvider>
        <NotificationProvider>
          <ModalProvider>
            <ToastProvider>
              <App />
            </ToastProvider>
          </ModalProvider>
        </NotificationProvider>
      </CartProvider>
    </UserProvider>
  </AuthProvider>
</ThemeProvider>

꼭 필요한 것만 Context로:

1
2
3
4
5
6
// 좋은 예: 필요한 것만
<ThemeProvider>
  <AuthProvider>
    <App />
  </AuthProvider>
</ThemeProvider>

나머지는 Props나 상태 관리 라이브러리(Zustand, Redux) 사용 고려!


정리

오늘은 Context API로 Props Drilling 문제를 해결하는 방법을 배웠어요!

핵심 포인트 ✅

Context API 3단계:

  1. createContext() - Context 만들기
  2. Provider - 데이터 방송하기
  3. useContext() - 데이터 수신하기

사용 패턴:

  • 테마, 인증, 언어 설정 등 전역 상태에 적합
  • Props Drilling을 피할 수 있어요
  • 필요한 컴포넌트에서만 데이터를 가져와요

주의사항:

  • Provider로 감싸야 해요
  • value 객체를 매번 새로 만들지 마세요 (성능 문제)
  • 모든 것을 Context로 관리하지 마세요

오늘 배운 내용 체크 ✅

  • Props Drilling 문제를 이해했나요?
  • createContext로 Context를 만들 수 있나요?
  • Provider로 데이터를 제공할 수 있나요?
  • useContext로 데이터를 가져올 수 있나요?
  • 테마 전환을 직접 구현해봤나요?
  • 인증 상태를 관리해봤나요?

숙제 📚

  1. 알림 시스템 만들기: 전역 알림 메시지 관리
  2. 언어 전환 기능: 한국어/영어 전환
  3. 즐겨찾기 기능: 상품 즐겨찾기 상태 관리

다음 단계

다음 포스트에서는 Custom Hooks를 배워봅니다! 반복되는 로직을 재사용 가능한 Hook으로 만드는 방법을 알아봐요.

“늦었다고 생각할 때가 가장 빠를 때입니다. Context API 마스터! 🎉”


React 완벽 가이드 시리즈

  1. React란 무엇인가?
  2. 첫 React 앱 만들기
  3. useState - 클릭하면 바뀌는 화면
  4. Props - 컴포넌트끼리 대화하기
  5. useEffect - 컴포넌트 생명주기
  6. 조건부/리스트 렌더링
  7. Context API - Props 지옥에서 탈출하기 ← 현재 글
  8. Custom Hooks 만들기
  9. React Router 완벽 가이드
  10. 폼 처리와 유효성 검증
  11. React 성능 최적화
  12. 에러 바운더리와 Suspense
  13. 상태 관리 라이브러리
  14. 서버 상태 관리
  15. React와 TypeScript
  16. Next.js 입문
  17. React 테스팅 전략
  18. 고급 패턴과 베스트 프랙티스
  19. 실전 프로젝트
  20. React 배포와 DevOps

관련 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.