전 글에서 전반적인 컴포넌트에 대한 내용과 생성 방법을 살펴보았습니다. 이 글에서는 컴포넌트를 좀 더 세밀하게 제어할 수 있도록 React 컴포넌트의 라이프사이클 이벤트에 대해 알아보도록 하겠습니다.
1. 라이프사이클 이벤트의 분류
컴포넌트는 각 프로세스가 진행되는 시점에 따라 라이프사이클 이벤트로 불리는 특별한 함수가 실행됩니다. 개발자는 이 라이프 사이클 이벤트를 기반으로 컴포넌트의 동작을 제어하고 사용자 정의를 할 수 있습니다.
라이프사이클 이벤트는 크게 세 가지로 분류됩니다.
- 마운팅(mounting) 이벤트 : 페이지에 컴포넌트를 나타내는 이벤트로, React 엘리먼트(컴포넌트 클래스의 인스턴스)를 DOM 노드에 추가하는 시점에 발생. 1회만 실행.
- 갱신(updating) 이벤트 : 컴포넌트 정보를 업데이트하는 이벤트로, 속성이나 상태가 변경되어 React 엘리먼트를 갱신하는 시점에 발생. 여러 번 실행.
- 언마운팅(unmounting) 이벤트 : 페이지에서 컴포넌트가 사라지는 이벤트로, React 엘리먼트를 DOM에서 제거하는 시점에 발생. 1회만 실행.
모든 React 컴포넌트는 라이프사이클 이벤트가 있습니다.(단, 상태 비저장 컴포넌트는 라이프사이클 메서드를 사용할 수 없습니다) 라이프사이클 이벤트를 사용하면 컴포넌트의 작업 수행을 향상하는 사용자 정의 로직을 구현할 수 있습니다. 위의 그림에서 본 세 분류의 이벤트를 컴포넌트의 전체 라이프 사이클 실행 순서에 따라 살펴보겠습니다.
- constructor() : 엘리먼트를 생성하여 props와 state를 설정할 때 실행
- 마운팅
- componentWillMount() : 컴포넌트 클래스가 DOM에 삽입되기 전 실행
- componentDidMount() : 컴포넌트 클래스가 DOM에 삽입되어 렌더링이 완료된 후 실행 - 갱신
- componentWillReceiveProps(nextProps) : 컴포넌트가 속성을 받기 직전 실행
- shouldComponentUpdate(nextProps, nextState) : 컴포넌트가 갱신되는 조건을 정의하여 리렌더링을 최적화할 수 있으며 bool 값 반환
- componentWillUpdate(nextProps, nextState) : 컴포넌트가 갱신되기 직전 실행
- componentDidUpdate(prevProps, prevState) : 컴포넌트가 갱신된 후 실행 - 언마운팅
- componentWillUnmount() : 컴포넌트를 DOM에서 제거하기 전 실행. 구독한 이벤트 제거나 다른 정리 작업 수행
표에서 보이는 this.forceUpdate()를 호출하여 컴포넌트를 리렌더링할 수도 있습니다. 이 메서드는 갱신을 강제합니다. 여러 가지 이유로 상태나 속성을 갱신하는 방법으로는 원하는 대로 리렌더링할 수 없는 경우 이 메서드를 사용할 수밖에 없습니다. 그러나 React 팀은 이 메서드를 사용하지 않을 것을 권고하는 데 이는 컴포넌트의 순수성을 해치기 때문입니다. (여기서 순수성이란 컴퓨터공학에서 말하는 일반적인 순수 함수의 의미와 동일합니다)
메서드 이름을 보면 각 메서드들이 언제 실행되는지 어느 정도 감이 옵니다. 실제로 동작이 되는지 예제를 통해 살펴보겠습니다.
2. 라이프사이클 이벤트 실행
라이프사이클 이벤트를 구현하려면 클래스에 메서드를 정의하여야 합니다. 이는 React의 규칙입니다. React는 이벤트 이름에 해당하는 메서드가 있는지 확인하고, 있으면 해당 메서드를 실행합니다. 존재하지 않는 경우 일반적인 흐름대로 진행됩니다. (자바스크립트 규칙과 동일하게 Case Sensitive 하게 이름을 작성해야 합니다)
아래는 모든 라이프사이클 이벤트를 호출하는 예제입니다.
[src/index.js]
import React from 'react'
import ReactDOM from 'react-dom'
import LifecycleEvent from './LifecycleEvent'
ReactDOM.render(
<LifecycleEvent />, document.getElementById('root')
);
[src/LifecycleEvent.js]
import React, { Component } from 'react'
import Logger from './Logger'
class LifecycleEvent extends Component {
constructor(props){
super(props)
this.launchClock()
this.state = {
counter: 0,
currentTime: (new Date()).toLocaleString()
}
}
launchClock(){
setInterval(() => {
this.setState({
counter: ++this.state.counter,
currentTime: (new Date()).toLocaleString()
})
},1000)
}
render() {
if(this.state.counter > 2) {
return
}
return <Logger time={this.state.currentTime}></Logger>
}
}
export default LifecycleEvent;
참고. render에서 counter가 2를 초과하면 return 하도록 코드를 작성하였기 때문에, 2초 후에 화면이 에러 화면으로 변경됩니다. 이는 콘솔 창에 초단위로 라이프사이클에 대한 로그가 찍히는 것을 멈추고 내용을 확인하기 위함이며, 이를 원하지 않을 경우 이 코드를 삭제하시면 됩니다.
[src/Logger.js]
import React, {Component} from 'react'
import ReactDOM from 'react-dom'
class Logger extends Component{
constructor(props){
super(props)
console.log('constructor')
}
componentWillMount(){
console.log('componentWillMount 실행')
}
componentDidMount(e){
console.log('componentDidMount 실행')
console.log('DOM node : ',ReactDOM.findDOMNode(this))
}
componentWillReceiveProps(newProps){
console.log('componentWillReceiveProps 실행')
console.log('새로운 속성 : ',newProps)
}
shouldComponentUpdate(newProps,newState){
console.log('shouldComponentUpdate 실행')
console.log('새로운 속성 : ',newProps)
console.log('새로운 상태 : ',newState)
return true
}
componentWillUpdate(newProps,newState){
console.log('componentWillUpdate 실행')
console.log('새로운 속성 : ',newProps)
console.log('새로운 상태 : ',newState)
}
componentDidUpdate(oldProps,oldState){
console.log('componentDidUpdate 실행')
console.log('이전 속성 : ',oldProps)
console.log('이전 상태 : ',oldState)
}
componentWillUnmount(){
console.log('componentWillUnmount')
}
render(){
return(
<div>{this.props.time}</div>
);
}
}
export default Logger;
- ReactDOM.findDOMNode() : 인자로 전달된 클래스의 DOM 노드에 대한 정보를 찾는 유틸리티 함수
코드가 길어 복잡해 보이지만, 실제 수행하는 동작은 간단합니다. 화면에 초단위로 시간이 표시되며, 콘솔 창에는 해당하는 라이프사이클 메서드가 호출된 결과가 출력됩니다.
마운팅 과정에서 호출된 메서드들이 순서대로 한 번 씩 호출되었고, 업데이트 과정에서 호출되는 componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, componentDidUpdate가 두 번 실행되는 것을 볼 수 있습니다.
마지막으로 컴포넌트가 DOM에서 제거되어 언마운트가 한 번 호출됨으로써 전체 라이프사이클이 종료되었습니다.
에러는 위에서 미리 언급했듯이 언마운팅되는 과정을 보기 위해서 렌더링 과정에 임의로 리턴을 시켜버렸기 때문에 발생하는 것으로 코드 상의 문제가 아닙니다.
그럼 이제 각각의 메서드들을 하나씩 살펴보겠습니다.
3. 마운팅 이벤트
마운팅 이벤트 유형은 모두 실제 DOM에 컴포넌트를 추가하는 것에 대한 이벤트입니다. 마운팅이 React 엘리먼트가 DOM에 노출되는 것이라 생각하면 이해가 쉬울 것입니다. 마운팅 이벤트에 해당하는 이벤트의 종류는 아래 두 가지이며, 이 이벤트들이 발생되기보다 먼저 constructor()가 실행됩니다.
(1) componentWillMount()
ReactDOM.render()를 호출하여 React 엘리먼트를 브라우저에 렌더링 하는 시점에 호출됩니다. 간단하게 React 엘리먼트를 실제 DOM 노드에 추가하는 시점이라고 생각하면 될 것입니다. 이 과정은 브라우저와 프론트엔드에서 이루어지고, 서버 렌더링 과정에서도 실행됩니다. 그러므로 어느 쪽에서도 동작할 수 있는 코드를 작성해야 합니다. 이 이벤트 메서드를 사용할 때 가장 좋은 점은 componentWillMount()에서 갱신하는 새로운 상태에 차이점이 있어도 재렌더링되지 않는다는 점입니다. 이 메서드 내에서 this.setState가 호출되더라도 render는 한 번만 호출되는 것입니다. 그러나 보통 componentWillMount는 고비용 작업이라고 합니다. 그러므로 이 내부에서 state를 변경하는 것은 고려해 볼 필요가 있습니다. 가능하면 constructor()에서 처리하는 것이 바람직합니다. 필요하다면 이 위치에서는 default state를 쓰기를 권장합니다.
보통 외부 API 연결 등 처음 실행 실행될 때만 할 수 있는 설정을 여기에서 처리하는데, 이런 설정은 최상위 컴포넌트(root)에서 이루어져야 하기 때문에 99%의 다른 컴포넌트는 이 메서드를 쓸 일이 거의 없습니다.
(2) componentDidMount()
초기 렌더링을 마친 후에 실행됩니다. 그러므로 이 시점에서는 컴포넌트의 엘리먼트가 실제 DOM에 반영되어 자식 엘리먼트를 포함한 모든 엘리먼트에 접근할 수 있습니다. 그렇기 때문에 다른 프론트엔드 프레임워크나 라이브러리와 통합하는 코드를 기재하거나, 서버에 xhr 요청을 보내는 코드를 작성하기 좋은 위치입니다. (또한 브라우저에서만 한 번 실행되고 서버 렌더링에서는 실행되지 않으므로 XHR 요청처럼 브라우저에서만 실행해야 하는 코드를 구현할 때 편리합니다)
이 메서드에서는 자식 엘리먼트로 접근할 수 있습니다. 이런 경우 자식 컴포넌트의 componentDidMount()가 부모의 것보다 먼저 호출됩니다.
isMounted()
ajax 요청하고 그 결과를 setState 하는 패턴이 자주 발생하게 되는데, 이때 ajax의 응답이 왔을 때 컴포넌트가 이미 unmount 되었다면 setState나 forceUpdate 호출 시 에러가 발생하게 됩니다. 그래서 이를 방어하기 위하여 isMounted()라는 메서드가 제공됩니다.
[isMounted()]
componentDidMount(){
request.get('/path', response => {
if(this.isMounted()){
this.setState({data : response.body.data});
}
});
}
4. 업데이팅 이벤트
마운팅 이벤트가 React를 다른 프레임워크나 라이브러리, 데이터 저장소 등과 연결하는 데 사용한다면, 업데이팅 이벤트는 컴포넌트를 갱신하는 것과 관련이 있습니다. 갱신에 대한 이벤트이므로 초기 렌더링 시에는 호출되지 않습니다.
처음에 보았던 그림에서 업데이팅 이벤트 부분만 잘라서 가져오면 아래와 같습니다.
네 가지 이벤트 메서드로 구성되어 있는 것을 볼 수 있습니다. (render 제외)
(1) componentWillReceiveProps(newProps)
컴포넌트가 새로운 속성을 전달받을 때 실행됩니다. 이 단계를 들어오는 속성의 전환이라고 합니다. 이 메서드는 새로운 속성을 받아오는 시점에 끼어들어, render()를 호출하기 전에 일부 메서드를 추가할 수 있도록 해줍니다. 특정 props가 바뀔 때 그에 따른 변경사항을 적용하기 위해 사용하는 것입니다. 반드시 속성을 인자로 받아야 합니다. 새로운 속성을 전달받고 다시 렌더링이 이뤄지기 전, 새로운 속성에 따라 상태를 변경하려는 경우에 유용합니다. 마운팅 이벤트 때와 마찬가지로 내부에서 setState() 메서드를 호출해도 추가로 다시 렌더링 하지는 않습니다.
예를 들어, 위와 같은 캔버스 요소가 있고, 여기에 this.props.percent값을 가지고 원형 그래프를 그린다고 가정해봅니다. 새 props를 받을 때 percent의 값이 바뀌면 그래프를 다시 그리게 됩니다.
단, 유의해야 할 점은 새로운 속성이 반드시 새로운 속성인 것은 아니라는 점입니다. 현재 속성의 값과 동일한 값일 수도 있습니다. React는 속성 값이 변경되었는지 알 수 있는 방법은 없으므로 재렌더링이 이루어질 때마다 실행됩니다. 그래서 반드시 DOM의 변경이 일어나는 것 역시 아닙니다. 실제 DOM에서 무엇을 갱신할지, 갱신을 수행할 것인지는 shouldComponentUpdate()와 reconciliation(보정) 과정에 위임되어 있습니다. (이에 대해 자세히 알고 싶다면 짐 스프로크 - (A => B) !=> (B => A)에서 확인해보시기 바랍니다) 그러므로 비교할 대상이 없는 초기 렌더링 시에는 이 메서드는 호출되지 않습니다.
(2) shouldComponentUpdate()
props 또는 state를 변경했을 때 리렌더링 여부를 지정할 수 있는 메서드입니다. 렌더링은 새로운 속성이나 상태가 전달된 후에 이루어지게 됩니다(forceUpdate() 호출 시에는 호출되지 않습니다) 이 메서드가 false를 반환하도록 구현하면 React가 다시 렌더링 되지 않도록 할 수 있습니다. 변경된 부분이 없고, 불필요한 성능 저하를 피하고 싶을 때 유용한 방법입니다. (디폴트는 true입니다) 그러나 실제로 이 메서드는 사용하는 것을 지양하기를 권고합니다. 실제로 성능에 대한 이점보다도 단점이 더 많기 때문인데 이에 대한 자세한 내용은 http://jamesknelson.com/should-i-use-shouldcomponentupdate/ 에서 확인해보시기 바랍니다. 프로젝트 성능을 최적화할 때, 상황에 맞는 알고리즘을 작성하여 리렌더링을 방지하고자 할 때 사용합니다.
(3) componentWillUpdate()
새로운 속성이나 상태를 받은 후 렌더링 직전에 호출됩니다. 갱신 전에 필요한 준비 작업을 여기서 처리하는데, this.setState()를 사용하는 것은 피하는 것이 좋습니다. 컴포넌트를 갱신하는 중에 다시 갱신하도록 한다면 무한루프에 빠질 수 있기 때문입니다. 그러므로 setState는 componentWillReceiveProps()에서 사용하여야 합니다. shouldComponentUpdate()가 false를 반환하게 되면 이 메서드는 실행되지 않습니다. 사실 componentWillReceiveProps와 기능적인 차이가 거의 없지만, componentWillReceiveProps은 props가 전달되지 않으면 실행되지 않습니다. 그러므로 업데이트에
(4) componentDidUpdate()
컴포넌트의 갱신 결과가 실제 DOM에 반영된 직후에 실행됩니다. 컴포넌트가 갱신된 후에 DOM이나 그 외의 요소를 다루는 코드를 작성해야 할 때 유용합니다. 이 시점에는 DOM에 모든 변경 사항이 반영되어 있기 때문입니다. 네트워크 요청을 수행하기 좋은 위치입니다. 현재 state/props와 이전 state/props를 비교하여 불필요한 네트워크 요청을 피할 수 있기 때문입니다. prevProps, prevState를 사용하여 이전의 데이터에 접근할 수 있습니다.
5. 언마운팅 이벤트
React에서 언마운팅이란 DOM에서 요소를 분리하거나 제거하는 것을 말합니다.
(1) componentWillUnmount()
DOM에서 컴포넌트가 제거되기 직전에 호출됩니다. 직접 생성한 DOM이 있다면 여기에서 제거작업을 해주어야 합니다. 또는 외부 네트워크 리퀘스트들을 취소하거나 연결했던 컴포넌트의 이벤트 리스너들을 제거할 수 있습니다. 당연하겠지만 setState는 사용할 수 없습니다.
추가. React 16 버전 이후로 추가된 라이프사이클 메서드
1. componentDidCatch()
오류나 예외처리를 위해 사용할 수 있습니다. 이 메서드가 선언된 컴포넌트는 하위 컴포넌트에서 발생하는 오류를 모두 처리할 수 있으므로 오류 경계라고 부릅니다.
componentDidCatch(error, errorInfo)
error는 발생한 오류이고, errorInfo는 객체가 전달되는데, componentStack이라는 키로, 오류가 발생한 컴포넌트에 대한 스택 트레이스를 제공합니다.
자세한 내용은 공식문서에서 확인해보시기 바랍니다.
2. getDerivedStateFromProps()
props로 받아 온 값을 state에 동기화시키는 용도로, 컴포넌트를 마운트 하거나 props를 변경할 때 호출합니다.
3. getSnapshotBeforeUpdate()
render 호출 후 DOM에 변경 사항을 적용하기 전에 호출합니다. componentDidUpdate의 세 번째 파라미터인 snapshot 값으로 전달받을 수 있어, 주로 업데이트하기 직전의 값을 참고할 일이 있을 때 활용됩니다. 예를 들어 스크롤바 위치 유지와 같은 것을 들 수 있습니다.
이런 새로운 라이프사이클 메서드가 추가된 이유는 React가 아래와 같이 라이프 사이클 메서드를 변경하고자 하는 의도입니다.
기존의 라이프 사이클 중 일부를 추가되는 메서드들로 대체하여, 기존의 라이프사이클 메서드를 사용함으로써 발생했던 성능상의 문제나, 잘못된 사용으로 인한 오류를 방지하기 위함으로 보입니다.
Conclusions
여기까지 컴포넌트의 라이프사이클에 대한 메서드를 살펴보았습니다. 라이프사이클 메서드는 렌더링 하는 데에 영향을 미치므로 최소한으로, 주의해서 써야 한다는 것을 유념해야 할 것입니다.
라이프사이클 메서드들은 각각이 이벤트입니다. React에서 이벤트를 어떻게 다루는 가도 중요한 사항이므로, 다음 글에서는 React에서 이벤트를 다루는 방법과 폼을 다루는 방법을 살펴보도록 하겠습니다.
[references]
「리액트 교과서」 - 아자트 마르단
react lifecycle method how and when to use them
짐 스프로크 - (A => B) !=> (B => A)
'Front-end Framework > React (Reactjs)' 카테고리의 다른 글
6. React의 이벤트 처리 (0) | 2020.10.19 |
---|---|
4. 컴포넌트 (0) | 2020.10.19 |
3. 리액트의 문법 (+JSX) (0) | 2020.10.19 |
2. React 개발 설정 (0) | 2020.10.19 |
1. React? (0) | 2020.10.19 |
댓글