Javascript/React 지식

[React] 컴포넌트의 생명주기

isfp_yykkng 2023. 4. 23. 13:34

컴포넌트의 생명주기 ⭐

컴포넌트의 생명주기 (LifeCycle)

컴포넌트의 생명주기는 컴포넌트의 생성부터 소멸까지의 과정으로 생명주기마다 함수를 가지고 있다. 이 함수들을 이용하면 특정 시점에 원하는 동작을 하도록 만들 수 있다. 또 이 생명주기 함수들은 리액트 엔진에서 자동으로 호출한다.

컴포넌트의 생성부터 생성완료까지의 4개의 생명주기 함수들을 생성 과정으로 부르고, 생성 완료부터 갱신 완료까지 5개의 생명주기 함수들을 갱신 과정으로 부른다. 마지막으로 갱신 완료부터 소멸완료까지 1개의 생명주기 함수를 소멸 과정으로 부른다. 각 과정들을 하나씩 살펴보자.

 

1) 생성 과정

순서 : constructor()  >  getDerivedStateFromProps()  >  render()  >  componentDidMount()  >  컴포넌트 생성 완료

  • constructor( props ) 함수 : 생성자 함수 이름 그대로 " 맨 처음에 생성될 때 한번만 호출 " 되며 상태( state 또는 객체 변수)를 선언할 때 사용된다. constructor() 함수를 정의할 때는 항상 super() 함수를 가장 위에 호출해야 한다. super() 함수에는 프로퍼티와 생명 주기 상태 등을 초기화하는 중요한 과정을 포함하고 있기 때문이다.
  • static getDerivedStateFromProps( props, state ) 함수 : 이 함수는 static 키워드가 붙은 정적 함수이다. 즉, 함수 안에서 this.props 과 this.state와 같은 방법으로 props나 state에 접근할 수 없다.  반드시 함수 인자로 전달된 props, state 를 이용해서 props나 state에 접근한다. 이 함수는 상위 컴포넌트에서 전달받은 props(프로퍼티)로 state값을 연동할 때 주로 사용되며 반환값으로 state를 변경한다. 
  • render( ) 함수 : 데이터가 변경되어 새 화면을 그려야 할 때 자동으로 호출되는 함수이다. render 함수가 반환(return)하는 JSX를 화면에 그린다.
  • componentDidMount( ) 함수 : 이 함수는 render() 함수가 JSX를 화면에 그린 이후에 호출되는 함수이다. 만약 컴포넌트가 화면에 모두 표현된 이후 해야 하는 작업들은 여기서 하면 된다. (컴포넌트가 올라간 뒤 후처리 작업)

2) 갱신 과정

갱신 과정은 갱신이 되었을 때부터 실행되는데 여기서 갱신은 " props가 변경되거나 setState() 함수가 호출 " 되는 경우를 말한다. ( forceUpdate() 를 통한 갱신도 갱신 과정의 시작이 된다. ) 갱신 과정은 sholudComponentUpdate() 함수의 true / false에 따라서 두 가지 순서를 가진다.

 

[중요] sholudComponentUpdate() 함수의 true / false에 따른 순서 ⭐

- true 일 때 : getDerivedStateFromProps() > shouldComponentUpdate() : true > render() > getSnapshotBeforeUpdate() > componentDidUpdate > 갱신 과정 완료

- false 일 때 : getDerivedStateFromProps()  >  shouldComponentUpdate() : false> 갱신 과정 종료

- 예외 ( forceUpdate() 실행으로 갱신 ) : getDerivedStateFromProps() > (shouldComponentUpdate() 생략) >  render() > getSnapshotBeforeUpdate() > componentDidUpdate > 갱신 과정 완료

 

  • shouldComponentUpdate(  nextProps, nextState  ) 함수 : 프로퍼티를 변경하거나 setState() 함수를 호출하여 stat값을 변경하면 ' 화면에 새로 출력해야 하는지' 판단하는 함수이다. 이 함수는 화면을 새로 출력할지 말지 판단하며, 데이터 변화를 비교하는 작업을 포함하므로 리액트 성능에 많은 영향을 준다. 즉, 화면 변경을 위해 검증 작업을 해야 하는 경우 이 함수를 사용하면 된다. 여기서 알아야 할 점은 forceUpdate() 함수를 호출하여 화면을 출력하면 이 함수는 호출되지 않는다.
  • getSnapshotBeforeUpdate(  prevProps, prevState  ) 함수 : 이 함수는 컴포넌트의 변경된 내용이 가상 화면에 완성된 이후 호출되는 함수이다. 이 함수는 컴포넌트가 화면에 실제로 출력되기 전에 호출되므로 화면에 출력될 엘리먼트의 크기 또는 스크롤 위치 등의 DOM 정보에 접근할 때 사용된다. 이 반환값으로 실제 화면에 출력될 snapshot을 반환한다. 
  • componentDidUpdate(  prevProps, prevState, snapshot  ) 함수 : 이 함수는 컴포넌트가 실제 화면에 출력된 이후 호출되는 함수이다. 부모 컴포넌트로부터 전달된 이전 프로퍼티(prevProps)와 이전 state값(prevState)과 함께 직전에 호출된 getSnapshotBeforeUpdate() 함수의 반환값인 snapshot을 인자로 전달받는다. 이 전달받은 값들을 이용하여 스크롤 위치를 옮기거나 커서를 이동시키는 등의 DOM 정보를 변경할 때 사용된다.

3) 소멸 과정

순서 : componentWillUnmount()  >  컴포넌트 소멸

  • componentWillUnmount() 함수 : 이 함수는 컴포넌트가 소멸되기 직전에 호출되는 함수이다. 보통 컴포넌트에서 감시하고 있는 작업들을 해제할 때 필요한 함수들이다. 예를 들어 setInterval() 함수가 실행되었다면 clearInterval() 함수로 해제하는 등의 작업을 수행한다. 이런 해제 작업이 생략되면 메모리 누수 현상이 발생하여 웹 브라우저 작동이 멈출 우려가 있다.

위 과정을 모두 도식화한 그림을 보면 이해가 좀 더 편하실 겁니다 :)

컴포넌트 생명 주기 함수 실행 과정 예제

//Lifecycle.js
import React, { Component } from "react";

class Lifecycle extends Component {
  constructor(props) {
    super(props);
    this.state = {};
    console.log("constructor 호출");
  }

  static getDerivedStateFromProps() {
    console.log("getDerivedStateFromProps 호출");
    return {};
  }

  componentDidMount() {
    console.log("componentDidMount 호출");
    this.setState({ updated: true });
    // this.forceUpdate();
  }

  componentWillUnmount() {
    console.log("componentWillUnmount 호출");
  }

  getSnapshotBeforeUpdate() {
    console.log("getSnapshotBeforeUpdate 호출");
    return {};
  }

  componentDidUpdate(prevProps, prevState) {
    console.log("componentDidUpdate 호출");
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("sholudComponentUpdate 호출");
    return true;
  }

  render() {
    console.log("render 호출");
    return <div></div>;
  }
}

export default Lifecycle;

위 코드 처럼 컴포넌트를 생성되고 setState로 state값을 변경했을 때, shouldComponentUpdate의 값이 true라면 아래같이 나온다.

...

  componentDidMount() {
    console.log("componentDidMount 호출");
    this.setState({ updated: true });
    // this.forceUpdate();
  }

...

  shouldComponentUpdate(nextProps, nextState) {
    console.log("sholudComponentUpdate 호출");
    return false;
  }

...

하지만 위 코드처럼 setState() 로 state 값을 변경했더라도 sholudComponentUpdate() 값이 false라면 다음과 같이 나온다.

두 경우는 sholudComponentUpdate의 값에 따라 true면 render 이후 과정을 호출하고, false면 생략하는 걸 보여준다. 앞서 말한 예외 경우같이 forceUpdate() 함수로 값을 변경한다면 어떻게 될까?

...

  componentDidMount() {
    console.log("componentDidMount 호출");
    //this.setState({ updated: true });
    this.forceUpdate();
  }

...

  shouldComponentUpdate(nextProps, nextState) {
    console.log("sholudComponentUpdate 호출");
    return false;
  }

...

forceUpdate()로 강제로 값을 변경한다면 shouldComponentUpdate()를 생략한다. 따라서 shouldComponentUpdate()의 값이 false로 반환되어도 render() 이후 과정을 호출하는 것을 볼 수 있다.

마지막으로 상위 컴포넌트인 App 컴포넌트에서 hasDestroyed 라는 state값을 설정해서 true면 null을 통해서 컴포넌트를 아무것도 띄우지 않음으로써 소멸시키고, false면 Lifecycle 컴포넌트를 생성하는 코드를 작성해보겠다.

//App.js
import React, { Component } from "react";
import Lifecycle from "./03/Lifecycle";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hasDestroyed: false,
    };
  }
  componentDidMount() {
    // this.setState({ hasDestroyed: true });
  }
  render() {
    return (
      <div>{this.state.hasDestroyed ? null : <Lifecycle />}</div>
    );
  }
}

export default App;

위 코드처럼 setState() 로 값을 바꾸지 않고 초깃값 그대로 hasDestroyed가 false라면 앞서 본 결과와 같이 생성과 갱신이 진행된다. 하지만 아래 코드처럼 setState로 hasDestroyed를 true로 바꾸어 소멸시킨다면 사진과 같은 결과가 나온다.

...

  componentDidMount() {
    this.setState({ hasDestroyed: true });
  }

...
 

위 사진과 같이 false일 때 Lifecycle 컴포넌트를 실행했다가 App 컴포넌트의 componentDidMount에서 setState() 로 state값이 true로 바뀌면서 null 값으로 컴포넌트를 소멸시켰다. 그 결과 componentWillUnmount() 가 호출되는 것을 볼 수 있다.