useEffect는 컴포넌트가 React 바깥의 시스템(DOM 이벤트, 네트워크, 타이머, 외부 라이브러리 등)과 동기화할 때 쓰는 훅입니다. 본 글에서는 전체 API를 모두 다루지 않고, 실무에서 가장 자주 마주치는 네 가지 축—언제 실행되는가, 의존성 배열을 어떻게 적는가, 클린업은 언제/왜 필요한가, 무엇을 주의해야 하는가—만 정확히 정리합니다.
useEffect는 화면이 실제로 바뀐 다음에 실행됩니다.
조금 더 풀어서 말하면, React는 먼저 화면을 어떻게 바꿀지 계산(렌더)하고, 그 결과를 실제 DOM에 반영(커밋)합니다. 사용자가 바뀐 화면을 볼 수 있는 상태가 된 뒤에야 useEffect 안의 코드가 실행됩니다.
또한 useEffect는 값이 바뀔 때마다 이전 동기화를 멈추고(정리/클린업) 새 값으로 다시 시작합니다. 즉, “시작 → 중지 → 다시 시작”의 흐름으로 동작합니다.
정리하면 다음과 같습니다.
useEffect가 실행됩니다. 간단 예시:
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 이 로그는 화면에 count 값이 반영된 "뒤에" 찍힙니다.
console.log("useEffect 실행 (커밋 이후):", count);
// 다음 렌더에서 count가 바뀌면 먼저 이곳이 정리(cleanup)된 뒤,
// 새 count 값으로 다시 위의 코드가 실행됩니다.
return () => {
console.log("이전 효과 정리:", count);
};
}, [count]);
return (
<button onClick={() => setCount((c) => c + 1)}>
{count}
</button>
);
}
위 코드를 클릭해 count가 증가하면, 먼저 버튼의 숫자가 화면에서 바뀌고(커밋), 그 다음에 useEffect가 실행됩니다. 이후 다시 눌러 값이 바뀌면, 이전 효과를 정리한 뒤 새 값으로 효과가 재실행됩니다.
의존성 배열은 이 효과가 어떤 값에 ‘반응’할지를 선언합니다.
useEffect(fn) — 의존성 생략: 매 렌더 후 실행 useEffect(fn, []) — 빈 배열: 초기 1회 실행 + 언마운트 시 클린업 useEffect(fn, [a, b]) — 배열의 값이 바뀔 때만 실행원칙은 단순합니다. 효과 내부에서 읽는 ‘반응형 값’(프롭/상태/컨텍스트/함수/메모된 값 등)은 모두 의존성에 기입해야 합니다. 누락하면 과거 값을 잡아두는 stale closure 문제가 발생합니다. 이를 자동 점검하기 위해 eslint-plugin-react-hooks의 react-hooks/exhaustive-deps 규칙 사용을 권장합니다.
의존성이 너무 자주 바뀌어 불필요한 재실행이 생긴다면, 콜백/값을 useCallback/useMemo로 안정화해 의존성 변화를 줄이십시오(의존성 누락의 대체가 아닙니다).
간단 예시:
useEffect(() => {
// props.userId, token, onLoaded를 읽으므로 모두 deps에 명시
// 읽는 값이 바뀌면 이전 동기화를 중단하고 최신 값으로 재실행
fetchUser(props.userId, token).then(onLoaded);
}, [props.userId, token, onLoaded]);
동기화를 멈추는 방법입니다. useEffect에서 함수를 반환하면 React가 다음 시점에 그 함수를 호출합니다.
1) 다음 실행 직전(의존성이 바뀌어 재실행되기 전)
2) 컴포넌트 언마운트 시
이를 통해 이벤트 리스너 제거, 타이머 해제, 요청 취소 등 누수(leak)를 방지합니다.
이벤트 리스너 예시:
useEffect(() => {
function onResize() {
// ...
}
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize); // 정리
}, []); // 한 번만 등록
네트워크 요청 취소(경쟁 상태 방지) 예시:
useEffect(() => {
const ac = new AbortController();
(async () => {
const res = await fetch(`/api/items?q=${query}`, { signal: ac.signal });
// ...
})();
return () => ac.abort(); // 최신 검색어로 전환되면 이전 요청 중단
}, [query]);
① 불필요한 Effect 남용
“값이 바뀌면 상태를 또 세팅” 같은 내부 로직은 종종 Effect 없이 계산/파생 상태로 처리할 수 있습니다. 외부 시스템과의 동기화가 아니라면 Effect를 제거하는 편이 코드가 단순해지고 오류도 줄어듭니다.
② 비동기 함수 직접 전달 금지
useEffect(async () => { ... })처럼 async 함수를 곧장 전달하지 마십시오. 반환값이 Promise가 되어 클린업 시그니처와 충돌합니다. 내부에서 즉시 실행하는 async 함수를 정의해 호출하십시오(상기 예시 참고).
③ 이벤트 vs 효과의 경계
사용자 상호작용에 즉시 반응할 일은 이벤트 핸들러에, 외부 시스템과의 지속적 동기화는 Effect에 두면 설계가 선명해집니다.
useEffect는 렌더 후 외부 시스템과의 상태를 시작/중지하는 도구입니다.