리액트를 오픈소스로 공개하기 위한 준비 작업은 순조롭게 진행되고 있었다. 팀은 외부 개발자들이 리액트의 강력함을 한눈에 이해할 수 있도록, 다양하고 실용적인 예제 컴포넌트들을 만드는 데 집중하고 있었다.
맷은 ‘실시간 뉴스 알림’ 컴포넌트를 만드는 임무를 맡았다. 기능은 간단했다. 5초마다 서버에 새로운 소식이 있는지 확인하고, 새로운 소식이 있으면 setState를 호출하여 화면에 알림 배지를 보여주는 것이었다.
그는 능숙하게 <NewsNotifier> 컴포넌트의 뼈대를 만들었다.
var NewsNotifier = React.createClass({
getInitialState: function() {
return { hasNewNotification: false };
},
// ... (여기에 5초마다 상태를 체크하는 로직이 들어가야 함) ...
render: function() {
if (!this.state.hasNewNotification) {
return null; // 새 소식이 없으면 아무것도 그리지 않음
}
return React.createElement('div', {className: 'notification-badge'}, '새 소식!');
}
});
문제는 주석으로 남겨둔 바로 저 부분이었다. 5초마다 주기적으로 코드를 실행하기 위해서는 자바스크립트의 setInterval 함수를 사용해야 했다.
setInterval(this.checkForNotifications, 5000);
그런데, 이 코드를 대체 어디에 두어야 할까?
맷은 여러 가지 가능성을 떠올렸지만, 모두 막다른 길이었다.
render()메서드 안? 절대 안 될 일이었다.render는setState가 호출될 때마다 다시 실행된다. 여기에setInterval을 넣으면, 상태가 한번 바뀔 때마다 새로운 타이머가 무한정으로 생성되는 재앙이 발생할 것이다.getInitialState()메서드 안? 이곳은 컴포넌트가 생성될 때 딱 한 번만 실행되니 괜찮을까? 하지만 이 메서드의 역할은 오직 초기 상태를 반환하는 것이었다. 여기에 타이머를 설정하는 ‘부작용(Side Effect)’을 일으키는 코드를 넣는 것은 리액트의 설계 원칙에 어긋났다.
결국 그는 컴포넌트 명세 객체의 최상단에 startPolling 같은 임의의 메서드를 만들어, 컴포넌트를 사용하는 외부 코드에서 수동으로 호출하는 임시방편을 생각했다. 하지만 이내 더 심각한 문제를 깨달았다.
“타이머를 시작하는 건 어떻게든 해보겠어요. 그런데… 타이머를 멈추는 건요?”
그는 팀 회의에서 이 문제를 제기했다.
“사용자가 다른 페이지로 이동해서, 이 <NewsNotifier> 컴포넌트가 화면에서 사라졌다고 가정해 봅시다. 컴포넌트는 사라졌지만, 우리가 시작한 setInterval 타이머는 메모리 어딘가에서 여전히 살아있을 겁니다. 5초마다 계속해서 this.checkForNotifications를 호출하려고 하겠죠. 이건 명백한 메모리 누수(Memory Leak)입니다.”
그의 지적에 회의실은 순식간에 조용해졌다. 모두가 문제의 심각성을 깨달았다.
컴포넌트는 이제 스스로의 ‘삶’을 갖게 되었다. 하지만 개발자에게는 그 컴포넌트가 언제 태어나는지(화면에 추가되는지), 그리고 언제 죽는지(화면에서 제거되는지)를 알 수 있는 방법이 전혀 없었다.
‘탄생’의 순간을 알아야 서버에 데이터를 요청하거나 타이머를 시작하는 등의 준비 작업을 할 수 있다.
‘죽음’의 순간을 알아야 사용했던 자원들을 깨끗하게 정리하고 떠날 수 있다.
“우리에게는 신호가 필요합니다.” 맷이 결론을 내렸다. “리액트가 우리에게 알려주는 신호요. ‘지금 당신의 컴포넌트가 막 세상에 태어났습니다’ 라고 알려주는 순간과, ‘이제 당신의 컴포넌트가 곧 사라질 겁니다’ 라고 알려주는 순간. 이 두 개의 시점을 알지 못하면, 우리는 안전한 컴포넌트를 만들 수 없습니다.”
팀원들은 모두 조던 워크를 바라보았다. 그의 표정은 놀랍도록 평온했다. 마치 이 질문이 나오기를 오랫동안 기다려왔다는 듯이. 그는 리액트 컴포넌트에 진정한 ‘생명’을 불어넣을 다음 단계의 설계를 이미 머릿속에 그려두고 있었다.


