생명주기 메서드의 함정

112025년 08월 26일4

소피가 화이트보드에 그린 그림은 팀원들의 머릿속에서 떠나지 않았다.
기능별로 똘똘 뭉친 코드 덩어리.
그것은 모든 개발자가 꿈꾸는 이상향이었지만, 클래스라는 현실의 벽은 너무나도 견고했다.

그 벽의 핵심에는 ‘생명주기 메서드(Lifecycle Methods)’라는 개념이 있었다.
componentDidMount, componentDidUpdate, componentWillUnmount.
이 세 가지 메서드는 클래스 컴포넌트의 삶과 죽음을 관장하는, 신과 같은 존재였다.

댄은 자신의 모니터에 간단한 예제 컴포넌트를 띄웠다. 친구의 온라인 상태를 실시간으로 보여주는, 바로 그 익숙한 기능이었다.

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
  }

  componentDidMount() {
    // 컴포넌트가 세상에 처음 나타났을 때, 단 한 번 호출된다.
    // "API야, 내 친구(this.props.friend.id)의 상태를 구독할게."
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

  componentDidUpdate(prevProps) {
    // props나 state가 변경될 때마다 호출된다.
    // "혹시 친구 ID가 바뀌었니? 그럼 이전 친구 구독은 끊고,"
    if (this.props.friend.id !== prevProps.friend.id) {
      ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);
      // "새로운 친구의 상태를 구독해야지."
      ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
    }
  }

  componentWillUnmount() {
    // 컴포넌트가 세상에서 사라지기 직전, 단 한 번 호출된다.
    // "나 이제 사라지니까, 친구 상태 구독은 그만할게."
    ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

  handleStatusChange = (status) => {
    this.setState({ isOnline: status.isOnline });
  };

  render() {
    // ... isOnline 상태에 따라 초록불/회색불 표시 ...
  }
}

이 코드는 완벽하게 작동했다.
하지만 댄은 이 코드 안에 숨겨진 함정들을 보고 있었다.

첫 번째 함정.
단순히 친구의 온라인 상태를 ‘구독’하고 ‘구독 해지’하는 하나의 기능을 구현하기 위해, 개발자는 세 개의 다른 메서드를 모두 이해하고 올바르게 사용해야만 했다.
구독 시작은 componentDidMount에서,
구독 대상이 바뀌는 경우는 componentDidUpdate에서,
구독 해지는 componentWillUnmount에서.
하나라도 놓치면, 메모리 누수가 발생하거나 엉뚱한 친구의 상태를 보여주는 버그가 생겼다.

두 번째 함정.
componentDidUpdate의 존재는 코드를 더욱 복잡하게 만들었다. 개발자는 항상 ‘이전 props’와 ‘현재 props’를 비교하는 조건문을 넣어야 했다. 만약 이 비교 로직을 빼먹으면, 컴포넌트는 렌더링될 때마다 불필요하게 구독과 해지를 반복하며 서버에 부담을 주게 될 터였다. 이 코드는 개발자의 실수를 유발하기 너무나 좋은 구조였다.

세 번째, 그리고 가장 교묘한 함정.
이 코드는 ‘친구 ID가 바뀌는 경우’를 처리하기 위해, componentDidMount에 있던 구독 로직을 componentDidUpdate에 거의 똑같이 복사해서 붙여넣어야 했다. 미묘하게 중복된 코드가 두 군데에 존재하게 된 것이다.

하나의 논리적인 기능(친구 상태 구독)이 세 개의 시간(Mount, Update, Unmount)으로 찢겨, 개발자의 집중력을 흩트리고 있었다.
마치 한 편의 영화를 보는데, 도입부는 1번 채널에서, 중간 내용은 2번 채널에서, 결말은 3번 채널에서 방송해주는 것과 같았다. 시청자는 모든 채널을 정확한 순서에 맞춰 돌려야만 전체 이야기를 이해할 수 있었다.

댄은 키보드에서 손을 떼고 의자에 등을 기댔다.
소피가 지적했던 ‘관심사 분리의 실패’가 바로 이것이었다.
생명주기 메서드는 코드를 논리적인 기능 단위가 아니라, 시간의 흐름에 따라 억지로 분리시켰다.

이것이 리액트가 가진 본질적인 한계였다.
이 한계를 극복하려면, 생명주기라는 개념 자체에 도전해야 했다.
‘언제’ 실행할지를 명령하는 방식에서 벗어나, 특정 데이터와 ‘관련된’ 로직을 그저 한 곳에 뭉쳐 선언할 수 있는 새로운 방법이 필요했다.

리액트 팀의 고민은 점점 더 깊어졌다.
그들은 이제 단순한 리팩토링이 아닌, 리액트의 근본 철학을 재정의하는 길목에 서 있었다. 그 길의 끝에 무엇이 기다리고 있을지, 아직은 아무도 알 수 없었다.