componentDidMount와 componentWillUnmount가 컴포넌트의 ‘탄생’과 ‘죽음’을 책임진다는 사실이 명확해지자, 팀의 관심은 컴포넌트 생애의 가장 긴 시간, 즉 ‘존재(Updating)’의 단계로 향했다.
“컴포넌트가 setState나 새로운 props 때문에 업데이트된 ‘직후’에 무언가 하고 싶을 때도 있지 않을까요?”
크리스가 질문을 던졌다. 그의 질문은 매우 실용적인 시나리오에 기반하고 있었다.
“예를 들어, 채팅 애플리케이션을 생각해 봅시다. 새로운 메시지가 도착해서 <MessageList> 컴포넌트가 업데이트되고, 목록의 길이가 길어졌습니다. 사용자 경험을 위해, 우리는 항상 가장 최근 메시지가 보이도록 스크롤을 맨 아래로 내려주고 싶을 겁니다.”
이 ‘스크롤을 내리는’ 동작은 명백히 DOM을 조작하는 부작용이다. 그리고 이 작업은 반드시 DOM 업데이트가 완료된 후에 일어나야 한다. 업데이트 전의 스크롤 높이와 업데이트 후의 스크롤 높이가 다르기 때문이다.
이 문제를 해결하기 위해, 리액트는 또 다른 생명주기 메서드를 제공했다.
componentDidUpdate(prevProps, prevState)
이름 그대로, “컴포넌트의 업데이트가 완료된 직후”에 호출되는 메서드였다.
prevProps: 업데이트되기 ‘이전’의props객체.prevState: 업데이트되기 ‘이전’의state객체.
리액트는 친절하게도 이전 props와 state를 인자로 전달해주었다. 이를 통해 개발자는 ‘무엇이 바뀌었기 때문에’ 업데이트가 일어났는지를 파악하고, 조건에 따라 다른 부작용을 수행할 수 있었다.
크리스는 자신의 아이디어를 코드로 구현했다.
var MessageList = React.createClass({
// ... getInitialState, componentDidMount 등 ...
// 컴포넌트 업데이트가 완료된 직후 호출된다.
componentDidUpdate: function(prevProps, prevState) {
// 이전 메시지 개수와 현재 메시지 개수를 비교한다.
if (prevState.messages.length < this.state.messages.length) {
console.log('새로운 메시지가 추가되었습니다. 스크롤을 맨 아래로 내립니다.');
// 실제 DOM 노드를 찾아서 스크롤 위치를 조작한다.
var listNode = React.findDOMNode(this);
listNode.scrollTop = listNode.scrollHeight;
}
},
render: function() {
// ... this.state.messages를 기반으로 메시지 목록을 렌더링 ...
}
});
코드는 명확했다. componentDidUpdate는 무조건 스크롤을 내리지 않는다. 오직 메시지 목록의 길이가 늘어났을 때, 즉 새로운 메시지가 추가되었을 때만 스크롤을 조작한다. 이 조건부 로직은 불필요한 DOM 조작을 막아주는 중요한 최적화였다.
componentDidUpdate의 등장은 컴포넌트의 업데이트 과정에 섬세함을 더했다. 이제 개발자들은 다음과 같은 흐름을 제어할 수 있게 되었다.
- 업데이트 발생:
setState가 호출되거나 새로운props가 들어온다. - 렌더링:
render메서드가 새로운 버추얼 DOM을 생성한다. - DOM 업데이트: 리액트가 실제 DOM을 변경한다.
- 업데이트 후 작업:
componentDidUpdate가 호출되어, DOM 변경이 완료된 것을 전제로 하는 추가적인 부작용을 수행한다.
이 메서드는 componentDidMount와 유사하게, 리액트의 선언적인 세계와 외부의 명령적인 세계를 잇는 또 하나의 다리 역할을 했다. 차이점은 componentDidMount가 ‘최초의 탄생’에 반응하는 반면, componentDidUpdate는 ‘모든 후속적인 변화’에 반응한다는 점이었다.
이제 컴포넌트는 탄생과 죽음뿐만 아니라, 자신의 삶의 매 순간순간에 일어나는 변화에까지 능동적으로 대처할 수 있는, 훨씬 더 정교한 생명체로 진화하고 있었다.


