Rendering sequence

React로 개발을 하다가 react-router에서 props를 받아서 사용해야 되는 경우가 발생하였습니다(흔히 있는 경우).

그리고 그 props를 받아서 render을 해줘야 되고 그 값을 state로 사용해 받아온 props를 setState를 이용해서 값을 저장해주고 싶었습니다.

그리하여 어떤 lifecylce 메소드를 사용해야 하나 생각을 하던찰라에 rendering 순서에 대해서 궁금하였습니다.

React에서 rendering은

  1. Constructer
  2. ComponentWillMount(deprecate지만 이 메소드만은 다룰 필요가 있어 다뤄보겠습니다.)
  3. Render
  4. ComponentDidMount

이렇게 render가 될때 실행이 되고

값이 변경이 될때의 method들은

  1. shouldComponentUpdate
  2. componentWillUpdate
  3. [new]static getDerivedStateFromProps (이런게 있었네요.. 20줄짜리 코드가 3줄이 되는 마법)
  4. [new]getSnapshotBeforeUpdate
  5. componentDidUpdate

컴포넌트 제거 시 method는

  1. componentWillUnmout

이렇게 React의 Lifecycle method가 구성이 되어있습니다.

하나하나 정리를 해보겠습니다.

(deprecated 경우엔 skip하겠습니다.)


1. constructor

이 API는 component가 rendering 되기 전에 호출됩니다. 다른 api들보다도 가장 먼저 호출되기 때문에 컴포넌트의 초기 state를 넣어주는 줍니다. 컴포넌트 생성자 함수로써 컴포넌트가 새로 만들어질때마다 이 함수가 호출됩니다. 여기에 bind를 해주는 경우가 많은데 현재는 classfield 문법을 통해 bind메소드를 생략하는 경우가 많습니다.

If you don’t initialize state and you don’t bind methods, you don’t need to implement a constructor for your React component.

이렇게 적혀있네요 저는 평소에 constructer를 사용하지 않습니다.

혹시 constructer에서 제가 놓치고 있는 부분이 있다면 댓글이나 dm으로 알려주시면 감사하겠습니다.

constructor(props) {
  super(props);
  // Don't call this.setState() here!
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

2. ComponentWillMount

이 API는 constructor와 마찬가지로 component가 rendering되기 전에 호출됩니다. reactjs.org의 말을 인용해보면

UNSAFE_componentWillMount() is invoked just before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering. Generally, we recommend using the constructor() instead for initializing state.

동기적 seState로 사용이 되기에 이 method는 추가적인 렌더링을 유발하지 않습니다. 그렇기에 16.3 이후로는 deprecated되는 상황입니다. 이 method는 constructer와 componentDidMount로 충분히 대처가 가능한 상황입니다.

3. render()

이 메소드는 오직 class component에서만 요구되어 집니다.

호출되어졌을 때 this.props나 this.state을 검사하고 다음 중 하나의 타입을 반환합니다.

  • React elements => 일반적으로 JSX를 만듭니다. 예를들어 <div/>, <MyComponent/>는 각각 React가 DOM노드 또는 다른 사용자 구성 component를 렌더링하도록 지시하는 React요소입니다.
  • Arrays and fragments => 당신은 render로 부터 많은 elements를 반환할 수 있습니다.
  • Portals => 당신은 다른 DOM subtree에 자식을 render할 수 있습니다.
  • String and numbers => DOM에 text 노드를 렌더링할 수 있습니다.
  • Booleans or null

render() 함수는 순수해야합니다. 이는 component의 state를 변경하지 않아야 한다는 뜻입니다. 그것은 매번 같은 결과를 반환해야합니다. 그리고 브라우저와 직접적으로 interact하지 않습니다. 만약 브라우저랑 상호작용 해야한다면, componetDidMount나 다른 라이프싸이클 메소드안에서 당신의 작업을 수행하면 됩니다.

4. ComponentDidMount

Component가 화면에 렌더링된 직 후 호출됩니다. DOM을 사용하는 외부 라이브러리나 데이터를 요청하는 ajax요청을 합니다. 필자는 기본적으로 가볍고 렌더링 이후 진행하는 테스트를 진행할 때 이 메소드를 자주 이용합니다. 이 메소드는 약간의 구독을 사용하기 좋은 장소인데 componentWillUnmout()를 통해 구독해제하는 것을 잊어서는 안됩니다.

여기서 setState를 부른다면 추가적인 렌더링을 유발할 수 있습니다. 하지만 이것은 브라우저가 업데이트 되기 전에 발생합니다. 이런 경우에 render()가 두번 호출될 지라도 사용자는 즉각적으로 state를 볼 수 없습니다.이것은 퍼포먼스 이슈랑 연관되기 때문에 이 패턴은 주의사항과 함께 사용하길 권장합니다. 대부분의 경우에는 constructor에 최초의 state를 할당하는 것이 권장됩니다.


5. shouldComponentUpdate

이 API는 컴포넌트를 최적화하는 작업에서 매우 유용하게 사용됩니다. React에서는 가장 큰 장점인 virtual DOM을 사용하는데 이는 변화가 발생하는 부분만 재 렌더링해주기 때문에 성능이 꽤 잘나오는 부분입니다. 변화가 생기는 부분을 감지하기 위해서는 virtual DOM 을 한번 그려야 하는데 불필요한 Virtual DOM을 방지하기 위해서 이 메소드를 사용합니다. 이 메소드는 기본적으로 true를 반환하는데 조건을 주어 render함수를 호출하지 않을 수 있습니다.

이 메소드는 components의 결과가 state, props가 현재의 변화에 의해 영향을 끼쳤는지 아닌지 알게 해줍니다. 기본 행동은 모든 state가 변하면 재렌더링 해주는 것으로 대다수의 케이스에서는 기본 행동에 의지해야한다. 이 메소드는 새로운 props나 state를 받았을 때 rendering전에 호출됩니다. default는 true이고 이 메소드는 최초 렌더링이나 forceUpdate() 사용될 때는 호출되지 않습니다.

이 메소드는 오직 퍼포먼스 최적화를 위해 존재합니다. 이 메소드를 쓰기 전에 순수컴포넌트를 사용하는 것에 대해 생각해 보세요 순수 컴포넌트는 props, state를 얇은 비교를 수행합니다. 그리고 필수적인 업데이트를 줄입니다.

6. static getDerivedStateFromProps()

이 API는 props로 받아온 값을 state로 동기화 하는 작업을 해줘야하는 경우에 사용합니다.

static getDerivedStateFromProps(nextProps, prevState) {
  // 여기서는 setState 를 하는 것이 아니라
  // 특정 props 가 바뀔 때 설정하고 설정하고 싶은 state 값을 리턴하는 형태로
  // 사용됩니다.
  if (nextProps.value !== prevState.value) {
    return { value: nextProps.value };
  }
  return null; // null 을 리턴하면 따로 업데이트 할 것은 없다라는 의미
}

이 메소드는 render 호출전에 호출됩니다. 최초의 mount 뿐 아니라 변경될 때마다 실행이 됩니다. 이 메소드는 state가 전체적인 시간에 props의 변화에 의존하는 드문 사용경우입니다.

끌어올린 state는 코드가 장황스럽게 이끌고 당신의 컴포넌트가 무엇에 대한 것인지 생각하기 어렵게 만듭니다. 그렇기에 더 간단한 대안에 익숙해져야 합니다.

  • 만약 당신이 props의 변화에 따른 반응에 사이드 이펙트(데이타 fetching이나 애니메이션)를 수행해야만 한다면, componentDidUpdate를 사용하세요.
  • 만약 당신이 props가 변할 때 데이터를 재계산해야 한다면, memoization helper를 사용하세요.
  • 만약 당신이 props가 별할 때 몇몇의 state가 reset되길 원한다면, 컴포넌트를 완전 통제하거나 키와 함께 완전 불통제 하는 방법을 고려해보세요.

이 메서드는 원인과 관계없이 모든 redner에서 시작됩니다. UNSAFE_componentWillReceiveProps와 대조적으로 비교할 수 있는데 부모가 재렌더링하고 로컬 setState의 결과가 아닌 경우에만 발생한다는 점입니다.

위에는 공식페이지의 번역이었고 실제적인 예시를 들어보면

이번 프로젝트에서 router를 사용하였고 이전 페이지에서 props를 넘겨주어 그 데이터를 통해 render에서 컴포넌트를 실행해야하는 상황이 왔습니다. props를 state화로 바꾸어서 render가 호출되기 전에 사용하고 싶어서 다양한 시도를 해보았습니다. state, constructer, componentDidMount등등 을 사용해 보았는데 작동이 되질 않아서 어쩔 수 없이 render함수에 삼항연산자를 통해 렌더링하는 방식을 택했으나 코드길이가 길었고 조건연산자가 붙어 좋은 코드는 아니라는 느낌을 받았습니다.

리펙토링 중 lifecycle메소드를 찾아보니 getDerivedStateFromProps를 통해 render가 호출되기 전 props를 state값으로 바꾸어주는 기능을 하는 메소드를 찾았습니다. 그리해서 20줄짜리 코드가 3줄이 되는 마법을 보며 다시한번 아는게 힘이고 공부를 최신기술 공부를 계속해야됨을 느꼈습니다.(예전에 공부할 때는 이 메소드가 없었습니다.)

공식문서에서는 자주 사용하는 케이스가 아니라고 했으나 제 코딩 스타일과 router를 통한 props를 내려줄 때는 자주 사용할 것 같습니다.

7. [NEW]getSnapshotBeforeUpdate

이 API 발생하는 시점은 render 호출 직후로 실제 DOM에 변화 발생일어나기 직전의 상태를 가져올 수 있습니다. 그것은 당신의 컴포넌트가 잠재적으로 변하기 전에 DOM으로부터 몇가지 정보를 포착할 수 있게 해줍니다.

흔한 케이스는 아니지만 chat과 같은 UI에서 scroll position을 특별한 방법으로 사용할 때 사용합니다.

getSnapshotBeforeUpdate(prevProps, prevState) {
    // DOM 업데이트가 일어나기 직전의 시점입니다.
    // 새 데이터가 상단에 추가되어도 스크롤바를 유지해보겠습니다.
    // scrollHeight 는 전 후를 비교해서 스크롤 위치를 설정하기 위함이고,
    // scrollTop 은, 이 기능이 크롬에 이미 구현이 되어있는데,
    // 이미 구현이 되어있다면 처리하지 않도록 하기 위함입니다.
    if (prevState.array !== this.state.array) {
      const {
        scrollTop, scrollHeight
      } = this.list;

      // 여기서 반환 하는 값은 componentDidMount 에서 snapshot 값으로 받아올 수 있습니다.
      return {
        scrollTop, scrollHeight
      };
    }
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      const { scrollTop } = this.list;
      if (scrollTop !== snapshot.scrollTop) return; // 기능이 이미 구현되어있다면 처리하지 않습니다.
      const diff = this.list.scrollHeight - snapshot.scrollHeight;
      this.list.scrollTop += diff;
    }
  }

https://codesandbox.io/s/484zvr87ow 여기서 참고하면 좋을 것 같습니다.

실제로 사용해보지 않아서 많이 필요한 케이스인지는 모르겠습니다. 이번 프로젝트에서 scroll은 overflow:auto를 사용하여 해결하였습니다. react-scrolllock를 사용하였으나 더 나은 UI를 위해 auto방식을 택하였습니다. 조금 더 경험 후 추가하도록 하겠습니다. 혹시 경험이 있으신 분이 있으시다면 댓글로 남겨주세요!

8. componentDidUpdate

이 API의 순서는 컴포넌트에서 render()를 호출하고 난 다음에 발생하게 됩니다. 이 메소드는 최초의 render에서는 호출되지 않습니다. 이 시점에선 this.props와 this.state가 바뀌어 있습니다. 이 메소드는 component가 없데이트 되었을 때 DOM에서 작동하는 기회로 사용합니다. 이 메소드는 network 메소드 뿐만 아니라 현재 props와 previous props와 비교할 며 사용하기 에도 좋습니다.

componentDidUpdate(prevProps){
  // Typical usage (don't forget to compare props);
  if(this.props.userID !== prevProps.userID){
    this.fetchData(this.props.userID);
  }
}

위는 componentDidUpdate의 usage입니다.

이 메소드안에서 setState를 호출한다면 그것은 wrapped 되어져 있어야 합니다. 그렇지 않으면 무한루프에 빠질 것입니다. 그것은 추가적인 재렌더링을 유발하고 component 퍼포먼스에 영향을 끼칠 것입니다.

9. componentWillUnmount

이는 컴포넌트가 Unmount될 때 호출됩니다. 필수적인 제거, 유효하지 않은 타이머, 네트워크 요청 취소, 구독 해제 같은 일들을 할 때 호출됩니다. 여기서는 setState를 호출해서는 안됩니다. 이 컴포넌트가 다시 재렌더링될지 않기 때문입니다.

10. componentDidCatch

render함수에서 에러가 발생한다면 React앱이 크러쉬 되는데 그런 상황에서 유용하게 사용가능한 method입니다.

componentDidCatch(error, info) {
  this.setState({
    error: true
  });
}
import React, { Component } from "react";

const Problematic = () => {
  throw new Error("버그가 나타났다!");
  return <div />;
};

class Counter extends Component {
  // ... 생략

  render() {
    return (
      <div>
        <h1>카운터</h1>
        <div>: {this.state.number}</div>
        {this.state.number === 4 && <Problematic />}
        <button onClick={this.handleIncrease}>+</button>
        <button onClick={this.handleDecrease}>-</button>
      </div>
    );
  }
}

export default Counter;

오늘 포스팅을 하는 중에 Hooks에 대한 lifecycle method도 정리를 해야겠다라는 생각이 문득들었습니다. 다음 포스팅은 Hooks Lifecycle method 대해 알아보도록 하겠습니다.

참고

react 공식문서
velopert