🔑한번 보고 모르겠으면, 두번 보고, 세번 보고, 누가 이기나 해보자
💡 useState로 Counter 구현하기
📃 useState를 사용하여 간단한 Counter 기능을 구현한 후, 이를 기반으로 useReducer와의 차이점을 이해해 보겠습니다
💻useState를 활용한 Counter 구현
import React, { useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0);
const changeCount = e => setCount(count + Number(e.target.innerText));
return (
<>
<div>현재 카운터 값은 <b>{count}</b> 입니다</div>
<div>
<button onClick={changeCount}>+1</button>
<button onClick={changeCount}>-1</button>
</div>
</>
)
}
const App = () => {
return (
<>
<Counter />
</>
)
}
export default App
❗ 현재 구현은 버튼의 텍스트가 반드시 숫자여야 하는 제한사항이 있습니다. 문자열 형태의 버튼 텍스트를 사용할 경우 정상적인 동작을 보장할 수 없으므로, 아래와 같이 개선된 버전을 구현해보겠습니다
💻문자열 텍스트 대응 (useState로 함수 분리)
import React, { useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0);
const countPlus = () => setCount(count + 1);
const countMinus = () => setCount(count - 1);
return (
<>
<div>현재 카운터 값은 <b>{count}</b> 입니다</div>
<div>
<button onClick={countPlus}>하나 더하기</button>
<button onClick={countMinus}>히나 빼기</button>
</div>
</>
)
}
const App = () => {
return (
<>
<Counter />
</>
)
}
export default App
⬆️ 각각의 동작을 독립된 함수로 분리함으로써, 버튼 텍스트와 상관없이 안정적인 동작을 보장할 수 있습니다. 하지만 나누기, 곱하기, 세제곱 등등 기능이 추가되면 컴포넌트 내에 함수도 계속해서 정의해줘야할 것 입니다
💡 useReducer로 Counter 구현하기
📃 useReducer는 복잡한 상태 로직을 관리하기 위한 useState의 대안입니다. 상태 업데이트 로직을 컴포넌트에서 분리하여 관리할 수 있으며, 여러 상태 전환을 체계적으로 관리할 수 있습니다
💻CODE
import React, { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<div>현재 카운터 값은 <b>{state.count}</b> 입니다</div>
<div>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>하나 더하기</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>히나 빼기</button>
</div>
</>
)
}
const App = () => {
return (
<>
<Counter />
</>
)
}
export default App
⬆️ 사실 단순한 카운터의 경우 useState로도 충분하나, 상태 관리 로직이 복잡해질 경우 useReducer를 활용하면 더욱 체계적인 상태 관리가 가능합니다. 또한 reducer 함수를 별도로 정의함으로써 로직의 재사용성이 향상됩니다
💡 useMemo 란
📃 useMemo는 메모이제이션을 통해 성능을 최적화하는 Hook입니다. 특정 값이 변경될 때만 계산을 수행하도록 하여 불필요한 연산을 방지합니다. 이렇게만 설명하면, 무조건 사용하는 것이 좋지 않을까? 라고 생각할 수 있지만, 결국 Memo, useCallback 누군가가 정의해놓은 함수라는 것 입니다. 즉 메모리를 사용하고 비교 연산이 있다는 것이죠. 그래서 무분별하게 Memo와 Callback을 사용하는 것은 오히려 성능을 저하시킵니다
💻useMemo 훅 사용
import React, { useState, useMemo } from 'react';
const ExpensiveCalculation = ({ number }) => {
const calc = useMemo(() => {
console.log('복잡한 계산 수행...');
return number * 2;
}, [number]);
return <div>계산 결과: {calc}</div>;
};
const App = () => {
const [number, setNumber] = useState(0);
return (
<>
<button onClick={() => setNumber(number + 1)}>증가</button>
<ExpensiveCalculation number={number} />
</>
);
};
export default App;
⬆️ useMemo를 통해 의존성 배열의 값이 변경될 때만 계산이 실행되어 성능을 최적화할 수 있습니다. 하지만 핵심은 위와 같은 간단 연산은 useMemo를 사용하는 것이 오히려 성능 저하를 불러일으킵니다
💡 useCallback 이란
📃 useCallback은 컴포넌트의 성능을 최적화하기 위한 Hook으로, 메모이제이션된 콜백 함수를 반환합니다. 특히 자식 컴포넌트에 함수를 props로 전달할 때 불필요한 렌더링을 방지하는데 효과적입니다
💻useCallback 사용 사례
import React, { useState, useCallback } from 'react';
const Child = React.memo(({ onClick }) => {
console.log('Child 컴포넌트 렌더링');
return <button onClick={onClick}>자식 컴포넌트 버튼</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// useCallback을 사용하지 않은 경우
// const increment = () => setCount(count + 1);
// useCallback을 사용하여 함수 메모이제이션
const increment = useCallback(() => setCount((prev) => prev + 1), []);
return (
<div>
<div>
<button onClick={increment}>카운트 증가</button>
<p>현재 카운트: {count}</p>
</div>
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="텍스트 입력"
/>
</div>
{/* 자식 컴포넌트에 메모이제이션된 콜백 전달 */}
<Child onClick={increment} />
</div>
);
};
export default App;
⬆️ 적용 전
- increment 함수가 App 컴포넌트가 리렌더링될 때마다 새로운 함수 인스턴스로 생성됩니다.
- 이는 Child 컴포넌트가 불필요하게 리렌더링되는 원인이 됩니다.
⬆️ 적용 후
- increment 함수는 메모이제이션되어 동일한 함수 인스턴스를 재사용합니다.
- 따라서 Child 컴포넌트는 리렌더링되지 않습니다.
💡 useContext 란
📃 useContext는 React 컴포넌트 트리 전체에 데이터를 공유할 수 있게 해주는 Hook입니다. Context API를 통해 prop drilling을 방지하고, 전역 상태를 효율적으로 관리할 수 있습니다.
💻useContext 사용 사례 (Dark 모드)
import React, { createContext, useContext, useState } from 'react';
// 1. Context 생성
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
const toggleTheme = () => setIsDarkMode(!isDarkMode);
return (
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemeToggleButton = () => {
const { isDarkMode, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
{isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
</button>
);
};
const AppContent = () => {
const { isDarkMode } = useContext(ThemeContext);
return (
<div
style={{
backgroundColor: isDarkMode ? '#333' : '#FFF',
color: isDarkMode ? '#FFF' : '#000',
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
<h1>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</h1>
<ThemeToggleButton />
</div>
);
};
const App = () => {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
};
export default App;
⬆️ useContext를 활용하면 컴포넌트 계층 구조의 모든 레벨에서 데이터에 직접 접근할 수 있어 props 전달 과정을 단순화할 수 있습니다. 이는 특히 테마, 인증 상태, 언어 설정과 같은 전역적으로 관리되어야 하는 상태에 매우 효과적입니다.