useEffect의 세 가지 얼굴: Unmount

552025년 10월 09일3

useEffect의 마지막 얼굴은 새로운 API나 개념을 필요로 하지 않았다. 그것은 이미 그들이 설계한 ‘클린업 함수’의 동작 방식 속에 자연스럽게 포함되어 있었다. 바로 컴포넌트가 화면에서 완전히 사라지는, 즉 ‘언마운트(unmount)’되는 순간을 다루는 역할이었다.

댄은 이 개념을 설명하기 위해, 간단한 토글(Toggle) UI를 만들었다.

function App() {
  const [showTimer, setShowTimer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
      {showTimer && <Timer />}
    </div>
  );
}

function Timer() {
  useEffect(() => {
    console.log('Timer component mounted, effect is running.');

    const timerId = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // 클린업 함수
    return () => {
      console.log('Timer component will unmount, cleaning up.');
      clearInterval(timerId);
    };
  }, []); // 빈 배열: 이 효과는 마운트 시 한 번만 실행된다.
}

Timer 컴포넌트의 useEffect를 보세요.”
댄이 워크숍 청중에게 말했다.
“의존성 배열이 비어 있으니, 이 효과는 Timer 컴포넌트가 처음 화면에 나타날 때 딱 한 번만 실행될 겁니다. 그리고 setInterval을 통해 1초마다 ‘Tick’을 출력하기 시작하죠.”

그는 코드를 실행했다.
콘솔에는 ‘Timer component mounted, effect is running.’이라는 메시지와 함께, 1초 간격으로 ‘Tick’이 찍히기 시작했다.

“이제, Toggle Timer 버튼을 눌러보겠습니다.”
댄이 버튼을 클릭했다.
showTimer 상태가 false로 바뀌면서, Timer 컴포넌트는 화면에서 사라졌다. 즉, 언마운트되었다.

그 순간, 콘솔에 새로운 메시지가 나타났다.
Timer component will unmount, cleaning up.
그리고 더 이상 ‘Tick’은 찍히지 않았다.

“무슨 일이 일어난 걸까요?”
댄이 물었다.
“리액트는 Timer 컴포넌트를 DOM에서 제거하기 직전에, 이 컴포넌트가 이전에 등록했던 모든 효과들의 클린업 함수를 실행합니다. 우리가 useEffect에서 반환했던 바로 저 함수 말이죠.”

클린업 함수는 clearInterval(timerId)를 호출하여, 더 이상 필요 없어진 타이머를 깔끔하게 제거했다. 만약 이 클린업 과정이 없다면, Timer 컴포넌트는 화면에서 사라졌음에도 불구하고 보이지 않는 곳에서 계속 메모리를 차지하며 ‘Tick’을 외치고 있었을 것이다. 이것이 바로 ‘메모리 누수(Memory Leak)’였다.

이것으로 useEffect의 세 가지 얼굴이 모두 드러났다.

  1. Mount: useEffect(fn, []) - 빈 의존성 배열은 componentDidMount처럼 작동한다.
  2. Update: useEffect(fn, [dep1, dep2]) - 의존성을 명시하면 componentDidUpdate처럼 특정 값의 변화에 반응한다.
  3. Unmount: useEffect에서 반환된 클린업 함수는 컴포넌트가 사라질 때 componentWillUnmount처럼 작동한다.

댄은 이 세 가지를 하나의 표로 정리했다.
“보시다시피, useEffect는 이 세 가지 생명주기를 각각 별개의 개념으로 다루지 않습니다. 그것들은 모두 ‘효과와 정리’라는 단일한 모델의 다른 측면일 뿐입니다.”

개발자들은 더 이상 Mount, Update, Unmount라는 시간의 흐름에 얽매일 필요가 없었다.
그들은 그저 ‘이 효과는 어떤 일을 하고, 어떻게 정리되어야 하며, 무엇에 의존하는가?’ 이 세 가지만 생각하면 되었다.

useEffect는 클래스 시절의 흩어지고 복잡했던 생명주기 개념을, 하나의 통일되고 선언적인 API로 완벽하게 통합해냈다.

하지만 이 강력한 도구에는 여전히 숨겨진 함정들이 도사리고 있었다.
특히 의존성 배열은, 개발자들이 가장 흔하게 실수를 저지르는, 양날의 검과도 같은 존재였다. 팀은 곧, 이 의존성 배열이 일으킬 수 있는 가장 흔하고도 치명적인 문제와 마주하게 될 터였다.