React 앱이 커지면 props drilling과 불필요한 리렌더링이 문제가 됩니다. Context API, useReducer, 그리고 메모이제이션 훅으로 이 문제들을 해결하는 방법을 알아봅니다.
As React apps grow, props drilling and unnecessary re-renders become problems. Let's learn how to solve these issues with Context API, useReducer, and memoization hooks.
Context API: Props Drilling 해결 Context API: Solving Props Drilling
깊은 컴포넌트 트리에서 데이터를 전달할 때, 중간 컴포넌트들이 단지 props를 전달하는 역할만 하게 됩니다. Context는 이 문제를 해결합니다.
When passing data deep into a component tree, intermediate components just pass props along. Context solves this problem.
const ThemeContext = createContext('light');
// 2. Provider로 감싸기
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
// 3. 어디서든 useContext로 접근
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
현재: {theme}
</button>
);
}
const ThemeContext = createContext('light');
// 2. Wrap with Provider
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
// 3. Access anywhere with useContext
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Current: {theme}
</button>
);
}
useReducer: 복잡한 상태 관리 useReducer: Complex State Management
useState로 관리하기 어려운 복잡한 상태 로직에는 useReducer가 적합합니다. Redux와 유사한 패턴으로 상태 변화를 예측 가능하게 만듭니다.
For complex state logic that's hard to manage with useState, useReducer is suitable. It makes state changes predictable with a Redux-like pattern.
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
// 컴포넌트에서 사용
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = (text) => {
dispatch({ type: 'ADD', text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE', id });
};
// ... render
}
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
// Use in component
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = (text) => {
dispatch({ type: 'ADD', text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE', id });
};
// ... render
}
useMemo: 값의 메모이제이션 useMemo: Value Memoization
비용이 큰 계산의 결과를 캐싱하여 불필요한 재계산을 방지합니다.
Cache the results of expensive calculations to prevent unnecessary recalculations.
// filter가 바뀔 때만 재계산
const filteredItems = useMemo(() => {
console.log('필터링 실행');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Recalculate only when filter changes
const filteredItems = useMemo(() => {
console.log('Filtering executed');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
useCallback: 함수의 메모이제이션 useCallback: Function Memoization
자식 컴포넌트에 전달되는 콜백 함수의 불필요한 재생성을 방지합니다. React.memo와 함께 사용하면 효과적입니다.
Prevent unnecessary recreation of callback functions passed to child components. Effective when used with React.memo.
const ExpensiveChild = React.memo(({ onClick, data }) => {
console.log('Child 렌더링');
return <button onClick={onClick}>{data}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// count가 바뀔 때만 함수 재생성
const handleClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
{/* text가 바뀌어도 Child는 리렌더링 안 됨 */}
<ExpensiveChild onClick={handleClick} data={count} />
</>
);
}
const ExpensiveChild = React.memo(({ onClick, data }) => {
console.log('Child rendered');
return <button onClick={onClick}>{data}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Recreate function only when count changes
const handleClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
{/* Child won't re-render when text changes */}
<ExpensiveChild onClick={handleClick} data={count} />
</>
);
}
⚠️ 과도한 최적화 주의 ⚠️ Avoid Over-Optimization
useMemo와 useCallback은 비용이 있습니다. 모든 곳에 사용하지 말고, 실제 성능 문제가 측정된 경우에만 적용하세요.
useMemo and useCallback have costs. Don't use them everywhere—only apply when actual performance issues are measured.
Context + useReducer 조합 Context + useReducer Combination
전역 상태 관리가 필요할 때, Context와 useReducer를 조합하면 Redux 없이도 강력한 상태 관리가 가능합니다.
When global state management is needed, combining Context and useReducer enables powerful state management without Redux.
const StateContext = createContext();
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// 커스텀 훅으로 사용하기 편하게
function useAppState() {
return useContext(StateContext);
}
function useAppDispatch() {
return useContext(DispatchContext);
}
const StateContext = createContext();
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// Custom hooks for easy use
function useAppState() {
return useContext(StateContext);
}
function useAppDispatch() {
return useContext(DispatchContext);
}
💡 왜 Context를 분리하는가? 💡 Why separate Contexts?
state와 dispatch를 별도 Context로 분리하면, dispatch만 사용하는 컴포넌트가 state 변경 시 불필요하게 리렌더링되는 것을 방지합니다.
Separating state and dispatch into different Contexts prevents components that only use dispatch from re-rendering unnecessarily when state changes.
Error Boundary: 에러 처리 Error Boundary: Error Handling
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('에러 발생:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>문제가 발생했습니다.</h1>;
}
return this.props.children;
}
}
// 사용
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error occurred:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
