본문 바로가기
Programming 개발은 구글로/Web[프론트엔드&백엔드]

[Web] React의 LifeCycle API 알아보자

by 40대직장인 2022. 6. 28.

React LifeCycle 관련 API

: 브라우저에서 보이고 사라지는 등 업데이트 및 호출되는 컴포넌트  API입니다.

 

1. 컴포넌트 생성

: 컴포넌트가 브라우저에 나타나기 전, 후에 호출되는 API들입니다.

1.1 constructor

: 컴포넌트 생성자 API로 컴포넌트가 새로이 만들어질 때마다 호출됩니다.

constructor(props) {
  super(props);
}

1.2 componentWillMount

: 컴포넌트가 화면에 디스플레이가 되기 전에 호출되는 API입니다.

React v16.3부터 해당 API가 deprecated가 되었으며, v16.3 이후부터는 UNSAFE_componentWillMount()로 사용됩니다.

 

기존에 담당하던 부분은 1.3 componentDidMount에서 처리됩니다.

componentWillMount() {
}

1.3 componentDidMount

: 컴포넌트가 화면에 디스플레이가 될 때 호출되는 API입니다.

여기선 주로 D3, masonry처럼 DOM을 사용해야 하는 외부 라이브러리 연동을 하거나, 해당 컴포넌트에서 필요로 하는 데이터를 요청하기 위해 사용됩니다.

 

axios, fetch 등으로 ajax 요청을 하거나 DOM의 속성을 읽어서 직접 변경하는 작업을 진행합니다.

componentDidMount() {
  // 외부 라이브러리 연동: D3, masonry, etc
  // 컴포넌트에서 필요한 데이터 요청: Ajax, GraphQL, etc
  // DOM 에 관련된 작업: 스크롤 설정, 크기 읽어오기 등
}

 

2. 컴포넌트 업데이트

: 컴포넌트의 업데이트는 props와 state의 변화에 따라 결정됩니다. 업데이트가 되기 전과 후에 호출되는 API들입니다.

2.1 componentWillReceiveProps

: 컴포넌트가 새로운 props를 받게 됐을 때 호출됩니다.

 

state가 props에 따라 변해야 하는 로직을 작성합니다.

새로 받게 될 props는 nextProps로 조회할 수 있으며, 이때 this.props를 조회하면 업데이트되기 전의 API이니 참고하세요. 

 

이 API 또한 v16.3부터 deprecate 됩니다. v16.3부터는 UNSAFE_componentWillReceiveProps()라는 이름으로 사용됩니다. 그리고, 이 기능은 상황에 따라 새로운 API 2.2 getDerivedStateFromProps로 대체될 수도 있습니다.

componentWillReceiveProps(nextProps) {
  // this.props 는 아직 바뀌지 않은 상태
}

2.2 getDerivedStateFromProps() [NEW API] 

: v16.3 이후에 만들어진 라이프사이클 API입니다. 이 API는 props로 받아온 값을 state로 동기화하는 작업을 해줘야 하는 경우에 사용됩니다.

static getDerivedStateFromProps(nextProps, prevState) {
  // setState를 하는 것이 아니라 특정 props가 바뀔 때 설정하고 싶은 state 값으로 리턴시킴
  if (nextProps.value !== prevState.value) {
    return { value: nextProps.value };
  }
  return null; // null을 리턴하면 업데이트 내용이 없다는 의미함
}

2.3 shouldComponentUpdate

: 컴포넌트를 최적화하는 작업에서 매우 유용하게 사용되는 API입니다.

 

React에서는 변화가 발생하는 부분만 업데이트를 해줘서 성능이 좋아지는데 변화가 발생한 부분만 감지해내기 위해서는 Virtual DOM에 한번 그려줘야 합니다. 즉, 현재 컴포넌트의 상태가 업데이트되지 않아도, 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트들도 렌더링 됩니다.

 

여기서 “렌더링” 된다는 건, render() 함수가 호출된다는 의미입니다.

변화가 없으면 물론 DOM 조작은 하지 않게 됩니다. 그저 Virutal DOM 에만 렌더링 할 뿐이죠. 이 작업은 그렇게 부하가 많은 작업은 아니지만, 컴포넌트가 무수히 많이 렌더링 된다면 얘기가 조금 달라집니다.

 

CPU 자원을 어느 정도는 사용하고 있기 때문입니다. 쓸데없이 낭비되고 있는 이 CPU 처리량을 줄여주기 위해서 우리는 Virtual DOM에 리렌더링 하는 것도 불필요할 경우엔 방지하기 위해서 shouldComponentUpdate를 작성합니다. 이 함수는 기본적으로 true를 반환합니다. 조건에 따라 false를 반환하면 해당 조건에는 render 함수를 호출하지 않습니다.

shouldComponentUpdate(nextProps, nextState) {
  // return false 하면 업데이트를 안함
  // return this.props.checked !== nextProps.checked
  return true;
}

2.4 componentWillUpdate

: shouldComponentUpdate에서 true를 반환했을 때만 호출되는 API입니다. 애니메이션 효과를 초기화하거나, 이벤트 리스너를 없애는 작업을 합니다.

 

이 함수가 호출된 이후 render()가 호출이 됩니다. 

이 API 또한 v16.3 이후 deprecate 됩니다. 기존의 기능은 2.5 getSnapshotBeforeUpdate로 대체될 수 있습니다.

componentWillUpdate(nextProps, nextState) {
}

2.5 getSnapshotBeforeUpdate()  [NEW API] 

이 API 가 발생하는 시점은 다음과 같습니다.

  1. render()
  2. getSnapshotBeforeUpdate()
  3. 실제 DOM에 변화 발생
  4. componentDidUpdate

DOM 변화가 일어나기 직전의 DOM 상태를 가져오고, 여기서 리턴하는 값은 componentDidUpdate에서 3번째 Parameter로 받아올 수 있게 됩니다.

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // DOM 업데이트가 일어나기 직전의 시점입니다.
    // 새 데이터가 상단에 추가되어도 스크롤바를 유지해보겠습니다.
    if (prevState.array !== this.state.array) {
      const {
        scrollTop, scrollHeight
      } = this.list;

      return {
        scrollTop, scrollHeight // componentDidUpdate에서 snapshot Parameter
      };
    }
  }
  
  // scrollHeight는 전 후를 비교해서 스크롤 위치를 설정
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      const { scrollTop } = this.list;
      // scrollTop은 이 기능이 크롬에 이미 구현이 되어있다면 처리하지 않음.      
      if (scrollTop !== snapshot.scrollTop) return; 
      const diff = this.list.scrollHeight - snapshot.scrollHeight;
      this.list.scrollTop += diff;
    }
  }

2.6 componentDidUpdate

: 컴포넌트에서 render()를 호출하고 난 다음에 발생하게 됩니다.

이 시점에선 this.props 와 this.state 가 바뀌어있습니다. 그리고 Parameter를 통해 이전의 값인 prevProps와 prevState를 조회할 수 있습니다.

// getSnapshotBeforeUpdate에서 반환한 snapshot 값은 세번째 Parameter로 받아옵니다.
componentDidUpdate(prevProps, prevState, snapshot) { 
}

 

3. 컴포넌트 제거

: 컴포넌트가 더 이상 필요하지 않게 되면 단 하나의 API 가 호출됩니다:

3.1 componentWillUnmount

: 주로 등록했었던 이벤트를 제거하고, 만약에 setTimeout이 설정이 되어있다면 clearTimeout을 통하여 제거를 합니다. 추가적으로, 외부 라이브러리를 사용한 게 있고 해당 라이브러리에 dispose 기능이 있다면 여기서 호출해주시면 됩니다.

componentWillUnmount() {
  // 이벤트, setTimeout, 외부 라이브러리 인스턴스 제거
}

 

 

 

 

4. LifeCycle API를 작성

기존에 우리가 만들었던 카운터에 LifeCycle API를 작성해보겠습니다. Counter.js를 다음과 같이 수정해보세요.

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0
  }

  constructor(props) { // 생성자
    super(props);
    console.log('constructor');
  }
  
  componentWillMount() { // 사용하지 않음
    console.log('componentWillMount (deprecated)');
  }

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

  shouldComponentUpdate(nextProps, nextState) {
    console.log('shouldComponentUpdate');        
    if (nextState.number % 5 === 0) return false; // 5의 배수라면 리렌더링 하지 않음
    return true;
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('componentWillUpdate');
  }
  
  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate');
  }  

  increase = () => {
    this.setState({
      number: this.state.number + 1
    });
  }

  decrease = () => {
    this.setState({
      number: this.state.number - 1
    });
  }

  render() {
    return (
      <div>
        <div>카운터</div>
        <div>값: {this.state.number}</div>
        <button onClick={this.increase}>+</button>
        <button onClick={this.decrease}>-</button>
      </div>
    );
  }
}

export default Counter;

 

5. 컴포넌트 에러

render 함수에서 에러가 발생한다면, React App이 Crash가 되어버립니다. 

5.1 componentDidCatch

render 함수에서 에러가 발생될 경우 유용하게 사용할 수 있는 API입니다.

에러가 발생하면 이런 식으로 componentDidCatch가 실행이 됩니다.

state.error를 true로 설정하게 하고, render 함수 쪽에서 이에 따라 에러를 띄워주시면 됩니다.

 

※ 해당 API를 사용하시게 될 때 주의사항으로 컴포넌트 자신의 render 함수에서 에러가 발생하는 것은 Catch를 못합니다. 하지만,  컴포넌트의 자식 컴포넌트 내부에서 발생하는 에러들을 잡아낼 수 있습니다.

componentDidCatch(error, info) {
  this.setState({
    error: true
  });
}

 

< 예제 코드 >

import React, { Component } from 'react';

const Problematic = () => {
  throw (new Error('Catch bug!'));
  return (
    <div>      
    </div>
  );
};

class Counter extends Component {
  ...
  
  render() {
    return (
      <div>
      <div>카운터</div>
      <div>값: {this.state.number}</div>
      { this.state.number === 4 && <Problematic /> }
      <button onClick={this.increase}>+</button>
      <button onClick={this.decrease}>-</button>
      </div>
    );
  }
}

export default Counter;

Problematic이라는 컴포넌트를 만들고 이 값이 4가 되면 렌더링을 하도록 설정했습니다. Problematic은 렌더링이 될 때 에러가 발생했음을 알리는 throw를 사용했습니다.

 

이제 componentDidCatch를 통하여 자식 컴포넌트에서 발생한 에러를 잡아보겠습니다.

import React, { Component } from 'react';

const Promblematic = () => {
  throw (new Error('Catch bug!'));
  return (
    <div>      
    </div>
  );
};

class Counter extends Component {
  state = {
    number: 0,
    error: false
  }
  ...
  componentDidCatch(error, info) {
    this.setState({
      error: true
    });
  }
  
  render() {
    if (this.state.error) return (<h1>Error!</h1>);

    return (
      <div>
        <div>카운터</div>
        <div>값: {this.state.number}</div>
        { this.state.number === 4 && <Promblematic /> }
        <button onClick={this.increase}>+</button>
        <button onClick={this.decrease}>-</button>
      </div>
    );
  }
}

export default Counter;

카운터의 값을 4로 증가시키면 에러가 발생했다는 메시지가 뜨게 됩니다.

 

 

보통, 렌더링 부분에서 오류가 발생하는 것은 사전에 방지해주어야 합니다. 주로 자주 에러가 발생하는 이유는 다음과 같습니다.

 

  1. 존재하지 않는 함수를 호출하려고 할 때 (예를 들어서 props로 받았을 줄 알았던 함수가 전달되지 않았을 때)
this.props.onClick();

 

  1. 배열이나 객체가 올 줄 알았는데, 해당 객체나 배열이 존재하지 않을 때
this.props.object.value; // object is undefined
this.props.array.length; // array is undefined

 

이러한 것들은 render 함수에서 다음과 같은 형식으로 막아 줄 수 있습니다.

render() {
  if (!this.props.object || !this.props.array || this.props.array.length ===0) return null;
  // object 나 array 를 사용하는 코드
}

 

아니면 컴포넌트의 기본값을 설정하는 defaultProps를 통해서 설정하면 됩니다.

class Sample extends Component {
  static defaultProps = {
    onIncrement: () => console.warn('onIncrement is not defined'),
    object: {},
    array: []
  }
}

 

 


참고 글: 누구든지 하는 리액트 5편: LifeCycle API
https://velopert.com/3631

 

 

 

 

댓글