Frontend/React

useEffect의 cleanup 함수 알아보기

JTB 2025. 10. 24. 18:20

1. Cleanup 함수의 기본 역할

React를 다루다 보면 useEffect 내부에 return으로 전달하는 cleanup 함수(clean-up function)를 자주 보게 된다. cleanup 함수는 컴포넌트가 언마운트되거나, 의존성 배열(dependency array)에 변화가 생길 때 이전 이펙트를 정리하기 위해 동작한다. 이를 통해 메모리 누수나 불필요한 이벤트 리스너 잔존 문제를 방지한다.

useEffect(() => {
  const id = setInterval(() => {
    console.log('tick');
  }, 1000);

  // cleanup 함수
  return () => {
    clearInterval(id);
    console.log('cleanup!');
  };
}, []);

예를 들어 setInterval, addEventListener, WebSocket, Subscription 등 외부 리소스를 사용하는 경우, 이를 정리하지 않으면 메모리 누수나 중복 실행 문제가 생길 수 있다.

 

 

2. Cleanup 함수의 실행 타이밍

(1) 컴포넌트 언마운트 시

컴포넌트가 화면에서 사라질 때(unmount), cleanup 함수가 한 번 실행된다.

useEffect(() => {
  const timer = setInterval(() => console.log('running...'), 1000);

  return () => {
    clearInterval(timer);
    console.log('unmounted - cleanup!');
  };
}, []); // 빈 배열
  • []는 의존성이 없기 때문에 effect는 최초 마운트 시 한 번 실행한다.
  • 컴포넌트가 언마운트될 때 cleanup이 실행된다.

(2) 디펜던시 변경 시

dependency array에 값이 들어 있다면, 값이 바뀌기 직전에 cleanup이 먼저 실행된다.

useEffect(() => {
  console.log('effect 실행:', count);

  return () => {
    console.log('cleanup 실행:', count);
  };
}, [count]);

실행 순서 예시는 다음과 같다:

count = 0 → effect 실행: 0
count = 1 → cleanup 실행: 0 → effect 실행: 1
count = 2 → cleanup 실행: 1 → effect 실행: 2

 

즉, useEffect는 의존성 배열에 있는 값이 바뀌기 전에, 이전 effect에서 생성된 side effect(이전 effect가 남긴 부수작용)을 먼저 정리(cleanup)한다. 이렇게 함으로써 이전 effect와 새 effect가 동시에 영향을 주어 꼬이는 것을 방지하고, side effect가 안전하게 관리되도록 보장한다.

 

side effect 제거 예시

1. 타이머/Interval

useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);

  return () => clearInterval(id); // cleanup
}, [count]);

count가 바뀔 때마다 새 interval이 생성되면 이전 interval이 그대로 남아 중복 실행될 수 있기 때문에 cleanup이 이전 interval을 제거해서 꼬임을 방지한다.

 

2. 구독/이벤트 리스너

useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener("resize", handleResize);

  return () => window.removeEventListener("resize", handleResize);
}, []);

 

컴포넌트가 언마운트되거나 dependency가 바뀔 때 이전 리스너를 제거하지 않으면 메모리 누수나 중복 호출이 발생할 수 있다.

 

3. API 요청 취소

useEffect(() => {
  const controller = new AbortController(); // 취소 토큰 생성
  fetch("/api/data", { signal: controller.signal });

  return () => controller.abort(); // 이전 요청 취소
}, [query]);

 

사용자가 빠르게 검색어를 바꾸면 이전 요청이 완료되지 않은 상태에서 새 요청이 발생한다. 이때, cleanup에서 이전 요청을 취소하면, 꼬이는 케이스나 불필요한 네트워크 요청을 막을 수 있다.

 

3. 주의해야 할 점

1. Cleanup이 없으면?

이벤트 리스너를 추가하는 예시에서 cleanup이 없으면, 이벤트가 계속 추가되며 점점 중복 실행된다.

useEffect(() => {
  window.addEventListener('scroll', handleScroll);
}, []);

이를 정리하지 않으면 컴포넌트가 사라져도 이벤트가 남아 있을 수 있다.

 

따라서, 아래와 같이 명시적으로 제거해줘야 한다. 

useEffect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

2. 의존성 배열 누락 시

useEffect의 의존성 배열을 잘못 설정하면 cleanup이 제대로 작동하지 않거나, 필요 이상으로 effect가 반복 실행된다.

React의 ESLint 플러그인(eslint-plugin-react-hooks)을 사용하면 문제를 자동으로 감지한다.

# 설치
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc.json 예시
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error", // 훅 사용 규칙
    "react-hooks/exhaustive-deps": "warn"  // 의존성 배열 체크
  }
}

 

ESLint 경고 예시

React Hook useEffect has missing dependencies: 'count' and 'name'. Either include them or remove the dependency array. (react-hooks/exhaustive-deps)

 

4. 실무에서의 팁

  • Subscription / Timer / EventListener → 반드시 cleanup을 작성한다.
  • 단순 API 호출만 하는 경우 cleanup은 필수는 아니지만, 실시간 검색처럼 사용자가 계속 입력을 바꾸며 새로운 쿼리가 발생하는 상황에서는 이전 요청을 취소하기 위해(취소 토큰 관리를 위해) cleanup을 활용할 수 있다.
  • React.StrictMode에서는 개발 환경에서 effect가 두 번 실행되므로 cleanup이 정확히 되어 있는지 검증할 수 있다.

 

React.StrictMode 에서의 문제 발생 케이스: cleanup 누락

import { useEffect, useState } from 'react';

function ScrollTracker() {
  const [scroll, setScroll] = useState(0);

  const handleScroll = () => {
    setScroll(window.scrollY);
    console.log('스크롤 이벤트 실행');
  };

  useEffect(() => {
    // cleanup을 작성하지 않음
    window.addEventListener('scroll', handleScroll);
  }, []);

  return <div>현재 스크롤 위치: {scroll}</div>;
}

 

스크롤 이벤트 실행
스크롤 이벤트 실행
  • StrictMode에서 effect가 두 번 실행되기 때문에, 이벤트 리스너가 두 번 등록
  • 결과적으로 스크롤을 한 번 움직여도 handler가 두 번 호출
  • 실제 배포 환경에서는 한 번만 실행되지만, 개발 중에는 중복 문제를 발견할 수 있도록 도와준다.

 

5. 요약 및 마무리

상황 cleanup 실행 시점
[] (빈 배열) 언마운트 시 한 번
[deps] (의존성 있음) deps 변경 시마다, 다음 effect 실행 전에
생략 시 cleanup 없음 — 메모리 누수 가능성 ↑
StrictMode 개발환경에서 두 번 호출되어 정상 동작 여부 테스트

cleanup 함수는 “효과를 없애는 함수”가 아니라, 이전 상태를 정리하고 새로운 상태를 준비하는 과정이다. React는 이를 통해 side effect를 예측 가능한 순서로 관리한다.