React 기초6편

useEffect 정리 함수(cleanup) — 켠 것을 끄는 대칭의 기술

useEffect의 정리 함수가 언제, 왜 실행되는지 생명주기로 정리합니다. 흔히 오해하는 '개발 모드 이중 실행'의 진짜 이유(StrictMode)까지 명확히 짚습니다.

지난 글에서 “타이머를 켜거나 서버에 연결했다면, 컴포넌트가 사라질 때 꺼 줘야 한다”고 했습니다. 그 끄는 일을 담당하는 것이 정리 함수(cleanup)입니다. 개념 자체는 단순한데, 실행 시점이 헷갈려서 useEffect에서 가장 많은 혼란이 생기는 지점이기도 합니다. 오늘 그 타이밍을 확실히 잡아 보겠습니다.


정리 함수란

정리 함수는 설정 함수가 반환하는 함수입니다. 문법은 이게 전부입니다.

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();          // 설정: 연결한다
  return () => connection.disconnect(); // 정리: 연결을 끊는다
}, [roomId]);

return 뒤의 함수가 정리 함수입니다. 이름 그대로 설정 함수가 벌여 놓은 일(연결·구독·타이머 등)을 되돌리는 역할을 합니다. 핵심 감각은 “정리 함수는 설정 함수의 ”이라는 것입니다. 설정에서 켰으면 정리에서 끄고, 설정에서 구독했으면 정리에서 구독을 해지합니다.


언제 실행되나 — 세 가지 시점

React는 필요할 때마다 설정과 정리를 부르는데, 정리하면 세 가지입니다.

  1. 마운트(컴포넌트가 처음 화면에 등장): 설정 함수 실행
  2. 의존성이 바뀌어 다시 실행될 때: 먼저 옛 값으로 정리 함수를 실행하고, 그다음 새 값으로 설정 함수를 실행
  3. 언마운트(컴포넌트가 화면에서 사라짐): 정리 함수를 마지막으로 한 번 실행

2번이 특히 중요합니다. 위 예시에서 roomId1번방에서 2번방으로 바뀌면, React는 “1번방 연결을 끊고 → 2번방에 연결”하는 순서로 동작합니다. 새로 연결하기 전에 옛 연결을 정리하기 때문에, 방을 옮길 때 이전 연결이 그대로 남아 중복되는 사고가 생기지 않습니다.

즉 정리 함수는 “컴포넌트가 죽을 때만” 도는 게 아니라, 설정이 다시 실행되기 직전마다 옛 것을 되돌리기 위해 함께 돕니다.


개발 모드에서 두 번 실행되는 이유

여기서 많은 사람이 크게 헷갈립니다. 개발 환경에서 이펙트를 만들면, 마운트 시 설정이 두 번 불리는 것처럼 보입니다(설정 → 정리 → 설정). 로그를 찍어 보면 초기화가 두 번 도는 것 같아 당황하게 되죠.

이건 버그도, JavaScript 비동기 동작 탓도 아닙니다. React의 StrictMode가 개발 모드에서만 일부러 설정→정리→설정을 한 번 더 돌리는 것입니다. 목적은 하나, 정리 함수가 빠졌거나 잘못된 이펙트를 조기에 드러내려는 스트레스 테스트입니다.

논리는 이렇습니다. 정리 함수가 설정 함수를 완벽히 되돌린다면, “설정을 한 번 부른 것(프로덕션)“과 “설정→정리→설정(개발)“은 사용자 입장에서 구별되지 않아야 합니다. 만약 이중 실행 때문에 눈에 보이는 문제(중복 연결, 이벤트 두 번 등록 등)가 생긴다면, 그건 React가 알려 주는 신호입니다 — 당신의 정리 함수에 빠진 로직이 있다고요.

그러니 개발 모드의 이중 실행은 없애야 할 대상이 아니라, 정리 로직이 온전한지 검증받는 기회입니다. 프로덕션 빌드에서는 이중 실행이 사라지고 설정은 한 번만 돕니다.


그래서 정리 함수를 이렇게 보면 쉽다

정리 함수를 “가끔 필요한 마무리 청소” 정도로 보면 타이밍이 계속 헷갈립니다. 대신 이렇게 보세요. 모든 설정 함수는 자신을 되돌리는 정리 함수와 한 쌍이어야 한다. 연결했으면 끊고, 구독했으면 해지하고, 타이머를 걸었으면 해제한다 — 이 대칭만 지키면, React가 그 짝을 언제 실행하든(재실행 전이든 언마운트든, 개발 모드의 이중 실행이든) 항상 올바르게 동작합니다.

이렇게 State와 Effect까지 왔습니다. 다음 글에서는 상태와는 또 다른 성격의 값 — 바뀌어도 화면을 다시 그리지 않으면서, 렌더링을 넘어 값을 기억하는 useRef를 살펴보겠습니다.