티스토리 뷰

프로그래밍/React

[문법]Life Cycle API

개발자 카니 2019. 2. 4. 06:36

 React에는 컴포넌트가 로드될 때, 업데이트 될 때, 제거될 때 자동으로 호출되는 API가 존재합니다. 이를 React의 Life Cycle API라고 합니다.


초기 생성 API


 브라우저에 컴포넌트가 나타나기 전에 호출되는 API에 대해 알아보겠습니다.


 - constructor(props) : 컴포넌트 생성자 함수로 컴포넌트가 새로 만들어 질 때마다 호출됩니다.

 - componentWillMount() : 컴포넌트가 화면에 나타나기 직전에 호출되는 API입니다. 주로 서버를 호출하는 용도로 사용되었는데 이 API를 더 필요하지 않다고 판단하여 React v16.3에서 더 이상 사용하지 않게 되었습니다. v16.3 이후 UNSAFE_componentWillMount()로 사용됩니다. 기존에 여기서 처리하던 부분은 constructor나 componentDidMount에서 처리할 수 있습니다.

 - componentDidMount() : 컴포넌트가 화면에 나타난 후에 호출되는 API입니다. 주로 D3, masonry처럼 DOM을 사용하는 외부 라이브러리와 연동을 하거나, 데이터를 axios, fetch 등을 통해 ajax 요청을 하거나, DOM 속성(스크롤 설정, 크기 등)을 읽거나 직접 변경하는 작업을 합니다.


업데이트 API


 로드된 컴포넌트가 props나 state의 변화로 인해 변경될 경우 호출되는 API에 대해 알아보겠습니다.


 - componentWillReceiveProps(nextProps) : 자식 Component가 부모 Component로부터 새로운 props를 받게 되었을 때 호출됩니다. 주로 state가 props에 따라 변해야 하는 로직을 작성합니다. 여기서 this.props를 사용하게 되면 변경되기 전의 props이고 변경된 props는 매개변수로 받은 nextProps입니다. 이 API 또한 React v16.3에서 더 이상 사용하지 않게 되었습니다. v16.3 이후 UNSAFE_componentWillReceiveProps()로 사용됩니다. 기존에 여기서 처리하던 부분은 상황에 따라 getDerivedStateFromProps에서 처리할 수 있습니다.

 - static getDerivedStateFromProps(nextProps, prevState) : React v16.3 이후 추가된 API입니다. props로 받아온 값을 state로 동기화하는 작업을 해주어야할 경우 사용됩니다. 이 경우 setState로 state 값을 설정하는 것이 아니라 객체 형태로 state 값을 리턴시킵니다. 업데이트할 값이 없다면 null을 리턴해줍니다.

 - shouldComponentUpdate(nextProps, nextState) : 컴포넌트 최적화 작업에서 유용한 API입니다. React에서 Virtual DOM을 설명하던 중에 React는 변화가 발생한 DOM만 찾아서 변화시켜주기 때문에 성능이 나온다고 설명했었는데, 변화가 발생한 부분을 감지하기 위해서는 일단 한 번 Virtual DOM에 컴포넌트를 그려야합니다. 이 때, 자식 Component가 직접적으로 변하지는 않았어도 부모 Component가 리렌더링되면 자식 컴포넌트도 다시 렌더링되면서 Virtual DOM에 그려지게 됩니다. 기존 DOM과 비교해서 변화가 발생했다면 실제 DOM이 변경되고 그렇지 않다면 아무 작업도 하지 않습니다. 그저 Virtual DOM에 렌더링될 뿐인데 이 작업의 부하는 크지는 않습니다. 하지만 많은 렌더링 작업이 일어나고 있다면 아무래도 CPU 자원을 사용하고 있다보니 부하가 발생합니다. 이 부하조차 줄이기 위해 Virtual DOM에 리렌더링하는 작업조차 이 API를 사용하여 막을 수 있습니다. 기본적으로 true값을 반환하고 우리가 조작하는 상황에 따라 false를 반환함으로써 render() 함수를 호출하지 않게 되고 Virtual DOM에 리렌더링하는 작업을 막을 수 있습니다.

 - componentWillUpdate(nextProps, nextState) : shouldComponentUpdate에서 true가 반환되었을 때 호출됩니다. 주로 애니메이션 효과를 초기화하거나, 이벤트 리스너를 없애는 작업을 합니다. 이 함수 다음에 render 함수가 호출됩니다. React v16.3 이후 사용되지 않고 getSnapshotBeforeUpdate라는 함수로 이 기능을 대체합니다.

 - getSnapshotBeforeUpdate(prevProps, prevState) : 이 API는 render 함수와 componentDidUpdate 사이에 호출됩니다. DOM 변화가 일어나기 직전의 DOM 상태를 가져오고, 여기서 그 상태 값(snapshot)을 반환해서 componentDidUpdate의 3번째 매개변수로 받아올 수 있습니다. 예를 새로운 데이터가 상단에 추가되었을 때 현재 위치의 스크롤바를 유지하고 싶을 때, 이 API에서 현재 스크롤바 위치를 구해 그 값을 매개변수로 반환해주면, componentDidUpdate에서 그 인자를 받아 스크롤바 위치 처리를 해주는 식으로 쓸 수 있습니다. render -> getShapshotBeforeUpdate -> 실제 DOM 업데이트 -> componentDidUpdate 순으로 진행됩니다.

 - componentDidUpdate(prevProps, prevState, snapshot) : render 함수가 호출된 후에 호출됩니다. 이 시점에 this.props와 this.state는 변경된 상태입니다. 매개 변수로 받은 prevProps와 prevState는 말그대로 변경 이전의 props와 state 값입니다.


제거 API


 컴포넌트가 더 이상 사용되지 않을 경우 호출되는 API에 대해 알아보겠습니다.


 - componentWillUnmount() : 주로 이벤트 제거, setTimeout의 clearTimeout를 통한 제거, setInterval의 clearInterval을 통한 제거 등의 작업을 합니다. 또한 외부 라이브러리를 사용했다면 이 부분에서 해당 라이브러리의 dispose 기능을 실행하면 됩니다.


 Life Cycle API를 테스트 해보겠습니다. src/StopWatch.js 파일을 다음과 같이 수정해 보세요. 기존 코드에서 주석 범위 안에 Life Cycle API 부분만 추가하면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// StopWatch.js
import React, { Component } from "react";
 
class StopWatch extends Component {
  // class fields
  state = {
    sec: 0,
    buttonFlag: true,
    intervalFunction: null
  };
 
  /* Life Cycle API 영역 시작 */
  constructor(props) {
    super(props);
    console.log("constructor");
  }
 
  componentWillMount() {
    console.log("componentWillMount");
  }
 
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    if (nextState.sec % 2 === 0) {
      console.log(`${nextState.sec} % 2 === 0`);
      return false;
    }
    return true;
  }
 
  componentWillUpdate(nextProps, nextState) {
    console.log("componentWillUpdate");
  }
 
  componentDidUpdate(prevProps, prevState) {
    console.log("componentDidUpdate");
  }
 
  /* Life Cycle API 영역 끝 */
 
  // stop watch 로직
  timer = () => {
    // 비구조화 할당 사용( { sec } 부분 )
    this.setState(({ sec }) => ({
      sec: sec + 1
    }));
  };
 
  // start 버튼 클릭
  start = () => {
    this.setState({
      sec: 0,
      buttonFlag: false,
      intervalFunction: setInterval(this.timer, 1000)
    });
  };
 
  // stop 버튼 클릭
  stop = () => {
    clearInterval(this.state.intervalFunction);
 
    this.setState({
      buttonFlag: true
    });
  };
 
  render() {
    return (
      <React.Fragment>
        <h1>Stop Watch</h1>
        <div>
          <b>{this.state.sec}</b>
          <span></span>
          {this.state.buttonFlag ? (
            <button onClick={this.start}>start</button>
          ) : (
            <button onClick={this.stop}>stop</button>
          )}
        </div>
      </React.Fragment>
    );
  }
}
 
export default StopWatch;
cs



 다음과 같은 실행 결과가 나옵니다. state 내의 sec가 0, 2, 4, ... 즉, 짝수일 경우에는 컴포넌트를 변화시키지 않으므로 shouldComponentUpdate까지만 호출되고 실제 변화는 발생하지 않습니다. sec가 5에서 6으로 변할 때 쯤 stop 버튼을 눌러서 state안에 buttonFlag 값이 바뀌었기 때문에 다음과 같이 마지막에 컴포넌트의 변화가 일어났습니다.


에러 처리 API


 render 함수 안에서 에러가 발생한다면, react 앱의 기능이 정지해버립니다. 이런 현상을 막기 위해 사용하는 API에 대해 알아보겠습니다.


 - componentDidCatch(error, info) : render 함수 안에서 에러가 발생했을 때, 호출됩니다. state에 error라는 변수를 만들고 해당 API가 호출되었을 때 setState로 error 값을 변경하여 render 함수 쪽에서 다시 에러용 UI를 보여주게 하면됩니다. 이 API를 사용하실 때 주의할 점은 Component 자신의 render 함수에서 발생하는 에러는 잡을 수 없고 자식 Component 내부의 render 함수에서 발생하는 에러를 잡을 수 있습니다. 


 src/StopWatch.js를 다음과 같이 수정해봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// StopWatch.js
import React, { Component } from "react";
 
// Error 생성용 자식 Component
const ErrorMaker = () => {
  throw new Error("Error Occured!!");
  return <div />;
};
 
class StopWatch extends Component {
  // class fields
  state = {
    sec: 0,
    buttonFlag: true,
    intervalFunction: null,
    error: false
  };
 
  /* Life Cycle API 영역 시작 */
  constructor(props) {
    super(props);
    console.log("constructor");
  }
 
  componentWillMount() {
    console.log("componentWillMount");
  }
 
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    if (nextState.sec % 2 === 0) {
      console.log(`${nextState.sec} % 2 === 0`);
      return false;
    }
    return true;
  }
 
  componentWillUpdate(nextProps, nextState) {
    console.log("componentWillUpdate");
  }
 
  componentDidUpdate(prevProps, prevState) {
    console.log("componentDidUpdate");
  }
 
  // render에서 에러 발생시 호출
  componentDidCatch(error, info) {
    console.log("componentDidCatch");
    this.setState({
      error: true
    });
  }
 
  /* Life Cycle API 영역 끝 */
 
  // stop watch 로직
  timer = () => {
    // 비구조화 할당 사용( { sec } 부분 )
    this.setState(({ sec }) => ({
      sec: sec + 1
    }));
  };
 
  // start 버튼 클릭
  start = () => {
    this.setState({
      sec: 0,
      buttonFlag: false,
      intervalFunction: setInterval(this.timer, 1000)
    });
  };
 
  // stop 버튼 클릭
  stop = () => {
    clearInterval(this.state.intervalFunction);
 
    this.setState({
      buttonFlag: true
    });
  };
 
  render() {
    // 다음과 같이 에러 발생시에 앱이 중지되는 것을 방지한다.
    if (this.state.error) return <h1>에러 발생!!</h1>;
 
    return (
      <React.Fragment>
        <h1>Stop Watch</h1>
        {this.state.sec === 3 && <ErrorMaker />}
        <div>
          <b>{this.state.sec}</b>
          <span></span>
          {this.state.buttonFlag ? (
            <button onClick={this.start}>start</button>
          ) : (
            <button onClick={this.stop}>stop</button>
          )}
        </div>
      </React.Fragment>
    );
  }
}
 
export default StopWatch;
cs


 ErrorMaker라는 인위적으로 에러를 생성하는 컴포넌트를 함수형 컴포넌트 형식으로 만들었습니다. StopWatch 내부에서는 state의 인자로 error라는 값이 추가되었고 Life Cycle API 영역에서 마지막에 componentDidCatch가 추가되었습니다. render 함수에서는 앱 중지를 방지하는 코드가 추가되었고, state의 sec가 3이 되었을 때, ErrorMaker를 생성하도록 했습니다. 결과는?



 다음과 같은 화면이 나타날 것입니다. 이 화면은 에러가 발생했을 때 개발 모드에서 제공해주는 기능으로 오류가 나타난 위를 표시해 줍니다. 오른쪽 상단의 x버튼을 클릭하면 이 내용들은 사라지게 됩니다.


 x버튼을 누르면 다음과 같이 에러 발생시에 정해둔 UI가 나타납니다. 만약 에러 처리를 해두지 않는다면 render 내부에서 예기치 않은 에러가 발생했을 때, 앱의 기능이 정지하고 빈 화면만이 나타날 것입니다. 그래서 렌더링 부분의 에러 처리는 상당히 중요합니다. 주로 다음과 같은 이유로 에러가 자주 발생합니다.


 - 존재하지 않는 함수를 호출하려고 할 때(props로 받은 줄 알았던 함수를 호출할 때)

 - 배열이나 객체를 받을 줄 알았는데 존재하지 않을 때


1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
    // 에러를 유발시키는 유형(실제로 props에서 onClick(), object, array를 받지 않았다고 가정)
    /*
        this.props.onClick();
        this.props.object.value;
        this.props.array.length;
    */
 
    // 다음과 같은 분기문을 추가하여 에러를 방지한다.
    if!this.props.object || this.props.array || this.props.array.length === 0 ) return null;
 
    // ...
}
cs


 혹시 누락이 발생할 것 같으면 다음과 같이 defaultProps를 이용해 해당 값들을 초기화해서 사용하면 됩니다.


1
2
3
4
5
6
7
8
 
class Test extends Component {
    static defaultProps = {
        onClick: () => console.warning("onClick is not defined"),
        object: {},
        array: []
    }    
}
cs


 이런 대표적인 에러 말고도 다양한 에러가 발생할 수 있습니다. 이 때는 componentDidCatch를 사용하면 됩니다.


References


[Velopert 블로그 LifeCycle API] https://velopert.com/3631

'프로그래밍 > React' 카테고리의 다른 글

[예제]간단한 가계부2 - 배열관련 처리1  (0) 2019.02.08
[예제]간단한 가계부1 - input 상태 관리  (0) 2019.02.06
[문법]props와 state  (0) 2019.01.31
[문법]JSX  (0) 2019.01.29
React 프로젝트 시작하기  (0) 2019.01.29
댓글