본문 바로가기
React

React - Life Cycle

by 양털의매력 2020. 3. 24.
velopert님의 https://velopert.com/3631와 React 공식문서를 참고하였습니다.

 

리액트의 Life Cycle API에 대해서 정리하려고 한다. 이 API는 리액트의 컴포넌트가 브라우저에서 나타나고, 사라지고, 업데이트 될 때 호출되는 API이다. 각 시점에서 호출될 때 내가 실행하고 싶은 기능이나 코드가 동작하도록 할 수 있다.

 

React  v16.3 이후 Life Cycle  (ZeroCho님 블로그)

 

리액트의 라이프사이클은 크게 mount, update, unmount의 3가지로 나눌 수 있다. 예제를 만들어서 각 시점에 어떻게 호출되는지 알아보려고 한다.

 

$ npx create-react-app react-life-cycle

 


Mount

컴포넌트가 만들어지고 렌더링 되는 과정이다. 여기에는 다음과 같은 method가 순서대로 호출된다.

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

 

constructor

  constructor(props) {
    super(props);
    console.log("constructor");
    this.state = {
      text: "",
      value: 0
    };
    this.clickBtn = this.clickBtn.bind(this);
  }

 

컴포넌트의 생성자 함수이다. 컴포넌트가 새로 만들어질 때마다 호출된다.

 

 

 

static getDerivedStateFromProps

  static getDerivedStateFromProps(nextProps, prevState) {  // 인자로 받은 props와 현재 state를 받는다
    console.log("getDerivedStateFromProps");
    console.log("prevState", prevState);
    console.log("nextProps", nextProps);
    if (nextProps.text !== prevState.text) {
      return { text: nextProps.text, value: 1 };
    }
    return null;
  }

 

render 메소드를 호출하기 직전에 호출된다. mount 시에는 props로 받아온 값을 state로 동기화해주는 경우에 사용된다. 기존에는 componentWillReceiveProps로 사용되었다.

 

특정 props가 바뀔 때, 설정하고 싶은 state의 값을 리턴하는 형태로 사용하고, null을 리턴하면 따로 업데이트 할 것이 없다는 의미이다.

 

주의할점은 렌더링될 때마다 렌더링 직전에 이 메소드가 호출된다는 점이다.

 

 

render

  render() {
    console.log("render");
    return (
      <div>
        {this.state.text}
        {this.state.value}
        <button onClick={this.clickBtn}>클릭</button>
      </div>
    );
  }

 

render 메소드가 실행되면 props와 state의 값을 활용하여 브라우저에 렌더링이 된다. 이때 render는 순수함수여야 한다. 즉, 컴포넌트의 state를 변경하지않고 호출될 때마다 동일한 결과를 반환해야하고, 브라우저와 직접 상호작용 해서는 안된다. 상호작용이 필요할 경우, render가 아닌 다른 life cycle 메소드 내에서 수행해야 한다.

 

 

componentDidMount

  componentDidMount() {
    console.log("componentDidMount");
  }

 

이 메소드는 컴포넌트가 render된 이후(마운트된 이후)에 호출된다. 주로 외부 api 연동을 하거나, 서버에 데이터를 요청하기 위한 ajax 요청, DOM의 속성을 읽거나 변경 작업 등의 동작을 여기에 넣는다. 컴포넌트가 mount되고 한번만 호출 된다. 

 

Mount에 관한 코드와 실행 순서를 직접 눈으로 확인하기 위해서 다음과 같이 코드를 작성하고 실행시켜보았다.

 

import React, { Component } from "react";

export default class Test1 extends Component {
  constructor(props) {
    super(props);
    console.log("constructor");
    this.state = {
      text: "",
      value: 0
    };
    this.clickBtn = this.clickBtn.bind(this);
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("getDerivedStateFromProps");
    console.log("prevState", prevState);
    console.log("nextProps", nextProps);
    if (nextProps.text !== prevState.text) {
      return { text: nextProps.text, value: 1 };
    }
    return null;
  }

  clickBtn() {
    console.log("버튼실행");
    this.setState({ value: 3 });
  }

  componentDidMount() {
    console.log("componentDidMount");
  }

  render() {
    console.log("render");
    return (
      <div>
        {this.state.text}
        {this.state.value}
        <button onClick={this.clickBtn}>클릭</button>
      </div>
    );
  }
}

 

props로는 hello World 라는 text를 내려받고 있다. 처음 실행시켰을 때 화면 및 console의 내용이 다음과 같이 나온다.

 

 

콘솔을 보면 메소드들이 위에서 언급한 순서대로 호출되는 것을 알 수 있다. 

 

처음에 constructor가 호출되고, getDerivedStateFromProps가 호출된다. 여기에서 현재 state의 값과 props로 받은 hello world를 확인할 수 있고, state의 text를 render전에 hello world로 설정해주었다. 이때 value값은 props가 아닌 그냥 1로 설정을 같이 해주었다. 그 다음 render 함수가 실행이 되고 브라우저에 렌더링 된 이후에 componentDidMount가 실행이 되는 것을 확인할 수 있다. 

 

버튼의 경우,  getDerivedStateFromProps가 렌더링될 때마다 실행되는 것을 확인해보기 위하여 넣었다. 버튼은 클릭 시에 setState를 호출하여 state의 value 값을 3으로 바꾸어준다. 실행시킨 결과는 다음과 같다.

 

 

클릭 함수가 실행이 되면 setState가 실행이되고, state의 value 값이 3으로 설정된다. 이후에 render 실행전에 getDerivedStateFromProps가 실행이 되고 이때 state 값은 현재 state의 값인 것을 알 수 있다. if문에서 현재 state의 text 값과 받은 props의 값이 같기 때문에 null을 반환하였고 따라서 value가 1로 다시 바뀌지 않는 것을 알 수 있다. 그리고 이후에 render 함수가 다시 실행되었다. componentDidMount는 해당 컴포넌트가 다시 마운트 되는 것이 아니기 때문에 호출되지 않는 것을 알 수 있다.

 

 


Update

컴포넌트 업데이트는 props가 바뀌거나, state가 바뀔 때 결정된다. 다음과 같이 3가지 경로가 있다.

  • New props (props가 바뀔 때)
  • setState() (state가 바뀔 때)
  • forceUpdate() (강제로 업데이트)

 

New Props

props가 바뀔 때 다음과 같은 method가 순서대로 호출된다.

  • static getDerivedFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

setState

setState는 현재 state를 변화시킬 때 사용하는 메소드이다. 호출하면 해당 컴포넌트와 자식 컴포넌트들이 바뀐 state로 다시 렌더링 되야한다고 알려주는 것이다. shouldComponentUpdate가 false를 반환하지않는다면 항상 리렌더링이 일어나도록 한다. setState로 인해 호출되는 life cycle method들은 위에서 props가 바뀔 때와 비교해서 getDerivedFromProps를 제외한 동일한 method를 호출한다.

 

static getDerivedStateFromProps

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("getDerivedStateFromProps");
    console.log("prevState", prevState);
    console.log("nextProps", nextProps);
    if (nextProps.text !== prevState.text) {
      return { text: nextProps.text, value: 1 };
    }
    return null;
  }

props의 변화로 컴포넌트가 업데이트될 때에도 마찬가지로 render전에 호출된다.

 

 

 

shouldComponentUpdate

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.text === nextProps.text) {
      return false;
    }
  }

 

리액트에서는 변화가 발생하는 부분만 업데이트하여 브라우저에 렌더링을 해준다. 변화가 생긴 부분을 감지하기 위해서 리액트는 virtual DOM에 먼저 렌더링을 하고, 바뀐 부분을 감지하여 그 부분만 업데이트를 해주는 것이다. 렌더링이 된다는 것은 render 메소드가 호출된다는 이야기이다.

 

즉, render가 호출되면 컴포넌트가 업데이트 되지않더라도 (변화가 없더라도) virtual DOM에 렌더링하는 작업이 이루어진다는 것이다. 이때 변화가 없기 때문에 실제 DOM 조작은 하지않는다. 이 작업은 그렇게 부하가 많은 작업은 아니지만 컴포넌트가 무수히 많아지면 성능에 영향을 줄 수 있다. 

 

그래서 이 작업량을 줄이고 렌더링 작업을 최적화하기 위해 이 메소드를 사용한다.

 

이 메소드의 기본 default 반환 값은 true이다. 따로 컴포넌트에 작성하지않으면 항상 true로 설정되어있다. 따로 작성하여 false를 반환하도록 하면 render 함수를 호출하지 않는다. 인자로 nextProps와 nextState 값을 받기때문에 이 값들을 적절히 활용하여 false를 반환하도록 해서 virtual DOM이 불필요하게 리렌더링되는 것을 줄여서 렌더링을 최적화할 수 있다.

 

 

render

  render() {
    console.log("render");
    return (
      <div>
        {this.state.text}
        {this.state.value}
        <button onClick={this.clickBtn}>클릭</button>
      </div>
    );
  }

 

앞서 shouldComponentUpdate가 false를 반환하지않는다면 render 메소드가 호출되게 된다.

 

 

 

getSnapshotBeforeUpdate

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    if (this.state.value !== prevState.value) {
      console.log(document.querySelector(".value").innerText);
      return { value: 5 };
    }
  }

 

이 메소드는 render가 호출되고나서 실제 DOM에 변화가 발생하기 직전에 호출된다. 실제 DOM의 변화가 일어나기 직전의 DOM의 상태를 가져오는 등의 동작을 여기서 구현한다. 예를들어 현재 스크롤의 위치를 받아와 컴포넌트가 업데이트되더라도 현재 스크롤의 위치를 유지하는 등의 동작을 구현할 수 있다. 

 

이 메소드는 스냅샷 값을 반환하는데 이 반환값은 이 이후에 호출되는 componentDidUpdate에서 스냅샷 값으로 받아와 사용할 수 있다.

 

 

componentDidUpdate

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate");
    console.log("업데이트 후, 이전 props", prevProps);
    console.log("업데이트 후, 이전 state", prevState);
    console.log("snapshot", snapshot);
  }

 

render가 호출되어 렌더링이 이루어지고 난 다음에 호출된다. 인자로 3개를 받는데 업데이트 이전의 props, state를 조회할 수 있고, 마지막 인자에는 위에서 getSnapshotBeforeUpdate로 반환된 스냅샷 값을 받아올 수 있다.

 

이번에 update 과정을 알아보기 위해서 코드를 다음과 같이 바꾸고 실행시켜보았다.

 

import React, { Component } from "react";

export default class Test1 extends Component {
  constructor(props) {
    super(props);
    console.log("constructor");
    this.state = {
      text: "",
      value: 0
    };
    this.clickBtn = this.clickBtn.bind(this);
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("getDerivedStateFromProps");
    console.log("prevState", prevState);
    console.log("nextProps", nextProps);
    if (nextProps.text !== prevState.text) {
      return { text: nextProps.text, value: 1 };
    }
    return null;
  }

  clickBtn() {
    console.log("버튼실행");
    this.setState({ value: 2 });
  }

  componentDidMount() {
    console.log("componentDidMount");
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    if (this.props.text === nextProps.text) {
      return false;
    }
    // if (this.state.value === nextState.value) {
    //   return false;
    // }
    return true;
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    if (this.state.value !== prevState.value) {
      const prevValue = document.querySelector(".value").innerText;
      console.log("dom 업데이트 직전 ", prevValue);
      return prevValue;
    }
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate");
    console.log("업데이트 후, 이전 props", prevProps);
    console.log("업데이트 후, 이전 state", prevState);
    console.log("snapshot", snapshot);
  }

  render() {
    console.log("render");
    return (
      <div>
        {this.state.text}
        <div className="value">{this.state.value}</div>
        <button onClick={this.clickBtn}>클릭</button>
      </div>
    );
  }
}

 

버튼 실행 시, setState를 통해 state의 value가 2로 바뀌도록 하였고, 이번 경우에는 shouldComponentUpdate에서 현재 props와 nextProps를 비교하여 업데이트 이후에 props의 변화가 없을 경우에 어떻게 호출되는지 알아보았다. 결과는 다음과 같다.

 

 

componentDidMount까지는 mount 과정이고 이후에 버튼을 클릭하여 setState를 실행하였다. 위에서 언급한대로 method가 순서대로 호출되는데, props가 바뀌지 않았기 때문에 shouldComponentUpdate 이후 method들은 호출이 되지 않는 것을 알 수 있다. 렌더링도 그대로 1을 보여주는 것을 알 수 있다.

 

이번에는 shouldComponentUpdate를 다음과 같이 변경하고 실행시켜보았다.

 

  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    // if (this.props.text === nextProps.text) {
    //   return false;
    // }
    if (this.state.value === nextState.value) {
      return false;
    }
    return true;
  }

 

결과는 다음과 같다.

 

 

mount 이후 버튼을 클릭하면 setState로 인해 value가 2로 바뀐다. shouldComponentUpdate에서 state 비교를 통해 true를 반환하기 때문에 render를 호출하게 되고 이후에 getSnapshotBeforeUpdate가 호출되어 실제 DOM이 변화하기 전에 DOM에 그려져있는 값을 읽어오는 것을 알 수 있다. 그리고 DOM의 변화가 일어나고, componentDidUpdate를 호출하여 업데이트 되기 전의 props와 state를 조회할 수 있고, getSnapshotBeforeUpdate로 반환되는 snapshot 값을 읽는 것을 볼 수 있다.

 

 

 

forceUpdate

forceUpdate 메소드는 컴포넌트가 render를 호출하도록하는 메소드이다. shouldComponentUpdate를 무시하고 건너뛰는데 여기서 자식 컴포넌트들은 통상적인 life cycle 메소드들이 실행된다.

 

 

위에서 shouldComponentUpdate에서 props가 변하지 않으면 render가 호출되지 않도록 하는 경우가 있었는데 이 경우에서 forceUpdate 를 호출하여 render를 강제로 호출해보았다. 

 

코드를 다음과 같이 바꿔주었다. 

 

  clickBtn() {
    console.log("버튼실행");
    this.setState({ value: 2 }, () => {
      console.log("setState 콜백 실행");
      this.forceUpdate();
    });
  }
  
  ...
  
    getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    if (this.state.value !== prevState.value) {
      const prevValue = document.querySelector(".value").innerText;
      console.log("dom 업데이트 직전 ", prevValue);
      return prevValue;
    }
    return document.querySelector(".value").innerText;
  }

 

setState는 두번째 인자로 callback을 받을 수 있는데 callback 함수는 life cycle들이 호출된 이후에 실행된다. 

 

getSnapshotBeforeUpdate에 리턴 값을 추가해준 이유는 shouldComponentUpdate 가 true를 반환한다고 가정하면 setState로 state가 바뀌고 이전 state값과 다르기 때문에 if문이 실행되고 DOM 업데이트 직전의 값을 알수가 있다. 

 

하지만 이번 경우에는 shouldComponentUpdate가 false를 반환하기 때문에 state만 바뀐 상태에서 일단 cycle이 한번 종료가 되고, setState callback으로 forceUpdate가 실행되면서 렌더링 과정이 다시 시작된다. 그때의 state는 이미 업데이트가 된 상태라서 이전 state값과 현재 state 값이 같기 때문에 if문이 실행이 되지않는다 그래서 return 값을 dom 업데이트 직전 값을 보여줄 수 있도록 설정하였다.

 

코드 수정 후 실행 결과는 다음과 같다.

 

 

 

 

mount되고 이후에 클릭을 누르면 먼저 shouldComponentUpdate가 false를 반환하면서 render를 호출하지 않는다. 이때 state는 이미 setState로 인해 value는 2로 바뀐 상태이다. 그 이후 setState의 callback인 forceUpdate가 실행이 되고 렌더링 과정이 다시 진행되는데 이때 shouldComponentUpdate를 무시하고 건너뛰어 render가 호출된다. 그 이후에는 위에서와 동일한 method들이 쭉 호출되는 것을 볼 수 있다.

 

 


Unmount

컴포넌트가 mount가 해체될 때인데 이때는 딱 하나의 method가 호출된다.

 

  • componentWillUnmount

컴포넌트의 마운트 해체 직전 호출되는데 여기에는 주로 등록했었던 이벤트나 타이머 제거, 네트워크 요청 취소, 구독 해제 등의 정리 작업을 수행한다.

 


 

React의 Life Cycle을 예제를 만들어보면서 정리하였다.

 

정리하면서 기존의 두루뭉실하게 알고 있던 메소드들이 어떻게 작동하는지 잘 알수 있게 된 것 같고, 리액트에서 컴포넌트의 렌더링 동작이 어떤 흐름으로 되는지 정리할 수 있었던 것 같다.

'React' 카테고리의 다른 글

리액트 개발환경 설정해보기  (0) 2020.03.16
React - HOC  (0) 2020.03.10

댓글