React 입문 11편 - Hooks 정리

처음 React 배울 때 클래스 컴포넌트만 쓰다가 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 | 복잡한 상태 | 상태 로직이 복잡할 때 |
운영자 실전 노트
실제 프로젝트 진행하며 겪은 문제
- useEffect 의존성 배열 누락으로 버그 발생 → ESLint 규칙 활성화 필수
- 클린업 함수 미작성으로 메모리 누수 → 타이머, 이벤트 리스너는 반드시 정리
이 경험을 통해 알게 된 점
- useMemo/useCallback 과도한 사용은 조기 최적화다
- 커스텀 Hook으로 로직 재사용하면 코드 품질이 크게 향상된다
마무리
이 시리즈로 React의 기초를 모두 다뤘다. 여기까지 따라왔으면 React로 간단한 앱은 만들 수 있다. 다음 단계로는 React Router, 상태 관리 라이브러리(Redux, Zustand), 서버 통신(React Query) 등을 공부해보자. 실제로 뭔가 만들어보는 게 가장 빨리 배우는 방법이다. 할 일 앱, 날씨 앱, 블로그 등 작은 프로젝트부터 시작해보자.
React 시리즈 탐색:
← 블로그 목록으로