app.jsx
import { AppProvider, useApp } from './context/AppContext';
import Toolbar from './components/Toolbar';
import ContentPanel from './components/ContentPanel';
/**
* Context 방식: 상태는 Provider 안에 있고,
* 자식은 useApp()으로 접근합니다 (props는 최소화).
*/
function AppShell() {
const { theme } = useApp();
return (
<div className={`app ${theme}`}>
<header>
<h1>useContext 예제</h1>
<span className="badge">상태는 AppContext · useApp()</span>
</header>
<Toolbar />
<ContentPanel />
</div>
);
}
export default function App() {
return (
<AppProvider>
<AppShell />
</AppProvider>
);
}
components/ContnetPanel.jax
import { useApp } from '../context/AppContext';
/**
* ContentPanel — 역시 useApp()으로 theme, count를 읽습니다.
*/
function ContentPanel() {
const { theme, count } = useApp();
return (
<section className="panel">
<p className="note">
<strong>ContentPanel</strong>도 <code>useApp()</code>으로 동일한 값을 구독합니다.
</p>
<p>현재 테마 문자열: <code>{theme}</code></p>
<p className="counter">{count}</p>
</section>
);
}
export default ContentPanel;
components/Toobar.jsx
import { useApp } from '../context/AppContext';
/**
* Toolbar — props 없이 useApp()으로 theme, count, 액션을 가져옵니다.
*/
function Toolbar() {
const { theme, count, toggleTheme, increment } = useApp();
return (
<section className="panel">
<p className="note">
<strong>Toolbar</strong>는 props 없이 <code>useApp()</code>만 사용합니다.
</p>
<button type="button" className="btn-toggle" onClick={toggleTheme}>
테마: {theme === 'light' ? '라이트' : '다크'} (전환)
</button>
<button type="button" className="btn-toggle" onClick={increment} style={{ marginLeft: '0.5rem' }}>
카운트 +1 (현재 {count})
</button>
</section>
);
}
export default Toolbar;
context/AppContext.jsx
import { createContext, useContext, useState, useMemo } from 'react';
const AppContext = createContext(null);
/**
* App 전역 상태(theme, count)를 Context로 제공합니다.
* 깊은 트리에서도 props drilling 없이 useApp()으로 접근합니다.
*/
export function AppProvider({ children }) {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
const toggleTheme = () => {
setTheme((t) => (t === 'light' ? 'dark' : 'light'));
};
const increment = () => setCount((c) => c + 1);
const value = useMemo(
() => ({
theme,
count,
toggleTheme,
increment,
}),
[theme, count]
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
export function useApp() {
const ctx = useContext(AppContext);
if (!ctx) {
throw new Error('useApp은 AppProvider 안에서만 사용하세요.');
}
return ctx;
}
index.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.app {
min-height: 100vh;
padding: 1.5rem;
transition: background 0.2s, color 0.2s;
}
.app.light {
background: #f8fafc;
color: #0f172a;
}
.app.dark {
background: #0f172a;
color: #f1f5f9;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
}
header h1 {
margin: 0;
font-size: 1.25rem;
}
.badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 6px;
background: rgba(16, 185, 129, 0.2);
color: #059669;
}
.app.dark .badge {
background: rgba(52, 211, 153, 0.15);
color: #6ee7b7;
}
.panel {
border-radius: 12px;
padding: 1.25rem;
margin-bottom: 1rem;
border: 1px solid rgba(148, 163, 184, 0.4);
}
.app.dark .panel {
border-color: rgba(148, 163, 184, 0.25);
}
button {
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 8px;
border: none;
font-weight: 600;
}
.btn-toggle {
background: #059669;
color: white;
}
.btn-toggle:hover {
filter: brightness(1.05);
}
.counter {
font-size: 2rem;
font-weight: 700;
margin: 0.5rem 0;
}
.note {
font-size: 0.85rem;
opacity: 0.85;
line-height: 1.5;
}
'FrontEnd > React' 카테고리의 다른 글
| React + TypeScript 시작 가이드 (Vite) (0) | 2026.03.29 |
|---|---|
| props · useContex 비교 테마 바꾸기 (0) | 2026.03.28 |
| props - 테마바꾸기 (0) | 2026.03.28 |
| 24차시. 프로젝트 정리 (0) | 2026.03.24 |
| 23차시. 빌드 & 배포 (0) | 2026.03.23 |