뒤섞인 관심사

122025년 08월 27일3

생명주기 메서드가 코드를 기능별로 찢어놓는다는 사실은 문제의 절반에 불과했다. 댄은 곧, 그 반대의 현상 또한 심각한 문제임을 깨달았다.

어느 날, 그는 페이스북의 한 페이지를 담당하는 컴포넌트 코드를 분석하고 있었다. 그 컴포넌트는 사용자의 타임라인을 보여주는, 제법 규모가 큰 코드였다. componentDidMount 메서드 하나만 해도 수십 줄에 달했다.

댄은 메서드 안의 코드를 천천히 읽어 내려갔다.

componentDidMount() {
  // 1. 타임라인 데이터를 서버에서 가져온다.
  fetchPosts(this.props.userId).then(posts => {
    this.setState({ posts });
  });

  // 2. 새로운 게시물이 올라오면 알려주는 푸시 알림을 구독한다.
  Notifications.subscribe(this.props.userId, this.handleNewPost);

  // 3. 사용자가 스크롤을 얼마나 내렸는지 추적하기 위해 이벤트 리스너를 등록한다.
  window.addEventListener('scroll', this.handleScroll);

  // 4. 분석을 위해 사용자가 페이지에 진입했음을 기록하는 로그를 보낸다.
  Analytics.logPageView(this.props.pageName);
}

그의 눈살이 미세하게 찌푸려졌다.
이 네 덩어리의 코드는 모두 ‘컴포넌트가 처음 화면에 나타났을 때’ 실행되어야 한다는 공통점 외에는, 서로 아무런 관련이 없었다.

데이터를 가져오는 로직.
알림을 구독하는 로직.
스크롤을 추적하는 로직.
분석 로그를 보내는 로직.

이 네 가지는 완전히 별개의 ‘관심사(Concern)’였다.
하지만 클래스의 생명주기 모델은 이 모든 것을 componentDidMount라는 단 하나의 바구니에 쑤셔 넣도록 강요했다.

마치 주방의 한 서랍을 열었는데, 그 안에 칼과 포크뿐만 아니라 건전지, 망치, 서류 뭉치가 함께 뒤섞여 있는 것과 같았다. 당장 필요한 물건을 찾기도 어려울뿐더러, 무엇이 들어있는지 전체 목록을 파악하는 것조차 불가능했다.

이 문제는 componentDidMount에서 끝나지 않았다.
componentWillUnmount 메서드 역시 똑같은 혼돈을 품고 있었다.

componentWillUnmount() {
  // 2. 푸시 알림 구독을 해제한다.
  Notifications.unsubscribe(this.props.userId, this.handleNewPost);

  // 3. 스크롤 이벤트 리스너를 제거한다.
  window.removeEventListener('scroll', this.handleScroll);
}

알림 구독 해지 로직은 componentDidMount의 알림 구독 로직과 짝을 이루었다.
스크롤 이벤트 리스너 제거 로직은 스크롤 이벤트 리스너 등록 로직과 짝을 이루었다.

개발자가 알림 관련 기능을 수정하려면, 그는 componentDidMount의 중간 부분과 componentWillUnmount의 첫 부분을 찾아내 함께 수정해야 했다. 두 메서드 사이를 끊임없이 오가며, 다른 로직들 사이에서 필요한 코드 조각을 찾아 헤매야 했다.

소피가 지적했던 문제가 다시 한번 명확하게 드러났다.
관련 있는 코드는 흩어지고(찢어진 기능),
관련 없는 코드는 뭉쳐있다(뒤섞인 관심사).

클래스 컴포넌트는 본질적으로 코드를 ‘기능’이 아닌 ‘시간’을 기준으로 정리하도록 강제했다. 이 구조적인 제약 때문에, 컴포넌트가 조금만 복잡해져도 코드는 필연적으로 뒤죽박죽이 되었다. 버그는 이런 혼돈 속에서 피어나는 곰팡이와 같았다.

댄은 생각했다.
‘만약 알림 구독 로직을 통째로 떼어내서 다른 컴포넌트에 이식하고 싶다면? 불가능하다.’
componentDidMountcomponentWillUnmount에서 해당 부분만 조심스럽게 잘라내어 옮겨야 하는데, 그 과정에서 실수가 일어나기 너무나 쉬웠다.

로직의 재사용성은 제로에 가까웠다.
유지보수성은 최악으로 치닫고 있었다.

리액트 팀은 이제 확신했다.
이것은 개발자의 부주의나 실력 문제가 아니었다. 이것은 리액트의 클래스 모델 자체가 가진, 태생적인 한계였다.

새로운 패러다임이 필요했다.
시간의 흐름에 코드를 맞추는 것이 아니라, 코드의 논리적 흐름에 따라 기능들을 자유롭게 묶고 해체할 수 있는, 그런 유연한 구조가 절실했다.
그들의 고민은 이제 ‘어떻게’를 넘어, ‘무엇으로’ 이 문제를 해결할 것인가로 옮겨가고 있었다.