[React][공식문서] state와 생명주기

Gyuwon Lee·2022년 6월 1일
0
post-thumbnail

React 공식 튜토리얼을 바탕으로, 필요한 개념을 보충하여 학습한 기록입니다.

1. 엘리먼트 렌더링과 불변객체

앞선 글에서, 엘리먼트는 불변객체라고 했다. 즉 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없다. 엘리먼트는 정의되어 있는 형태 자체가 어떤 메소드도 갖지 못한다. 엘리먼트는 본질적으로 immutable decription object 다.

따라서, 리액트 앱의 UI를 업데이트하는 유일한 방법은 새로운 엘리먼트를 생성해서 이를 ReactDOM.render() 로 전달하는 것이었다.

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

위의 코드와 같이 말이다. 위 함수는 setInterval() 콜백을 이용해 초마다 ReactDOM.render()를 호출한다.

  • new Date().toLocaleTimeString() : 자바스크립트는 날짜와 관련된 정보를 제공하는 Date 내장객체를 제공한다.
    • new Date() 를 통해 Date 객체를 생성한 후, 메소드 체이닝으로 toLocaleTimeString() 를 호출한다. 이에 따라 사용자의 문화권에 맞는 시간표기법으로 현재 시간이 리턴된다.
  • setInterval() 콜백으로 1초에 한 번씩 tick 함수를 실행시킨다.
    • 매번 element 엘리먼트가 새롭게 생성되며, 이 때 각 엘리먼트마다 toLocaleTimeString() 에 의해 시간이 1초씩 증가되어 표시될 것이다.
    • ReactDOM.render() 역시 1초에 한 번씩 호출되어, UI가 새로운 엘리먼트와 같아지도록 매번 루트 DOM 노드 에 새롭게 렌더링한다.

여기서 이런 생각을 해볼 수 있다:

"리액트 컴포넌트가 재사용성을 위해 딱 한가지의 역할만 수행해야 한다면, 반대로 그 역할만큼은 한 컴포넌트 안에서 완전히 수행되어야 하지 않을까?"

즉, 타이머를 설정하고 매 초 UI를 업데이트하는 것이 컴포넌트 안에 구현되어 있어야 한다는 것이다. 이를 위해서 읽기 전용인 props 가 아니라, state 를 사용해야 한다.

stateprops 와 유사하지만, 비공개이며 컴포넌트에 의해 완전히 제어된다.


2. state란?

state컴포넌트 내부에서 바꿀 수 있는, 바뀔 수 있는 값이다. props는 부모 컴포넌트에서 설정하여 자식 컴포넌트로 전달하거나, 더 바깥의 컴포넌트에서 자기가 감싸고 있는 컴포넌트 내용을 표현하기 위해 사용했다. 앞서 props는 읽기 전용으로 쓰인다고 한 것이 그 뜻이다.

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

바로 이전 글에서 보았던 Comment 컴포넌트의 코드를 다시 보자.

  • 자신의 props.authorUserInfoprops.user에 들어갈 값으로 전달하고 있다.
  • Comment 컴포넌트에서, 그 내부의 UserInfo 컴포넌트 내용이 user={props.author}라고 표현할 수 있다.

이번에는 위에서 본 Clock 컴포넌트를 캡슐화해서 아래와 같이 적어 보자:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);
  • dateClock 내부에서 설정되어도 아무 상관이 없다. (부모 컴포넌트에서 설정된 시간과 연동되어야 하는 경우 등이 아니라면)
  • 다른 컴포넌트에 의존하지 않고 스스로 작동하는 시계를 만들고 싶다.

이를 위해 ReactDOM.render() 부분을 아래와 같이 바꿀 수 있다.

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

2-1. 컴포넌트의 state

이제, 본격적으로 위의 Clock 컴포넌트가 state를 가질 수 있게 코드를 수정해보며, propsstate로 대체했을 때 일어나는 일과 그 효과를 생각해 보겠다.

먼저, 기존에 함수 컴포넌트였던 Clock클래스 컴포넌트로 바꿔 보자.

React.Componentextends 하는 ES6 클래스를 생성한다.

class Clock extends React.Component {}

여기서 extends 키워드는 클래스를 다른 클래스의 자식으로 만들기 위해 class 선언 또는 class 식에 사용된다. 이러면 부모 클래스의 기능은 사용하면서 거기서 부가적인 기능을 추가 또는 수정하여 사용할 수 있다.

이 다음, render() 메소드를 추가하여 함수 컴포넌트가 리턴하던 엘리먼트를 render() 안으로 집어넣는다.

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

마지막으로는 props를 this.props로 변경한다.

<h2>It is {this.props.date.toLocaleTimeString()}.</h2>

위의 코드에서 this클래스를 확장하여 생성된 객체 (인스턴스)를 가리키게 된다. [참고]

즉, Clock 의 인스턴스를 가리키므로, this.propsClock 컴포넌트를 생성할 때 넘겨받은 props를 가리킬 것이다.

Clock 클래스의 props 가 아니라, 인스턴스의 props 다!

Clock은 이제 함수가 아닌 클래스로 정의된다.

render() 메서드는 업데이트가 발생할 때마다 호출되지만, 같은 DOM 노드로 <Clock />을 렌더링하는 경우 Clock 클래스의 단일 인스턴스만 사용된다.

DOM 노드 란?
HTML DOM은 노드(node)라고 불리는 계층적 단위에 정보를 저장하고 있다. HTML DOM은 이러한 노드들을 정의하고, 그들 사이의 관계를 설명해 주는 역할을 한다.
W3C HTML DOM 표준에 따르면, HTML 문서의 모든 것은 노드다. 노드 트리의 모든 노드는 서로 계층적 관계를 맺고 있다.

2-2. 클래스 컴포넌트의 장점: local state와 생명주기

이렇게 한 이유는, 함수 컴포넌트일 때

  • const element 등 변수를 만들어 리턴할 엘리먼트를 대입하고, ReactDOM.render() 메소드를 사용하여 렌더링 과정을 거친다.
  • 또는, 엘리먼트를 리턴하는 함수를 별도의 컴포넌트로 분리하고, 렌더링을 맡는 함수를 정의하여 ReactDOM.render() 에서 앞서 분리한 컴포넌트를 사용한다.

크게 위와 같은 2가지 방법으로 엘리먼트를 렌더링하던 과정을, 클래스 컴포넌트로 바꾸고 그 안에서 render() 함수 안에 바로 리턴할 엘리먼트를 집어넣어서 조금 더 간단하게 작성할 수 있기 때문이다.

더 중요한 차이점은, 클래스 컴포넌트만 인스턴스를 가지며, local state를 가질 수 있다

인스턴스 란?

  • 객체 지향 프로그래밍(OOP)에서 인스턴스(instance)는 해당 클래스의 구조로 컴퓨터 저장공간에서 할당된 실체이다.
  • 설계도를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체. 즉, 객체를 소프트웨어에 실체화 하면 그것을 '인스턴스'라고 할 수 있다.
  • 인스턴스란 클래스를 복제한 것으로, 클래스의 복제본을 만들어서 서로 다른 상태를 유지할 수 있도록 한다.

React에서는 클래스로 선언된 컴포넌트들만 인스턴스를 가질 수 있는데, 이를 컴포넌트 인스턴스 (Component Instance) 라고 부른다. 컴포넌트 클래스 내부에서 this 키워드를 통해 참조하는 대상에 해당한다.
개발자가 직접 생성, 수정, 삭제하며 다룰 일은 거의 없다. React가 알아서 해주기 때문이다.
local state를 저장하고 생명 주기 이벤트들에 대한 반응을 제어할 때 매우 유용하다.
이와 달리, 함수형 컴포넌트는 인스턴스를 갖지 않는다.

3. 클래스에 local state를 추가해 보자

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

이제 props 를 사용해서 부모 컴포넌트로부터 받아온 date 를 렌더링하고 있는 이 코드에서, dateprops 에서 state 로 옮겨 볼 것이다. 그렇게 함으로써 모든 정보가 컴포넌트 내부에서 관리 및 수정될 수 있다.

this.state 로 변경하기: constructorsuper

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

이제 <h2> 요소가 렌더링될 때 컴포넌트의 props 가 아닌 state 로부터 정보를 가져올 것이다.

그러기 위해서는 초기 state 를 지정해 주어야 한다.

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>
    );
  }
}

🔄 클래스 초기화 메서드 constructor()

초기 this.state 를 지정해 주기 위한 class constructor 를 추가했다. JS ES6 문법에서 constructor 메서드는 클래스의 인스턴스 객체를 생성하고 초기화하는 특별한 메서드다. constructor 를 사용하면 다른 모든 메서드 호출보다 앞선 시점인, 인스턴스 객체를 초기화할 때 수행할 초기화 코드를 정의할 수 있다.

따라서 클래스 컴포넌트 내부에서 constructor 메서드를 사용해 컴포넌트 인스턴스 생성과 동시에 초기 상태의 state 를 갖도록 할 수 있다.

👪 부모 생성자 호출 키워드 super()

위의 코드에서 또 하나 유의해야 할 점은 props 가 기본 constructor 에 전달되는 부분이다.

constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

공식문서에 따르면, "클래스 컴포넌트는 항상 props 로 기본 constructor 를 호출해야 합니다." 라고 한다. 번역이 약간 어색한데, props 를 인자로 해서 부모 클래스의 생성자를 호출하라는 뜻이다.

extends 를 사용하여 상속을 통해 새롭게 작성된 클래스를 '자식 클래스' 또는 '파생 클래스' 라고 한다. 파생 클래스에서는 super() 함수가 먼저 호출되어야 this 키워드를 사용할 수 있다. 그렇지 않을 경우 참조 오류가 발생한다.

오류가 발생하는 이유는 this 키워드가 사용되는 맥락 때문이다. 앞서 말했듯 클래스 인스턴스(객체)를 생성할 때 이 객체가 this 에 할당되는데, 이 때 상속받은 클래스 즉 파생 클래스는 객체를 this 에 할당하는 작업이 부모 생성자에서 처리된다. 그래서 부모 생성자를 실행하는 super() 함수를 실행하지 않고 this 키워드를 사용하면 참조 오류가 발생하는 것이다. 부모 생성자를 참조해야 하는데 호출해오지 않은 상태이기 때문이다.

위 과정을 반영해서 현재까지 다듬어진 Clock 코드는 아래와 같다:

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')
);

4. 클래스에 생명주기 메서드를 추가해 보자

리액트 공식문서에서는 이 '생명주기' 개념에 대해 마운팅언마운팅 단계만 간략히 설명하고 있다. 이 부분에 많은 시간을 쏟을 필요는 없지만, 조금만 더 자세히 짚어보면 좋을 것 같다.

🔄 생명주기란?

리액트는 컴포넌트를 기반으로 를 구성하는 라이브러리다. 당연하게도, 한 번 만들어낸 컴포넌트가 붙박힌 듯 페이지에 영구적으로 올라와 있을 필요는 없다. 그러다 보니 각각의 컴포넌트에는 라이프사이클, 즉 컴포넌트의 수명 주기가 존재한다. 컴포넌트의 수명은 보통 페이지에서 렌더링되기 전인 준비 과정에서 시작하여 페이지에서 사라질 때 끝난다.

위의 도표에서, '생성될 때' 를 마운트, '업데이트할 때' 를 업데이트, '제거할 때' 를 언마운트 단계라고 한다. 이렇게 각 단계를 지날때, 특정한 이벤트들이 발생한다. 즉 생명주기란 어떤 구체적인 기능을 지칭하는 말이 아니라, 컴포넌트의 생성부터 제거까지 각 특정 시점에 필요한 메서드들을 묶어 지칭하는 말이다.

React provides the developers a set of predefined functions that if present is invoked around specific events in the lifetime of the component. Developers are supposed to override the functions with desired logic to execute accordingly. (출처: Geeksforgeeks.org)

"리액트는 개발자들에게 사전 정의된 일련의 함수 집합을 제공한다. 이 함수들은 컴포넌트의 생명 주기 중 특정한 이벤트에 대해 호출되는 기능이다. 개발자들은 코드의 정확한 실행을 위해, 적절한 로직을 사용하여 이 함수들을 override하도록 요구된다." 라고 한다.

각 생명주기 별 메서드

아직 마운트 단계에 메서드들을 제외하고 나머지는 사용해본 적이 없을 뿐더러 다른 레퍼런스들을 읽고 이해하기에도 아직 조금 난해해서 업데이트, 언마운트 단계의 메서드들은 설명하지 않고 넘어가려고 한다.

1. 마운트

먼저 마운트될 때 호출되는 메서드들은 아래와 같다:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

📌 constructor

constructor 는 위에서 자세히 본 바 있는 메서드로, 컴포넌트의 생성자 메서드 에 해당한다. 초기 state 를 정의하기 위해, 컴포넌트가 만들어지면 가장 먼저 실행되는 메서드다.

constructor(props) {
    super(props);
    this.state = {date: new Date()};
}

📌 getDerivedStateFromProps

getDerivedStateFromProps 는 props 로 받아온 것을 state 에 넣어주고 싶을 때 사용한다.

static getDerivedStateFromProps(nextProps, prevState) {
    console.log("getDerivedStateFromProps");
    if (nextProps.color !== prevState.color) {
        return { color: nextProps.color };
    }
    return null;
} 

다른 생명주기 메서드와는 달리 앞에 static 을 필요로 하고, 여기서 특정 객체를 반환하게 되면 해당 객체 안에 있는 내용들이 컴포넌트의 state 로 설정된다. 반면 null 을 반환하게 되면 아무 일도 발생하지 않는다.

이 메서드는 컴포넌트가 처음 렌더링 되기 전에도 호출 되고, 그 이후 리렌더링 되기 전에도 매번 실행된다.

📌 render

컴포넌트를 렌더링하는 메서드다. 반드시 호출되어야만 DOM 노드에 렌더링되어 화면에 컴포넌트가 나타날 수 있다.


2. 업데이트

컴포넌트가 업데이트될 때 호출되는 메서드들은 아래와 같다:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

3. 언마운트

컴포넌트가 언마운트될 때 호출되는 메서드는 아래의 한 개뿐이다:

  • componentWillUnmount

리액트와 컴포넌트에 대한 이해를 얼른 높여 생명주기 내의 나머지 메서드들에 대해서도 추후 설명하는 글을 작성해야겠다.


clock 컴포넌트의 마운트 시점에 타이머 설정하기: this.setState

위에서 다듬어 온 clock 컴포넌트의 코드로 돌아오자.

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

  componentDidMount() {
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

먼저, constructorrender 메서드 사이에 componeneDidMount 메서드를 추가했다. componentDidMount() 메서드는 컴포넌트 출력물이 DOM에 렌더링 된 후에 실행된다. 따라서 Clock 요소가 페이지에 올라감과 동시에 타이머가 설정될 것이다.

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

setInterval() 함수는 0이 아닌 숫자값의 형태로 intervalID 를 리턴해서, 이 ID 값으로 setInterval 또는 setTimeout 함수에 의해 설정된 타이머들을 식별할 수 있다. clearInterval 함수의 인자로 이 ID를 넘기면 원하는 타이머만 중단시킬 수 있다.

따라서 this.timerID 필드에 setInterval() 호출의 리턴값을 저장함으로써 나중에 컴포넌트가 언마운트되기 직전 "1초에 한 번 this.tick() 메서드를 실행해라" 라는 이 setInterval 타이머를 중단할 수 있게 된다.

클래스에 수동으로 필드 추가하기

여기서 this.state 가 아니라 this.timerID 필드에 타이머 ID를 저장하고 있는 점에 유의하자! 타이머의 ID는 흘러야 하는 데이터가 아니다. 컴포넌트에서 stateprops 를 사용하는 데는 명확한 이유가 있다: 컴포넌트 내부에서 데이터를 업데이트하거나, 부모 컴포넌트가 제어권을 갖고 자식 컴포넌트에게 데이터를 흘려보내기 위해 사용되는 것이 위의 두 변수다. 그 반면 여기서의 this.timerID 는 그저 setInterval 가 리턴한 ID를 저장해두기 위한 필드이므로 클래스에 수동으로 추가한 것이다. 이걸 잘 구분하지 않으면 stateprops 를 사용하는 의미가 흐려질 수 있다.

이제 1초에 한번씩 tick() 메서드가 작동되고, 이 메서드가 앞서 this.state.date 로 변경해 둔 date 항목을 업데이트하면 매 초 시간이 가는 것처럼 보일 것이다.

tick() {
  this.setState({
    date: new Date()
  });
}

컴포넌트의 로컬 state는 반드시 this.setState() 를 통해 업데이트되어야 한다. 이제 tick() 메서드는 date 속성의 값을 새로운 Date() 객체로 업데이트할 것이다.

clock 컴포넌트의 언마운트 시점에 타이머 해제하기

componentWillUnmount() {
    clearInterval(this.timerID);
}

componentWillUnmount() 메서드는 이름처럼 컴포넌트의 언마운트 직전에 메서드의 내용을 실행시킨다. 즉, 이 메서드의 내용이 실행되고 나면 컴포넌트가 언마운트 될 것(will unmount) 이라는 뜻이다. 따라서 이 메서드의 내용으로 clearInterval 함수를 사용하여 타이머 인터벌을 없애 주자. 이 예시는 크게 보면 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 과정이라고 생각할 수 있다.

clearInterval()

clearInterval(intervalID)

clearInterval() 함수는 setInterval() 함수에 의해 설정된 반복작업을 중단(cancel) 한다. 앞서 말했듯 setInterval() 함수가 리턴한 intervalID 를 인자로 넘겨줘야 한다.


clock 컴포넌트 전체 코드 리뷰

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')
);
  1. ReactDOM.render()

    • 가장 먼저 ReactDOM.render() 내부의 <Clock /> 컴포넌트를 트랜스파일하는 작업이 일어난다. 리액트는 Clock 컴포넌트의 인스턴스를 생성하며 constructor 를 호출할 것이다. 그러면 constructor 내부에 정의해둔 대로, 현재 시각이 포함된 객체로 this.state 가 초기화된다. 나중에 이 state는 업데이트될 것이다.
  2. render()

    • 그다음 여전히 Clock 컴포넌트 내부에서, render() 메서드를 호출한다. 이를 통해 리액트는 화면에 표시되어야 할 내용(엘리먼트들)을 알게 된다. 그 다음 리액트는 Clock 의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트한다. ReactDOM.render() 의 실행이 완전히 한 번 이루어졌다.
  3. componentDidMount()

    • Clock 출력값이 DOM에 삽입되면, 리액트는 componentDidMount() 생명주기 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setInterval() 호출을 통해 매초 컴포넌트의 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.
  4. setState()

    • 이제 매초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setState() 에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다. setState() 호출 덕분에 리액트는 state가 변경된 것을 인지하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이 때 render() 메서드 안의 this.state.date 가 달라지고 렌더링 출력값은 업데이트된 시각을 포함한다. 리액트는 이에 따라 DOM을 업데이트한다.
  5. componentWillUnmount()

    • Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 리액트는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출한다.
profile
하루가 모여 역사가 된다

0개의 댓글