React 독학 가이드 11 - Hooks 완벽 가이드
React 16.8에서 Hooks가 도입되면서 함수형 컴포넌트가 완전히 달라졌어요. 클래스 없이도 상태 관리, 생명주기, 최적화까지 다 할 수 있게 됐죠. 이번 글에서 주요 Hooks를 전부 정리해볼게요.
Hooks의 규칙
먼저 Hooks 사용 규칙을 알아야 해요. 안 지키면 에러 나요!
규칙 1: 최상위에서만 호출
// ❌ 조건문 안에서 사용 금지
if (condition) {
const [state, setState] = useState(0);
}
// ❌ 반복문 안에서 사용 금지
for (let i = 0; i < 3; i++) {
const [state, setState] = useState(0);
}
// ⭕ 컴포넌트 최상위에서 호출
function Component() {
const [state, setState] = useState(0); // OK
// ...
}
규칙 2: React 함수 안에서만 호출
// ❌ 일반 함수에서 사용 금지
function normalFunction() {
const [state, setState] = useState(0);
}
// ⭕ React 컴포넌트나 커스텀 Hook에서만
function Component() {
const [state, setState] = useState(0); // OK
}
function useCustomHook() {
const [state, setState] = useState(0); // OK
}
1. useState - 상태 관리
가장 기본이 되는 Hook이에요.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(prev => prev - 1)}>감소</button>
</div>
);
}
실행 결과:
카운트: 5
useState로 상태를 관리하여 버튼 클릭 시 화면이 업데이트됩니다
다양한 타입
const [text, setText] = useState(""); // 문자열
const [num, setNum] = useState(0); // 숫자
const [isOpen, setIsOpen] = useState(false); // 불린
const [list, setList] = useState([]); // 배열
const [user, setUser] = useState({ name: "" }); // 객체
지연 초기화
초기값 계산이 무거우면 함수로 전달하세요:
// ❌ 매 렌더링마다 실행됨
const [data, setData] = useState(expensiveCalculation());
// ⭕ 첫 렌더링에만 실행됨
const [data, setData] = useState(() => expensiveCalculation());
2. useEffect - 부수 효과 처리
컴포넌트 생명주기와 관련된 작업을 해요.
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []); // 빈 배열 = 마운트 시 1번만 실행
if (loading) return <p>로딩 중...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
의존성 배열
// 마운트 + 매 렌더링마다 실행
useEffect(() => {
console.log('렌더링됨');
});
// 마운트 시 1번만 실행
useEffect(() => {
console.log('마운트됨');
}, []);
// 마운트 + count가 바뀔 때마다 실행
useEffect(() => {
console.log('count:', count);
}, [count]);
// 마운트 + a 또는 b가 바뀔 때 실행
useEffect(() => {
console.log('a 또는 b 변경');
}, [a, b]);
클린업 함수
컴포넌트가 언마운트되거나 effect가 재실행되기 전에 정리 작업:
useEffect(() => {
const timer = setInterval(() => {
console.log('틱');
}, 1000); // 클린업: 언마운트 시 또는 다음 effect 전에 실행
return () => {
clearInterval(timer);
};
}, []);
이벤트 리스너 정리
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
3. useRef - DOM 접근 & 값 저장
렌더링 없이 값을 저장하거나 DOM에 접근해요.
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 자동 포커스
}, []);
return <input ref={inputRef} />;
}
렌더링 없이 값 저장
function Timer() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(countRef.current); // 화면은 안 바뀜
};
return <button onClick={handleClick}>클릭</button>;
}
4. useMemo - 값 메모이제이션
비싼 계산 결과를 캐싱해요.
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// items가 바뀔 때만 재계산
const sortedItems = useMemo(() => {
console.log('정렬 중...'); // 이게 자주 찍히면 안 됨
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const filteredItems = sortedItems.filter(item =>
item.name.includes(filter)
);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="검색"
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
언제 useMemo를 쓸까?
- 복잡한 계산 (정렬, 필터, 집계 등)
- 렌더링마다 새 객체/배열을 만들어서 자식에게 전달할 때
- 최적화가 필요할 때 (먼저 측정하고 쓰세요!)
5. useCallback - 함수 메모이제이션
함수를 캐싱해요. 자식 컴포넌트에 함수를 전달할 때 유용해요.
import { useCallback, useState, memo } from 'react';
// memo로 감싼 자식 컴포넌트
const Button = memo(({ onClick, children }) => {
console.log('Button 렌더링:', children);
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// count가 바뀔 때만 새 함수 생성
const handleIncrease = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<Button onClick={handleIncrease}>증가</Button>
</div>
);
}
useCallback이 없으면 text가 바뀔 때마다 Button도 다시 렌더링돼요.
6. useContext - 전역 상태 공유
Props 드릴링 없이 데이터를 공유해요.
import { createContext, useContext, useState } from 'react';
// 1. Context 생성
const ThemeContext = createContext();
// 2. Provider 컴포넌트
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>
);
}
// 3. 사용하는 컴포넌트
function ThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
현재 테마: {theme}
</button>
);
}
// 4. App에서 사용
function App() {
return (
<ThemeProvider>
<div>
<h1>테마 예제</h1>
<ThemeButton />
</div>
</ThemeProvider>
);
}
7. useReducer - 복잡한 상태 관리
useState보다 복잡한 상태 로직에 사용해요.
import { useReducer } from 'react';
// 리듀서 함수
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
case 'SET':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>리셋</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>
100으로 설정
</button>
</div>
);
}
실행 결과:
Count: 42
useReducer로 복잡한 상태 로직을 action 기반으로 관리합니다
실전 예제: Todo 앱
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 [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch({ type: 'ADD', text });
setText('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">추가</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
삭제
</button>
</li>
))}
</ul>
</div>
);
}
8. 커스텀 Hook 만들기
반복되는 로직을 재사용 가능한 Hook으로 만들어요.
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 사용
function App() {
const [name, setName] = useLocalStorage('name', '');
return <input value={name} onChange={e => setName(e.target.value)} />;
}
useWindowSize
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 사용
function App() {
const { width, height } = useWindowSize();
return <p>화면 크기: {width} x {height}</p>;
}
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 사용
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러 발생!</p>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
Hooks 요약 표
| Hook | 용도 | 언제 쓰나? |
|---|---|---|
| useState | 상태 관리 | 값이 바뀌면 화면 업데이트 필요할 때 |
| useEffect | 부수 효과 | API 호출, 이벤트 리스너, 타이머 |
| useRef | DOM 접근, 값 저장 | 렌더링 없이 값 유지할 때 |
| useMemo | 값 캐싱 | 비싼 계산 최적화 |
| useCallback | 함수 캐싱 | 자식에게 함수 전달 최적화 |
| useContext | 전역 상태 | Props 드릴링 없이 데이터 공유 |
| useReducer | 복잡한 상태 | 상태 로직이 복잡할 때 |
마무리
이 시리즈로 React의 기초를 모두 다뤘어요!
- React 소개
- 프로젝트 생성
- 컴포넌트
- JSX
- Props
- 이벤트 처리
- State
- State와 Props
- 사용자 입력
- useRef
- Hooks 총정리
여기까지 따라왔으면 React로 간단한 앱은 만들 수 있을 거예요. 다음 단계로는 React Router, 상태 관리 라이브러리(Redux, Zustand), 서버 통신(React Query) 등을 공부해보세요.
실제로 뭔가 만들어보는 게 가장 빨리 배우는 방법이에요. 할 일 앱, 날씨 앱, 블로그 등 작은 프로젝트부터 시작해보세요!
React 시리즈 탐색:
← 블로그 목록으로