소피가 화이트보드에 그린 그림은 팀원들의 머릿속에서 떠나지 않았다.
기능별로 똘똘 뭉친 코드 덩어리.
그것은 모든 개발자가 꿈꾸는 이상향이었지만, 클래스라는 현실의 벽은 너무나도 견고했다.
그 벽의 핵심에는 ‘생명주기 메서드(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번 채널에서 방송해주는 것과 같았다. 시청자는 모든 채널을 정확한 순서에 맞춰 돌려야만 전체 이야기를 이해할 수 있었다.
댄은 키보드에서 손을 떼고 의자에 등을 기댔다.
소피가 지적했던 ‘관심사 분리의 실패’가 바로 이것이었다.
생명주기 메서드는 코드를 논리적인 기능 단위가 아니라, 시간의 흐름에 따라 억지로 분리시켰다.
이것이 리액트가 가진 본질적인 한계였다.
이 한계를 극복하려면, 생명주기라는 개념 자체에 도전해야 했다.
‘언제’ 실행할지를 명령하는 방식에서 벗어나, 특정 데이터와 ‘관련된’ 로직을 그저 한 곳에 뭉쳐 선언할 수 있는 새로운 방법이 필요했다.
리액트 팀의 고민은 점점 더 깊어졌다.
그들은 이제 단순한 리팩토링이 아닌, 리액트의 근본 철학을 재정의하는 길목에 서 있었다. 그 길의 끝에 무엇이 기다리고 있을지, 아직은 아무도 알 수 없었다.