상태를 다루고 외부 효과를 처리하는 핵심 훅.
useState 와 useEffect
함수 컴포넌트에 "기억"과 "부수효과"를 더하는 두 훅을 더 깊이 다뤄 봅니다. 앞에서 기본기를 익혔다면, 이번에는 실수하기 쉬운 부분까지 짚어 봅시다.
학습 목표
useState의 초깃값·지연 초기화를 이해한다.useEffect와 의존성 배열의 동작을 정확히 안다.- 클린업(cleanup) 함수로 구독·타이머를 정리할 수 있다.
- stale closure(오래된 값 참조) 버그를 피할 수 있다.
useState — 상태 기억하기
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count} 번 클릭
</button>
);
}
useState(초깃값)은[현재값, 변경함수]를 반환합니다.setCount를 호출하면 컴포넌트가 다시 렌더링됩니다.
지연 초기화
초깃값을 만드는 데 비용이 큰 계산이 든다면, 값 대신 함수를 넘기세요. 첫 렌더에서 한 번만 실행됩니다.
// ❌ 매 렌더마다 expensiveInit()가 호출됨 (결과는 첫 렌더만 쓰임)
const [data, setData] = useState(expensiveInit());
// ✅ 첫 렌더에서 딱 한 번만 호출
const [data, setData] = useState(() => expensiveInit());
useEffect — 외부와 동기화
useEffect 는 렌더링 "이후"에 실행되어, 데이터 요청·구독·타이머처럼 React 바깥 세상과 동기화할 때 씁니다.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then(setUser);
}, [userId]); // userId가 바뀔 때만 다시 실행
if (!user) return <p>불러오는 중...</p>;
return <h2>{user.name}</h2>;
}
의존성 배열
useEffect(() => { /* ... */ }); // 매 렌더마다
useEffect(() => { /* ... */ }, []); // 처음 한 번만
useEffect(() => { /* ... */ }, [x]); // x가 바뀔 때마다
| 배열 | 실행 시점 |
|---|---|
| 생략 | 모든 렌더 후 |
[] | 마운트 시 한 번 |
[a, b] | a 나 b 가 바뀔 때 |
⚠️ 주의 — 의존성 배열을 잘못 비우면 오래된 값(stale closure)을 참조하는 버그가 생깁니다. effect 안에서 쓰는 값은 모두 배열에 넣으세요.
클린업 함수
effect가 함수를 반환하면 React가 그것을 "정리(cleanup)"용으로 기억합니다. 컴포넌트가 사라질 때, 그리고 effect가 다시 실행되기 직전에 호출됩니다.
function Clock() {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id); // 정리: 타이머 해제
}, []);
return <p>{new Date(now).toLocaleTimeString()}</p>;
}
정리하지 않으면 타이머·이벤트 리스너가 쌓여 메모리 누수와 중복 실행이 생깁니다. 구독 패턴도 같습니다.
useEffect(() => {
const onResize = () => console.log(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
stale closure 피하기
effect나 타이머 콜백은 만들어진 시점의 변수 값을 "기억"합니다. 의존성을 비워 두면 옛날 값에 갇히게 됩니다.
function Bad() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // ❌ count는 항상 0에 갇힘 → 1에서 멈춤
}, 1000);
return () => clearInterval(id);
}, []); // count가 빠져 있다
}
해결책은 함수형 업데이트로 최신 값을 받아오는 것입니다.
setCount((prev) => prev + 1); // ✅ 항상 최신 값 기준
💡 TIP — set 함수의 함수형 형태(
prev => ...)를 쓰면 해당 state를 의존성 배열에서 빼도 안전합니다.
요약
useState는[값, set함수]. 무거운 초깃값은 함수로 지연 초기화.useEffect는 렌더 후 실행, 의존성 배열로 실행 시점을 제어한다.- effect에서 쓰는 값은 모두 의존성 배열에 넣는다.
- 타이머·구독은 return 클린업 함수로 정리한다.
- 옛날 값에 갇히는 stale closure 는 함수형 업데이트로 피한다.
연습문제
- 마운트 시
document.title을 "환영합니다"로 바꾸고, 언마운트 시 원래대로 되돌리는 effect를 작성하세요. - 1초마다 1씩 증가하는 카운터를 만들되, stale closure에 빠지지 않게 함수형 업데이트를 쓰세요.
queryprops가 바뀔 때마다/api/search?q=로 fetch하는 컴포넌트를 만드세요.window의keydown이벤트를 구독하고 언마운트 시 해제하는 effect를 작성하세요.
힌트 — 1·4번은 effect에서 정리 함수를
return. 2번은setInterval+setCount(prev => prev + 1)+clearInterval클린업.
💡 연습문제 풀이
불러오는 중…
댓글 0
“React.js” 강좌에 대한 댓글입니다.