useEffect의 세 가지 얼굴: Update

542025년 10월 08일4

useEffect의 첫 번째 얼굴, componentDidMount의 역할을 이해한 개발자들 앞에, 댄은 두 번째 얼굴을 드러냈다. 이 얼굴은 클래스 시절의 componentDidUpdate가 해결하려 했던 문제, 즉 ‘props나 state의 변화에 반응하여 부수 효과를 다시 실행하는’ 시나리오를 다루었다.

그는 이전 예제였던 UserProfile 컴포넌트를 조금 더 발전시켰다. 이제 이 컴포넌트는 여러 사용자의 프로필을 버튼 클릭으로 전환하며 보여줄 수 있었다.

function ProfilePage() {
  const [userId, setUserId] = useState(1);

  return (
    <div>
      <UserProfile userId={userId} />
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
    </div>
  );
}

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    console.log(`Fetching data for user ${userId}`);
    fetchUser(userId).then((data) => {
      setUser(data);
    });
  }, [userId]); // 의존성 배열에 'userId'가 들어있다.

  // ... 렌더링 로직 ...
}

“이번에는 useEffect의 의존성 배열에 userId라는 prop을 넣었습니다.”
댄이 코드를 가리키며 설명했다.
“이것은 리액트에게 아주 구체적인 지시를 내리는 겁니다. ‘이 효과 함수는 userId라는 값에 의존하고 있으니, userId 값이 이전 렌더링과 비교해서 바뀌었을 때만, 이 함수를 다시 실행해 줘.’ 라고 말이죠.”

그는 컴포넌트의 동작을 단계별로 시연했다.

  1. 첫 번째 렌더링:

    • ProfilePage가 렌더링되고, userId는 1이다.
    • UserProfileuserId={1} prop을 받고 렌더링된다.
    • useEffect가 실행된다. 리액트는 이전 렌더링이 없었으므로(마운트 시점), 효과 함수를 즉시 실행한다. userId 1에 대한 데이터를 가져온다.
  2. ‘Next User’ 버튼 클릭:

    • setUserId(2)가 호출된다. ProfilePage가 리렌더링된다.
    • UserProfile이 새로운 userId={2} prop을 받고 리렌더링된다.
    • 리액트는 useEffect를 다시 만난다. 그리고 의존성 배열을 확인한다.
    • 이전 렌더링의 userId는 1이었고, 현재 userId는 2다. 값이 바뀌었다!
    • 리액트는 효과 함수를 다시 실행한다. 이제 userId 2에 대한 데이터를 가져온다.

이것은 componentDidUpdate에서 if (prevProps.userId !== this.props.userId)라는 조건문을 사용해 수동으로 변화를 감지하던 것과 본질적으로 같았다. 하지만 훨씬 더 선언적이고 우아했다.

개발자는 더 이상 ‘어떻게’ 변화를 감지할지 고민할 필요가 없었다. 그저 ‘무엇’에 의존하는지만 정직하게 명시하면, 나머지는 리액트가 알아서 처리해주었다.

댄은 여기서 한 걸음 더 나아갔다.
“만약 클린업 함수가 함께 있다면 어떻게 될까요?”

그는 useEffect 코드를 살짝 수정했다.

useEffect(() => {
  console.log(`Subscribing to user ${userId}`);
  const subscription = subscribeToUser(userId);

  return () => {
    console.log(`Unsubscribing from user ${userId}`);
    subscription.unsubscribe();
  };
}, [userId]);

userId가 1에서 2로 바뀌는 순간, 리액트는 다음 순서로 동작합니다.”

  1. “먼저, 이전 효과를 정리하기 위해 클린업 함수를 실행합니다. 이때 클린업 함수는 이전 렌더링의 userId 값, 즉 1을 기억하고 있습니다. 콘솔에는 ‘Unsubscribing from user 1’이 찍힙니다.”
  2. “그 후에, 새로운 효과 함수를 실행합니다. 이 함수는 새로운 userId 값인 2를 사용하죠. 콘솔에는 ‘Subscribing to user 2’가 찍힙니다.”

장내에서 나지막한 감탄사가 흘러나왔다.
클래스 시절 componentDidUpdate 안에서 이전 구독을 해지하고 새로운 구독을 시작하던, 복잡하고 실수하기 쉬웠던 그 모든 로직이, 이제는 의존성 배열 하나로 완벽하게 자동화된 것이다.

useEffect의 두 번째 얼굴은 ‘동기화(Synchronization)’의 본질을 명확히 보여주었다.
그것은 단순히 코드를 실행하는 것이 아니라, 리액트의 상태(propsstate)를 외부 세계의 상태(DOM, 네트워크 요청, 구독 등)와 일치시키는, 지속적인 동기화 과정이었다.

이제 남은 것은 useEffect의 마지막 얼굴이었다.
컴포넌트가 자신의 삶을 다하고, 세상에서 사라지는 순간을 다루는 방법.
그 마지막 얼굴은 이미 그들이 발견한 클린업 함수의 동작 안에 자연스럽게 녹아 있었다.