4. state와 생명 주기

22143 단어 ReactReact

목표

  • state와 생명 주기 대해 톺아본다.

4. state와 생명 주기

  • props는 다음과 같은 주의사항이 있었다.

    모든 리액트 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 합니다.

  • 하지만, 컴포넌트 안에서 변경해야만 하는 값이 있을 수 있다. 이러한 경우 때문에 리액트에서는 state라는 것을 제공한다.

  • state에 대해 알아보기 위해 매 초마다 현재 시각을 출력하는 Clock 컴포넌트를 구현하면 다음과 같다.

4-1. state 추가하기

// Clock.jsx
import React, { Component } from "react";

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

export default function App() {
  return (
    <>
      <Clock date={new Date()} />
    </>
  );
}
  • Clock 컴포넌트에 new Date()값을 props로 넘겨주어 현재 시각을 나타낸다.
  • 하지만, props를 넘겨주는 것만으로는 매 초마다 현재 시각을 나타낼 수 없다.

🤔 의문

  • 어떻게 하면 매 초마다 현재 시간을 나타낼 수 있을까?

Clock 컴포넌트에 State를 활용하면 나타낼 수 있다. State는 props와 유사하지만 비공개이며 컴포넌트에 의해 완전히 제어된다.

  • props 대신 state를 사용하려면 다음과 같은 과정을 따라야 한다.

    • 먼저, 초기 this.state를 지정하는 class constructor를 추가한다.
    • 그 후 this.props.date를 this.state.date로 바꿔준다.
    • 참고로 클래스 컴포넌트는 항상 props로 기본 constructor를 호출해야 한다.
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>
    );
  }
}
  • 하지만, 이것만으로는 매 초마다 변하는 시각을 나타낼 수 없다.

4-2. 생명주기 추가하기

  • Clock이 처음 DOM에 렌더링 될 때마다 타이머를 설정하려고 한다.
  • 이것을 리액트에서는 마운팅이라고 한다.
  • 클래스 컴포넌트에서는 componentDidMount()를 이용해 마운팅할 때 원하는 기능을 추가할 수 있다.
  • 먼저, date 값을 업데이트 해주는 tick() 함수를 만들고 마운팅할 때 매 초마다 이 tick 함수를 호출해준다.
class Clock extends React.Component {
  // 생략 
  
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }
  
  tick() {
    this.setState({
      date: new Date()
    });
  }
  
  // 생략
}
  • 그리고 Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제해야 한다.
  • 이를 리액트에서 언마운팅이라고 한다.
componentWillUnmount() {
    clearInterval(this.timerID);
 }

그럼 다음과 같이 매 초마다 시각이 갱신된다.

  • 간단한 코드이지만 리액트는 많은 일을 하고 있다. 위 코드는 다음과 같이 동작한다.

    1. Clock 컴포넌트가 ReactDOM.render()로 전달되었을 때 리액트는 Clock 컴포넌트의 constructor를 호출한다. 그 후 this.state를 초기화 한다.

    2. 리액트는 Clock 컴포넌트의 render() 메서드를 호출한다. 그 후 리액트는 DOM을 업데이트한다.

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

    4. 매초 브라우저가 tick() 메서드를 호출하면 그 안에서 Clock 컴포넌트는 setState()에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다. setState() 호출하면 리액트는 state가 변경된 것을 인지하고 render() 메서드를 다시 호출한다. 이에 따라 DOM을 업데이트 한다.

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


[부록] 함수형 컴포넌트로 나타내기

  • 위 코드는 클래스 컴포넌트로 현재 시각을 출력하는 코드이다.
  • 이를 함수형 컴포넌트로 바꿔보면 다음과 같다.
import { useEffect, useState } from "react";

export default function Clock() {
  const [date, setDate] = useState(new Date());

  useEffect(() => {
    const timerID = setInterval(() => tick(), 1000);

    return () => {
      clearInterval(timerID);
    };
  }, []);

  function tick() {
    setDate(new Date());
  }

  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}
  • 함수형 컴포넌트는 constructor 대신 useState를 이용해 state 값을 초기화한다.
  • 또한, componentDidMount()와 componentWillUnmount() 대신 useEffect를 사용해 마운팅, 언마운팅 될 때 원하는 기능을 추가할 수 있다.

4-3. State

  • state를 사용할 때에는 다음과 같은 주의사항이 있다.

4-3-1. 직접 State를 수정하지 말아야 한다.

this.state.comment = 'Hello';
  • 위 코드는 컴포넌트를 다시 렌더링하지 않기 때문에 화면에 있는 출력값이 변하지 않는다.
this.setState({comment: 'Hello'});
// or
setState({comment: 'Hello'});
  • this.setState 혹은 함수형 컴포넌트라면 setState를 사용해야 한다.

4-3-2. State 업데이트는 비동기적일 수도 있다.

  • 리액트는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.
this.setState({
  counter: this.state.counter + this.props.increment,
});
  • 위 코드는 업데이트에 실패할 수 있다.
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
  • 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용하는 것이 안전하다.
  • 이전 state를 첫 번째 인자로 받고, 업데이트가 적용된 시점의 props를 두 번째 인자로 받기 때문이다.
  • 이것은 리액트 훅을 사용할 때 setState를 할 때 이전 값인 prev를 사용하는 것도 마찬가지이다.
const onClick = () => {
  setState((prev) => !prev);
};

출처

좋은 웹페이지 즐겨찾기