React Hooks는 React 16.8에서 도입된 기능으로, 함수형 컴포넌트에서도 상태 관리와 생명주기 기능을 사용할 수 있게 해줍니다. 이 글에서는 React Hooks의 기본 개념부터 고급 활용법까지 자세히 다루겠습니다.
React Hooks를 사용하는 이유
기존 클래스 컴포넌트에서는 생명주기 메서드가 복잡하고, 관련 로직이 여러 메서드에 분산되는 문제가 있었습니다. Hooks를 사용하면 이러한 문제를 해결할 수 있습니다.
Hooks의 장점
- 간결한 코드: 클래스 문법 없이 상태 관리 가능
- 로직 재사용: 커스텀 훅으로 로직 추출 및 공유
- 관심사 분리: 관련 코드를 한 곳에 모아 관리
- 테스트 용이성: 순수 함수 형태로 테스트하기 쉬움
기본 Hooks
useState - 상태 관리
useState는 컴포넌트에 상태를 추가하는 가장 기본적인 Hook입니다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(prev => prev - 1)}>
Decrement
</button>
</div>
);
}
이전 상태를 기반으로 새 상태를 계산할 때는 함수형 업데이트를 사용하세요.
setCount(prev => prev + 1) 형태가 더 안전합니다.객체와 배열 상태 관리
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
</form>
);
}
useEffect - 부수 효과 처리
useEffect는 컴포넌트가 렌더링된 후 실행되는 부수 효과를 처리합니다.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
useEffect 의존성 배열
useEffect(() => {
}, []);
useEffect(() => {
}, [dep1, dep2]);
useEffect(() => {
});
의존성 배열에 필요한 값을 빠뜨리면 stale closure 문제가 발생할 수 있습니다. ESLint의
exhaustive-deps 규칙을 활성화하세요.Cleanup 함수
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>Seconds: {seconds}</div>;
}
useContext - 전역 상태 접근
useContext를 사용하면 Props drilling 없이 데이터를 전달할 수 있습니다.
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}
상태 흐름 이해하기
성능 최적화 Hooks
useMemo - 값 메모이제이션
계산 비용이 높은 연산의 결과를 캐싱합니다.
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Filtering items...');
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 - 함수 메모이제이션
함수의 참조 동일성을 유지하여 불필요한 리렌더링을 방지합니다.
import { useCallback, useState, memo } from 'react';
const Button = memo(({ onClick, children }) => {
console.log(`Button "${children}" rendered`);
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<p>Count: {count}</p>
<Button onClick={handleClick}>Increment</Button>
</div>
);
}
useMemo와 useCallback은 성능 최적화를 위한 도구입니다. 모든 곳에 사용하면 오히려 메모리 사용량이 증가할 수 있으니, 실제로 성능 문제가 있는 경우에만 적용하세요.useReducer - 복잡한 상태 관리
Redux와 유사한 패턴으로 복잡한 상태 로직을 관리합니다.
import { useReducer } from 'react';
const initialState = {
items: [],
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
default:
return state;
}
}
function TodoList() {
const [state, dispatch] = useReducer(reducer, initialState);
const addTodo = (text) => {
dispatch({
type: 'ADD_ITEM',
payload: { id: Date.now(), text, completed: false }
});
};
return (
<div>
{state.loading && <p>Loading...</p>}
{state.error && <p>Error: {state.error}</p>}
<ul>
{state.items.map(item => (
<li key={item.id}>
{item.text}
<button
onClick={() => dispatch({
type: 'REMOVE_ITEM',
payload: item.id
})}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
커스텀 Hooks
로직을 재사용 가능한 함수로 추출할 수 있습니다.
useFetch 커스텀 훅
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useLocalStorage 커스텀 훅
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<input
type="range"
min="12"
max="24"
value={fontSize}
onChange={e => setFontSize(Number(e.target.value))}
/>
</div>
);
}
Hooks 규칙
React Hooks를 올바르게 사용하기 위한 두 가지 규칙이 있습니다.
최상위에서만 Hook 호출: 반복문, 조건문, 중첩 함수 내에서 Hook을 호출하지 마세요.
React 함수에서만 Hook 호출: 일반 JavaScript 함수에서 Hook을 호출하지 마세요.
function Component() {
if (condition) {
const [state, setState] = useState();
}
const [state, setState] = useState();
if (condition) {
}
}
마무리
React Hooks는 함수형 컴포넌트에서 강력한 기능을 제공합니다. 핵심 포인트를 정리하면 다음과 같습니다.
- useState로 상태 관리, useEffect로 부수 효과 처리
- useContext로 전역 상태 접근
- useMemo, useCallback으로 성능 최적화
- 커스텀 훅으로 로직 재사용
- Hooks 규칙을 반드시 준수
다음 글에서는 React의 고급 패턴과 상태 관리 라이브러리 비교에 대해 다루겠습니다.