프론트엔드/ReactJS

useEffect와 생명주기 (1) - 클래스형 컴포넌트의 State와 Lifecycle

hyongti 2021. 12. 15. 12:35

이전 글 : useEffect와 생명주기 (0) - 주절주절

다음 글 :

 

매 초 시간이 흐르는 시계를 렌더링한다고 생각해봅시다.

(자식 컴포넌트에 props로 데이터를 넘기는 법은 알고, state는 모른다고 가정)

그럼 다음과 같이 setInterval()을 통해, 매초 tick() 함수를 호출해야 합니다.

 

하지만 시간이 흐르는 것, 즉 타이머를 설정하고 매초 UI를 업데이트 하는 것은 Clock 컴포넌트가 하는 게 좋을 것 같습니다.

 

이것을 구현하기 위해 Clock 컴포넌트에 추가해야하는 것이 바로 "state"입니다.

 

props와 state의 주요 차이점은 다음과 같습니다.

  • state는 내부에 있고 컴포넌트 자체에 의해 제어됩니다.
  • 반면 props는 외부에 있고 구성 요소를 렌더링하는 상위 컴포넌트에 의해 제어됩니다.

state와 생명주기에 대해 이해하기 위해 함수형 컴포넌트를 잠시 클래스형 컴포넌트로 변환하겠습니다.

 

함수를 클래스로 변환하는 과정은 다음과 같습니다.

  1. React.Component 를 확장하는 동일한 이름의 ES6 class를 생성하고,
  2. render()라는 빈 메서드를 추가합니다.
  3. 함수의 내용을 render() 안으로 옮깁니다.
  4. render() 내용 안에 있는 props를 this.props로 변경합니다.
  5. 남아있는 빈 함수 선언을 삭제합니다.

결과는 다음과 같습니다.

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}</h2>
      </div>
    );
  }
}

잠시 다음 그림을 보겠습니다.

리액트 컴포넌트의 생명주기(lifecycle)

그림은 리액트 컴포넌트의 생명주기(lifecycle)입니다. 클래스형 컴포넌트가 되면서 나타난 render()와 같은 메서드가 그림 중간에 보입니다. 클래스형 컴포넌트가 생성되면 생성될 때 블록에 있는 메서드가 순서대로 실행됩니다.

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

컴포넌트에 state를 추가하기 위해선 먼저 컴포넌트의 생성자를 살펴봐야 합니다.

constructor(props)

메서드를 바인딩하거나 state를 초기화하는 작업이 없다면, 해당 컴포넌트에는 생성자를 구현하지 않아도 됩니다.

 

리액트에서 생성자는 보통 아래의 두 가지 목적을 위하여 사용됩니다.

  • this.state에 객체를 할당하여 지역 state를 초기화
  • 인스턴스에 이벤트 처리 메서드를 바인딩

생성자는 다음과 같이 호출하면 됩니다.

constructor(props) {
  super(props);
  // 여기서 this.setState()를 호출하면 안 됩니다.
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

생성자는 this.state를 직접 할당할 수 있는 유일한 곳입니다. 그 외의 메서드에서는 this.setState()를 사용해야 합니다.


다시 본론으로 돌아가면,

우리는 Clock 컴포넌트가 타이머를 설정하고 매초 UI를 업데이트하게 만들 것이고, 그러기 위해 state를 추가하려고 했습니다. Clock 컴포넌트에 state를 추가하면 코드는 다음과 같이 변합니다.

 

Clock 컴포넌트가 타이머를 설정하고 매초 UI를 업데이트하게 만들 것이므로, setInterval()을 통해 ReactDOM.render()를 주기적으로 호출하는 부분도 지웠습니다.

import React from "react";
import ReactDOM from "react-dom";

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}</h2>
      </div>
    );
  }
}

ReactDOM.render(<Clock />, document.getElementById("root"));

 

아마 여기까지 코드를 작성하고, 실행하면 브라우저에 출력되는 시계는 멈춰있을 것입니다.

아직 타이머를 설정하지 않았기 때문입니다. 이번엔 타이머를 설정해보겠습니다.


타이머는 언제, 어떻게 설정하는 것이 좋을까요?

많은 컴포넌트가 있는 어플리케이션에서는 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 것이 중요합니다. 따라서 Clock 컴포넌트가 처음 DOM에 렌더링 될 때마다 타이머를 설정하고, Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제하겠습니다.

 

컴포넌트가 처음 DOM에 렌더링 되는 것을 "마운팅"이라고 하고, 생성된 DOM이 삭제되는 것을 "언마운팅"이라고 합니다.

 

다시 한번 리액트 생명주기 도표를 살펴보면,

리액트 컴포넌트의 생명주기(lifecycle)

컴포넌트가 마운트 됐을 때 실행되는 메서드인 componentDidMount와 컴포넌트가 언마운트 되고 나서 실행되는 메서드인 componentWillUnmount가 있습니다. 이를 활용해 Clock 컴포넌트에 타이머를 설정하고 해제하는 코드를 작성하겠습니다.

 

import React from "react";
import ReactDOM from "react-dom";

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.TimerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.TimerID);
  }
  
  tick() {
    this.setState({
      date: new Date(),
    });
  }
  
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}</h2>
      </div>
    );
  }
}

ReactDOM.render(<Clock />, document.getElementById("root"));

 

Clock 컴포넌트가 처음 DOM에 렌더링(마운팅)됐을 때, setInterval 함수를 통해 타이머를 설정했습니다. 타이머는 1초마다 setState()를 통해 state를 업데이트합니다. 매 초마다 setState()가 호출되므로, 업데이트 할 때 블록 안에 있는 메서드들이 실행될 것이지만, 우리가 정의한 메서드는 없으므로 매초 setState()에 따른 render()가 호출될 것입니다.

리액트 컴포넌트의 생명주기(lifecycle)

 

그리고 Clock 컴포넌트가 DOM에서 삭제될 때 clearInterval()을 통해 타이머를 해제합니다.

 

이제 코드를 실행하면, 매초 시간이 바뀌는 시계가 출력될 것입니다.

 


constructor를 호출하는 모양이 위와 같은지, constructor에서 setState()를 호출하면 안 되는지, static getDerivedStateFromProps()와 같은 메서드는 어떤 기능을 하는지 등 언급했지만 제대로 짚고 넘어가지 않은 부분이 있습니다. 당장 궁금하다면 리액트 공식문서를 보는 것이 역시 최고일 것입니다. 하지만 당장은 함수형 컴포넌트를 보면서 생겼던 궁금증을 해결하고 싶으니, 클래스형 컴포넌트에 있는 생명주기 메서드들은 아주 살짝 맛만 보고 넘어가겠습니다. 프로젝트를 하면서 혹은 공부를 하면서 궁금한 점이 생기면 다시 진입점인 여기로 돌아와서 내용을 발전시켜야겠습니다.

 

이번 글은 여기서 마무리하고, 다시 함수형 컴포넌트로 돌아가서 useStateuseEffect는 클래스형 컴포넌트의 생명주기 메서드의 어느 역할을 맡고 있는지 살펴보면서 궁금증을 해결해야겠습니다.