[React] 생명주기(2) - 업데이트단계,소멸단계 (shouldComponentDidUpdate, getSnapshotBeforeUpdate, componentDidUpdate, getDerivedStateFromError, componentDidCatch)

권준혁·2020년 11월 1일
2

React

목록 보기
8/20
post-thumbnail

안녕하세요!
이어서 생명주기에 대한 포스팅입니다.
모든 컴포넌트는 다음과 같이 세 가지 단계를 거칩니다.

오늘은 업데이트단계와 소멸단계에 대해 포스팅하겠습니다.

그림의 업데이트할 때 부분을 봐주세요.
업데이트단계는 세 가지 시작점이 있습니다.

  • 속성값이 부모컴포넌트로부터 들어왔을 때 New props
  • setState()가 호출되어 상태값이 변경될 때 setState()
  • forceUpdate()로 강제업데이트 시켰을 때 forceUpdate()

업데이트단계


하나씩 살펴보겠습니다.

shouldComponentUpdate

shouldComponentUpdate메서드는 성능 최적화를 위해 사용됩니다. 항상 자동으로 업데이트 하게되면 편하고 좋지만, 상황에 따라 성능상의 이슈가 생길 수 있습니다. 생명주기메서드들은 이름이 직관적이라서 기억하기가 쉽습니다.

shouldComponentUpdate(nextProps, nextState)

return 타입은 bool입니다. 매개변수 nextProps,nextState로 각각 속성값과 상태값으로 컴포넌트가 업데이트를 할지 결정합니다. true를 return하면 render메서드가 호출됩니다. 반데로 false를 반환하면 업데이트 단계는 중단됩니다.

다음 코드는 state의 price값이 변경 됐을 때만 컴포넌트를 업데이트합니다.

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState){
    const {price} = this.state
    return price !== nextState.price;
  }
}

참을 반환하게되면 가상 돔 수준에서 변경된 내용이 있는지 비교합니다. false를 반환하면 업데이트단계는 중단되므로 연산과정이 생략되게 됩니다.
이 메서드는 성능 이슈가 발생했을 때 메서드를 작성해도 늦지 않다고 합니다.


getSnapshotBeforeUpdate

getSnapshotBeforeUpdate는 렌더링 결과가 실제 돔에 반영되기 직전에 호출됩니다. 그리고 다음순서 메서드인 componentDidUpdate는 컴포넌트가 업데이트 된 후에 호출됩니다.
메서드의 이름에서처럼 업데이트 되기 직전에 snapshot(props & states)을 확보하는게 목적입니다.

getSnapshotBeforeUpdate(prevProps,prevState) => snapshot

중요한 점은 getSnapshotBeforeUpdate의 반환값이 componentDidUpdate의 세번째 인자로 들어간다는 것입니다.
따라서 "snapshot"을 이 메서드에서 리턴하면 이 후에
componentDidUpdate에서는 이전속성,이전상태,snapshot까지 이용해 돔의 상탯값 변화를 알 수 있습니다.


componentDidUpdate

componentDidUpdate(prevProps, prevState, snapShot)

컴포넌트에서 getSnapshotBeforeUpdate를 구현한다면, 해당 메서드가 반환하는 값은 componentDidUpdate에 세 번째 “snapshot” 인자로 넘겨집니다. 반환값이 없다면 해당 인자는 undefined를 가집니다.

componentDidUpdate 메서드에서는 돔요소가 업데이트된 이 후기 때문에 비동기통신등 부수효과를 발생시키는 작업을 많이 수행합니다.
하지만 setState로 상태를 조건식없이 발생시키면 무한루프에 빠지기 쉽습니다.

또, 상위컴포넌트에서 받은 props를 그대로 state로 이용할 때, 버그가 발생할 수 있습니다. 해결방법인 완전제어컴포넌트와 key를이용한 완전비제어컴포넌트에 대해서는 지난 포스팅을 참고하면됩니다.
간단히 짚고 넘어가자면, 상위컴포넌트에서 상품정보를 받는 하위컴포넌트, 예를들어, 수정폼이 있다고할 때

if(this.props.price !== this.state.price) {
  // ...
}

가격을 비교해서 다른 상품인지 판별하는 조건식을 세웠습니다.
하지만, 다른 상품이어도 가격이 같은 경우가 있습니다.
이럴 경우 버그가 발생합니다.
이 때의 해결방법이 앞서 말한 완전제어컴포넌트와 key를이용한 완전비제어컴포넌트였습니다.

props를 통한 state변경은 리액트로 프로그래밍을 하면서 많이 겪게되는 상황이기 때문에 잘 짚고 넘어가야겠습니다.

componentDidMount에서 snapshot을 이용하는 예제를 한번 해보겠습니다.

import React from 'react'

export default class MyComponent extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            items:[]
        }
    }
    divRef = React.createRef()
    getSnapshotBeforeUpdate(prevProps,prevState) {
        const {items} = this.state;
        if(prevState.items.length < items.length) {
            const rect = this.divRef.current.getBoundingClientRect()
            return rect.height;
        }
        return null
    }
    componentDidUpdate(prevProps, prevState, snapshot) {
        if(snapshot !== null) {
            const rect = this.divRef.current.getBoundingClientRect();
            if(rect.height !== snapshot) {
                console.log(`prevState : ${prevState.items}`)
                console.log(`this.state : ${this.state.items}`)
                console.log(rect.height)
                alert('새로운 줄이 추가됐습니다.')
            }
        }
    }
    onClick = () => {
        const {items} = this.state;
        this.setState({items : [...items,'아이템']})
    }
    render() {
        const {items} = this.state
        return (
            <React.Fragment>
                <button onClick={this.onClick}>추가하기</button>
                <div ref={this.divRef} style={{width:'50px'}}>
                {items.map((item,i)=><span key={i} style={{height:'50px' ,width:'100%'}}>{item}</span>)}
                </div>
            </React.Fragment>
        )
    }
}

클릭시에 state.items 배열에 요소를 하나씩 추가합니다.
map메서드로 길이만큼 요소를 출력하고 alert()를 호출합니다.
createRef()메서드로 생성한 divRef는 렌더링되는 div요소의 속성값으로 들어갑니다. divRef로 해당 요소를 참조할 수 있습니다.
divRef.current.getBoundingClientRect로 렌더링된 요소경계 box의 크기와 위치에 접근할 수 있습니다. 여기서는 getBoundingClientRect의 height를 이용했습니다.

getSnapshotBeforeUpdate에서 state.items에 요소가 추가됐을 경우 컴포넌트가 업데이트되기 직전에 div값의 height를 snapshot으로 반환합니다.
반환된 height를 업데이트 후 componentDidUpdate에서 현재 div의 height와 비교해 요소가 추가돼 높이가 달라진 것으로 판단되면 alert메세지를 출력합니다.


componentDidMount와 componentDidUpdate

class UserInfo extends React.Component {
  componentDidMount(){
    const {user} = this.props;
    this.setFriends(user)
  }
  componentDidUpdate(prevProps){
    const {user} = this.props;
    if(prevProps.user.id !== user.id) {
      this.setFriends(user)
    }
  }
  setFriends(user) {
    requestFriends(user).then(firends=>this.setState({friends}));
  }
}

다음 코드는 자주 쓰이는 패턴입니다.
componentDidMount와 componentDidUpdate 양쪽 모두에 코드를 작성해줘야 하는 경우입니다.
두 생명주기메서드가 각각 실행되는 시점이 다르기 때문인데, 16.8 이후에 추가된 함수형 컴포넌트에서 을 사용하면 좀 더 간결해집니다.

소멸단계

  • componentWillUnmount

componentWillUnmount는 소멸단계에서 호출되는 유일한 생명주기 메서드입니다. 끝나지 않은 네트워크 요청을 취소,타이머해제, 구독해제 등의 작업을 처리하기 좋습니다.

지난 포스팅에서 타이머 예제를 할 때도 사용했었습니다.
componentWillUnmount는 componentDidMount와 짝으로 많이 사용합니다.


예외처리 생명주기

  • getDerivedStateFromError
  • componentDidCatch

getDerivedStateFromError & componentDidCatch

생명주기 메서드에서 예외가 발생하면 getDerivedStateFromError 또는 componentDidCatch 메서드를 구현한 가장 가까운 부모 컴포넌트를 찾게됩니다.
두 메서드의 구조입니다.

  getDerivedStateFromError(error)
  componentDidCatch(error, info)

getDerivedStateFromError는 에러정보를 state에 저장해 화면에 나타내는 용도로 쓰입니다. componentDidCatch는 에러 정보를 서버로 전송하는 용도로 사용됩니다..

  • getDerivedStateFromError에서 에러정보를 서버로 전송하지 않는이유
    리액트에서 데이터변경에 의한 화면업데이트는 렌더단계와 커밋단게를 거칩니다.. 비동기 렌더링(나중에 react에 추가될수 있는? 혹은 추가된)에서는 렌더단계에서 실행을 멈췄다가 다시 실행하는 과정에서 같은 생명주기 메서드를 여러번 호출할 수도 있습니다..
    다시 말해 getSnapshotBeforeUpdate, componentDidMount, componentDidUpdate, componentDidCatch를 제외하고는 여러번 호출 될 수도 있다는 것인데, 따라서 getDerivedStateFromError에서 서버로 에러를 전송을 여러번 하지 않도록 componentDidCatch에서 에러전송을 하는 것이좋습니다..

  • componentDidCatch에서 화면에 에러정보를 나타내지 않는이유
    componentDidCatch에서 현재는 setState를 할 수 있습니다.
    하지만, 렌더링 된 결과를 돔에 반영한 후에 호출되기 때문에 몇가지 문제를 안고있습니다. 대표적으로 서버사이드 렌더링 시 에러가 발생해도 componentDidCatch메서드는 호출되지 않는 문제라고 합니다.

조금은 복잡하지만 getDerivedStateFromError는 에러정보를 state에 저장해 화면에 나타내는 용도, componentDidCatch는 에러 정보를 서버로 전송하는 용도라는 것을 머리 속에 새겨야겠습니다.

읽어주셔서 감사합니다!

profile
웹 프론트엔드, RN앱 개발자입니다.

2개의 댓글

comment-user-thumbnail
2021년 11월 26일

안녕하세요 작성하신 부분에서
중요한 점은 getSnapshotBeforeUpdate의 반환값이 componentDidMount의 세번째 인자로 들어간다는 것입니다.
이 부분 반환값이 componentDidUpdate의 세번째인자로 들어가는것 아닐까요?

1개의 답글