2026. 7. 16.

React useEffect 두 번 실행, setup → cleanup → setup 로그로 구분하세요

React 개발 서버에서 useEffect 로그나 API 요청이 두 번 나타날 때 Strict Mode를 끄기 전에 setup, cleanup, 의존성, 외부 시스템 연결 여부를 순서대로 확인하는 방법입니다.

4 min read
React useEffect 두 번 실행, setup → cleanup → setup 로그로 구분하세요 대표 이미지

React 개발 서버를 열었더니 useEffect 안의 console.log, API 요청, 이벤트 구독이 두 번 나타날 수 있다. 한 번만 실행되게 만들려고 <StrictMode>를 지우거나 useRef 플래그를 넣기 쉽지만, 그러면 React가 드러낸 cleanup 누락을 숨길 수 있다.

먼저 setup과 cleanup 양쪽에 로그를 넣는다. 콘솔이 setup → cleanup → setup 순서라면 개발 모드 검사를 보고 있을 가능성이 크다. 이때 할 일은 두 번째 setup을 막는 것이 아니라 cleanup을 맞추는 것이다. 첫 setup이 만든 연결, 타이머, 이벤트를 cleanup이 정확히 멈추는지 확인한다.

React 개발 모드의 setup cleanup setup 순서와 반복 실행 원인을 비교한 도식

두 번 찍힌 로그에서 먼저 가를 것은 실행 횟수가 아니라 순서다

React의 useEffect 공식 문서는 Strict Mode가 켜진 개발 환경에서 첫 실제 setup 전에 setup과 cleanup 주기를 한 번 더 실행한다고 설명한다. 누락된 cleanup을 빨리 찾기 위한 개발 전용 검사다. StrictMode 공식 문서에 따르면 운영 빌드에는 같은 검사가 영향을 주지 않는다.

아래처럼 setup과 cleanup에 서로 대응하는 로그를 남기면 현재 상황을 빠르게 나눌 수 있다.

useEffect(() => {
  console.log('chat: setup', roomId);
  const connection = createConnection(roomId);
  connection.connect();

  return () => {
    console.log('chat: cleanup', roomId);
    connection.disconnect();
  };
}, [roomId]);

개발 서버를 새로 열었을 때 setup → cleanup → setup이 보이고 마지막 연결 하나만 남는다면 검사는 제 역할을 한 것이다. 반대로 setup → setup만 보이면서 연결이나 이벤트가 두 개씩 쌓인다면 cleanup이 빠졌거나 시작한 작업과 다른 작업을 멈추고 있다.

여기서 콘솔이 계속 늘어나는지 함께 본다. 첫 마운트에서 한 번 더 실행된 뒤 멈추는 현상과 렌더마다 반복되는 현상은 원인이 다르다. 후자는 dependency 배열 누락이나 매번 새로 만들어지는 객체·함수, Effect 안의 state 업데이트를 점검해야 한다. 계속 반복되는 쪽은 상태 업데이트와 의존성을 함께 보는 useEffect 반복 점검으로 이어서 확인할 수 있다.

Strict Mode보다 setup과 cleanup의 짝을 먼저 본다

cleanup은 컴포넌트가 사라질 때만 실행되는 코드가 아니다. dependency가 바뀌어 새 setup을 시작하기 전에도 이전 값으로 만든 작업을 정리한다. 따라서 setup에서 시작한 외부 작업마다 대응하는 종료 작업이 있어야 한다.

연결 구독 타이머 이벤트 요청별 cleanup 코드를 짝지은 React 점검표

연결과 구독은 같은 대상을 해제한다

웹소켓이나 채팅 연결을 열었다면 같은 인스턴스를 닫는다. 이벤트를 등록했다면 같은 대상, 같은 이벤트 이름, 같은 함수 참조로 제거한다.

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }

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

removeEventListener에 새 화살표 함수를 넘기면 처음 등록한 함수와 참조가 달라 해제되지 않는다. setup 안에서 이름 있는 함수를 만들고 cleanup에서 같은 함수를 넘기면 대응 관계가 눈에 보인다.

타이머와 요청은 이전 작업을 남기지 않는다

setInterval은 반환된 ID를 clearInterval에 넘긴다. 검색어가 바뀔 때 요청을 다시 보내는 Effect라면 이전 응답이 늦게 도착해 새 결과를 덮지 않도록 무시 플래그나 요청 취소 방식을 둔다. 단순히 로그를 한 번만 찍게 만들어서는 이전 작업이 사라지지 않는다.

ref 플래그는 cleanup을 대신하지 못한다

hasRun.current로 첫 실행만 통과시키면 개발 콘솔은 조용해진다. 그러나 화면을 떠났다가 다시 돌아왔을 때 예전 연결이 남아 있다면 새 연결과 함께 누적된다. 한 번만 실행된 것처럼 보이는 상태는 cleanup 완료 증거가 아니다. Effect 동기화 공식 가이드도 Effect를 한 번만 돌리는 법보다 remount 뒤에도 같은 결과가 되도록 고치는 것을 우선한다.

두 번 실행과 계속 반복은 dependency에서 갈린다

빈 배열 []은 Effect가 읽는 reactive value가 없을 때만 맞다. props, state, 컴포넌트 안에서 만든 변수나 함수를 읽는다면 실제 dependency를 적어야 한다. 경고를 주석으로 숨기거나 필요한 값을 배열에서 빼면 오래된 값을 읽는 다른 버그가 생긴다.

Effect가 매 렌더 뒤 다시 돈다면 아래 순서로 좁힌다.

  1. dependency 배열 자체가 빠졌는지 본다.
  2. Effect가 state를 바꾸고 그 state가 다시 dependency를 바꾸는지 본다.
  3. 객체나 함수가 렌더마다 새 참조로 만들어지는지 콘솔에 기록한다.
  4. 서로 다른 동기화 작업을 한 Effect에 묶었는지 나눈다.

React는 dependency를 Object.is로 비교한다. 내용이 같아 보여도 렌더마다 새 객체나 새 함수를 만들면 다른 값으로 판단될 수 있다. 무조건 useMemouseCallback을 붙이기 전에 그 객체가 Effect 밖에 고정될 수 있는지, 함수가 Effect 안으로 이동할 수 있는지부터 본다.

렌더가 느리거나 dependency가 어디서 바뀌는지 찾기 어렵다면 memo보다 먼저 렌더 원인을 좁히는 React 점검법처럼 한 동작을 정해 기록하면 범위가 줄어든다.

외부 시스템이 없다면 Effect를 지우는 편이 맞을 수 있다

Effect가 필요 없는 경우를 설명한 React 공식 가이드에 따르면 Effect는 React 밖의 시스템과 동기화할 때 쓰는 탈출구다. 외부 시스템이 없다면 props와 state로 화면 값을 계산하거나 버튼 클릭으로 요청을 보내는 코드는 Effect가 아닐 수 있다.

예를 들어 이름 두 칸을 합쳐 fullName을 만드는 데 별도 state와 Effect를 두면 렌더가 한 번 더 생긴다. 렌더 중 바로 계산하면 dependency와 cleanup을 고민할 이유도 사라진다.

// Effect와 파생 state 대신 렌더 중 계산한다.
const fullName = `${firstName} ${lastName}`;

사용자가 버튼을 눌렀을 때만 일어나야 하는 POST 요청은 클릭 handler에 둔다. 컴포넌트가 보였다는 이유만으로 구매나 결제 요청이 나가면 remount뿐 아니라 뒤로 가기에서도 다시 실행될 수 있다.

수정을 마치면 개발 서버에서 setup → cleanup → setup을 다시 확인한다. 연결, 구독, 타이머는 하나만 남아야 한다. 이어서 production build에서도 핵심 동작을 한 번 실행해 사용자에게 중복 결과가 없는지 본다. 두 환경을 나눠 확인하면 개발 전용 검사를 버그로 오해하지 않으면서 실제 cleanup 결함도 놓치지 않는다.

오늘 할 일은 Strict Mode를 끄는 것이 아니다. 문제 Effect 하나를 골라 setup에서 시작한 작업과 cleanup에서 멈춘 작업을 한 줄씩 짝지어 적고, 콘솔 순서와 남은 연결 수를 확인한다.

참고 출처

이 글은 AI 코딩과 개발 학습의 일반 정보 제공 목적입니다. 도구, 모델, 커리큘럼, 요금은 버전과 시점에 따라 달라질 수 있으므로 실습이나 도입 전 공식 문서와 최신 릴리스 노트를 확인하세요.

다음으로 읽을 기사

같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.

댓글 0

이 글을 읽은 독자들의 생각을 나눠보세요.

비밀번호(선택)

첫 번째 댓글을 남겨보세요

여러분의 생각이 다른 독자에게 도움이 됩니다.