componentWillUnmount의 중요성을 강조하기 위해, 톰은 팀에게 의도적으로 버그가 있는 코드를 리뷰하게 했다. 그것은 페이지의 너비가 변경될 때마다 레이아웃을 다시 계산하는 <ResponsiveContainer>라는 컴포넌트였다.
리뷰를 맡은 맷은 코드를 훑어보았다.
var ResponsiveContainer = React.createClass({
getInitialState: function() {
return {
windowWidth: window.innerWidth
};
},
handleResize: function() {
// 창 너비가 바뀔 때마다 state를 업데이트한다.
this.setState({ windowWidth: window.innerWidth });
},
componentDidMount: function() {
// 윈도우의 resize 이벤트에 핸들러를 등록한다.
window.addEventListener('resize', this.handleResize);
},
render: function() {
var content = '현재 창 너비: ' + this.state.windowWidth + 'px';
return React.createElement('div', null, content);
}
});
언뜻 보기에는 문제가 없어 보였다. 컴포넌트가 마운트되면 resize 이벤트를 듣기 시작하고, 창 너비가 바뀔 때마다 setState를 호출하여 화면을 갱신하는 로직은 완벽했다.
“이 코드의 문제점이 뭐라고 생각합니까, 맷?” 톰이 물었다.
맷은 잠시 생각에 잠겼다. 그리고는 아차 하는 표정을 지었다.
“...componentWillUnmount가 없군요.”
“정확합니다.” 톰이 말했다. “그게 어떤 결과를 낳을까요?”
맷은 시나리오를 머릿속으로 그려보았다.
- 사용자가 어떤 페이지에 들어가서
<ResponsiveContainer>가 렌더링된다.componentDidMount가 호출되고,window객체에handleResize함수가 이벤트 리스너로 등록된다. - 사용자가 다른 페이지로 이동한다.
<ResponsiveContainer>컴포넌트는 화면에서 사라지고, 리액트는 해당 인스턴스를 메모리에서 제거(언마운트)한다. - 하지만,
window객체에 등록된 이벤트 리스너는 누가 제거해주지 않았기 때문에 여전히 메모리에 살아있다. 이 리스너는 존재하지 않는 컴포넌트 인스턴스의handleResize메서드를 참조하고 있다. - 사용자가 브라우저 창 크기를 조절한다.
resize이벤트가 발생하고,window는 등록된handleResize를 호출하려고 시도한다. - 이미 사라진 컴포넌트의 메서드를 호출하려 하니, 에러가 발생하거나 최악의 경우 브라우저가 불안정해진다.
더 심각한 것은, 사용자가 이 페이지를 여러 번 들락날락할 경우였다. 페이지에 들어올 때마다 새로운 이벤트 리스너가 window 객체에 계속해서 쌓여만 갈 것이다. 명백한 메모리 누수였다.
“모든 개발자는 자신이 벌인 일을 직접 치워야 합니다.” 톰이 강조했다. “addEventListener를 했으면, removeEventListener를 해야 합니다. 이것은 프로그래밍의 기본 원칙이죠. 그리고 리액트는 componentWillUnmount라는, 그 일을 하기에 가장 완벽한 장소를 제공해 줍니다.”
맷은 코드를 올바르게 수정했다.
var ResponsiveContainer = React.createClass({
// ... (이전과 동일)
componentDidMount: function() {
window.addEventListener('resize', this.handleResize);
},
// 컴포넌트가 사라지기 직전에, 등록했던 이벤트 리스너를 반드시 제거한다.
componentWillUnmount: function() {
window.removeEventListener('resize', this.handleResize);
},
render: function() { /* ... */ }
});
이 간단한 수정만으로, <ResponsiveContainer>는 비로소 안전하고 재사용 가능한 부품이 되었다.
이 작은 리뷰 세션은 팀 전체에 중요한 경각심을 일깨워주었다. componentDidMount와 componentWillUnmount는 단순한 옵션이 아니었다. 그들은 언제나 함께 고려되어야 하는, 동전의 양면과도 같은 존재였다.
하나의 부작용을 시작했다면, 그 부작용을 끝내는 책임도 반드시 져야 한다. 리액트는 생명주기 메서드를 통해 그 시작과 끝을 명확히 알려줌으로써, 개발자들이 책임감 있는 코드를 작성하도록 유도하고 있었다. 뒷정리의 중요성. 그것은 안정적인 애플리케이션을 만드는 가장 기본적인, 그러나 가장 쉽게 잊히는 덕목이었다.


