[React] State 와 라이프사이클

State 와 라이프사이클


지금까지 우리는 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);

이 섹션에서는 재사용가능하고 캡슐화된 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);

그러나 중요한 요구사항이 하나 빠져있습니다.
Clock 이 타이머를 설정하고 매 초 UI 를 업데이트 하는 것은 Clock 의 구현 세부사항이어야 합니다.

이상적으로 Clock 은 한번만 작성하고 자체적으로 업데이트 시키려고 합니다.

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

이걸 구현하기 위해, Clock 컴포넌트에 “state”를 추가할 필요가 있습니다.
State 는 props 와 비슷하지만 컴포넌트에 의해 완전히 제어되며 private 속성입니다.
이전에 언급 했던 대로 클래스로 정의한 컴포넌트에는 몇가지 추가 기능이 있습니다.
로컬 state 는 클래스에서만 사용 가능한 기능입니다.


함수를 클래스로 변환


Clock 같은 함수형 컴포넌트를 클래스로 변환하려면 다섯 단계를 진행합니다.

  1. ES6 class 를 같은 이름으로 만들고, React.Component 를 확장합니다.
  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>
    );
  }
}

Clock 은 이제 함수 대신 클래스로 정의합니다.
이를 통해 로컬 state 나 라이프사이클 훅 같은 추가 기능을 사용할 수 있습니다.


Class 에 로컬 state 추가하기


date 를 props 에서 state 로 옮기기 위해서 세 단계를 진행합니다.

  1. render() 메서드 내의 this.props.date 를 this.state.date 로 바꿉니다.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. this.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>
    );
  }
}

props 를 기본 생성자에 어떻게 전달하는 지 살펴보길 바랍니다.

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

클래스 컴포넌트는 항상 props 와 함께 기본 생성자를 호출합니다.

  1. 요소에서 date prop 을 삭제합니다.

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

나중에 타이머 코드를 컴포넌트 자체에 다시 추가합니다.

이 결과는 다음과 같은 코드가 됩니다.

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 에 자체 타이머를 설정하고 매 초마다 자체적으로 업데이트 하는 걸 만들어봅시다.


클래스에 라이프사이클 메서드 추가하기


많은 컴포넌트를 가진 어플리케이션에서, 컴포넌트가 제거될 때 리소스를 풀어주는 건 아주 중요한 일입니다.
Clock 이 DOM 에 최초로 렌더링 될 때 타이머를 설정 하려고 합니다. React 에서 이를 mounting 이라고 부릅니다.
그리고 DOM 에서 Clock 을 삭제했을 때 타이머를 해제 하려고 합니다. React 에서 이를 unmounting 이라고 부릅니다.
컴포넌트가 마운트 (mount) 되고 언마운트 (unmount) 될 때 특정 코드를 실행하기 위해 컴포넌트 클래스에 특별한 메서드를 선언할 수 있습니다.

// Life Cycle
// Render: componentWillMount() -> render() -> componentDidMount()
// Update componentWillReceiveProps() -> shouldComponentUpdate() -> componentWillUpdate() -> render() -> componentDidUpdate()

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

  componentDidMount() {}

  componentWillUnmount() {}

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

이런 메서드들을 “라이프사이클 훅” 이라고 부릅니다.
componentDidMount() 훅은 컴포넌트 출력이 DOM 에 렌더링 된 이후 동작합니다.
이 부분이 타이머를 설정하기 좋은 지점입니다.

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

this 에 timer ID 를 어떻게 저장하는 지 살펴봅시다.

렌더링과 관련된 데이터 흐름에서, this.props 는 React 에 의해 설정되고 this.state 는 특별한 의미를 갖고 있습니다.
반면 위와 같이 데이터 흐름에 참여하지 않는 무언가를 저장할 때 클래스에 직접 필드를 추가하는 것도 가능합니다.

이제 componentWillUnmount() 라이프사이클 훅에서 타이머를 종료할 것입니다.

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

마지막으로 Clock 컴포넌트에서 매 초마다 동작하는 tick() 이라는 메서드를 구현해봅시다.
this.setState() 를 사용해서 컴포넌트 로컬 state 에 대한 업데이트를 예약합니다.

// 리액트는 최초 렌더를 제외하고는 변화가 있어야 화면을 렌더합니다.
// componentDidMount는 1초마다 tick을 호출하고,
// tick은 state를 변경해줍니다.
// state가 변경되었기에 다시 렌더가 되고,
// 렌더가 완료된 후에 clearInterval이 이루어집니다.

class Clock extends React.Component {
  // 클래스 필드를 사용하면 생성자를 사용하지 않고도 초기 값을 지정할 수 있다.
  // state = { date: new Date() }
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
    // tick을 1초에 한번씩 호출하면 setState를 호출하니 화면을 다시 그려주게 되는 셈입니다.
  }

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

  tick() {
    this.setState({
      //  1. 상태를 바꿔주고, 2. 화면이 간접적으로 다시 그려지도록 해줍니다.
      date: new Date()
    });
  }

  // 상태로 부터 화면이 어떻게 그려져야 하는지를 render 메소드에 서술합니다.
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

이제 시계는 매 초 깜빡입니다.

어떤 작업을 했는 지와 메서드가 호출되는 순서를 간단히 요약해봅시다.

이 ReactDOM.render() 에 전달될 때, React 는 Clock 컴포넌트의 생성자 함수를 호출합니다.
Clock 이 현재 시각을 화면에 보여주어야 하기 때문에, 현재 시각을 포함하는 this.state 객체를 초기화합니다.
이 state 는 추후 업데이트됩니다.

React 가 Clock 컴포넌트의 render() 메서드를 호출합니다.
이를 통해 React 는 화면에 무엇을 보여줘야 하는지 알아냅니다.
그 다음 React 는 DOM 을 갱신해서 Clock 의 렌더링 출력과 일치시킵니다.

Clock 출력이 DOM 에 주입되었을 때, React 는 componentDidMount() 라이프 훅을 호출합니다.
그 안에서 Clock 컴포넌트는 브라우저에게 컴포넌트의 tick() 메서드를 초당 한 번씩 호출하는 타이머를 설정하라고 명령합니다.

브라우저에서 매 초마다 tick() 메서드를 호출합니다.
그 안에서 Clock 컴포넌트는 현재 시각을 갖고 있는 객체를 가지고
setState() 를 호출하여 UI 업데이트를 예약합니다.
setState() 호출 덕분에, React 는 상태가 변경된 걸 알게 됐고,
(setState()를 호출하면 화면이 다시그려지게 된다는 것도 기억해야 합니다.)
render() 메서드를 다시 한 번 호출해 화면에 무엇을 표시해야 할지 알 수 있습니다.
이번에는, render() 메서드 내의 this.state.date 가 달라지므로 바뀐 시간이 출력에 포함됩니다.
React 는 그에 따라 DOM 을 업데이트합니다.

만약 Clock 컴포넌트가 DOM 에서 삭제된다면,
React 는 componentWillUnmount() 라이프사이클 훅을 호출하기 때문에 타이머가 멈춥니다.


State 바르게 사용하기


  • State 를 직접 수정하지 마세요

이 코드는 컴포넌트를 다시 렌더링하지 않습니다.

// Wrong
this.state.comment = "Hello";

대신, setState() 를 사용하세요.

// Correct
this.setState({ comment: "Hello" });

this.state 를 할당할 수 있는 유일한 장소는 생성자 함수 내부입니다.


  • State 업데이트는 비동기일 수 있습니다

React 는 성능을 위해 여러 setState() 호출을 한 번의 작업으로 묶어서 처리하는 경우가 있습니다.
this.props 및 this.state 가 비동기로 업데이트될 수 있기 때문에,
다음 state 를 계산할 때 이 값을 신뢰해서는 안됩니다.
예를 들어, 카운터를 업데이트하는 이 코드는 실패할 수 있습니다.

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment
});

이 문제를 해결하기 위해 객체가 아닌 함수를 받는 두 번째 형식의 setState() 를 사용할 수 있습니다.
이 함수는 이전 state 를 첫 번째 인수로 받고, 두 번째 인수로 업데이트가 적용 될 때 props 를 받습니다.

// 이전 상태로부터 새 상태를 계산할 때는 콜백 형식의 setState를 사용해야 합니다.
// 객체를 바로 반환받음에도 중괄호를 괄호로 덮어줘야 합니다. 안그러면 에러가 발생합니다.

// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

위 예제에서는 arrow function 을 사용했지만, 평범한 함수도 동작합니다.

// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});


  • State 업데이트는 병합됩니다

setState() 를 호출할 때, React 는 넘겨받은 객체를 현재 state 에 병합합니다.
예를 들어, state 는 여러 독립적인 변수를 가질 수 있습니다.

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

그런 다음 개별 setState() 를 호출하여 아이템을 각각 업데이트할 수 있습니다.

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

이 때 얕은 병합을 수행하기 때문에, this.setState({comments}) 는
this.state.posts 는 그대로 두지만, this.state.comments 는 완전히 대체합니다.


  • 데이터는 아래로 흐릅니다

부모 컴포넌트나 자식 컴포넌트는 특정 컴포넌트의 state 유무를 알 수 없으며,
해당 컴포넌트가 함수나 클래스로 선언되었는 지 알 수 없습니다.
이것이 바로 state 가 ‘지역적이다’ 혹은 ‘캡슐화되었다’고 하는 이유입니다.
State 를 지정해 준 컴포넌트 외의 다른 컴포넌트에서는 state 에 접근할 수 없습니다.
컴포넌트는 자신의 state 를 props 로서 자식 컴포넌트에 내려줄 수 있습니다.

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

이는 사용자 정의 컴포넌트에서도 마찬가지로 동작합니다.

<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 props 에서 date 를 받지만</h2> 이 값이 Clock 의 상태인지, Clock 의 props 인지,</h2> 혹은 타이핑된 것인지 알 수 없습니다.</h2>

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

이런 데이터 흐름을 보통 “하향식(top-down)” 혹은 “단방향(unidirectional)” 데이터 흐름이라고 합니다.
모든 state 는 항상 특정 컴포넌트가 가지며, 해당 state 에서 파생된 모든 데이터와
UI 는 트리의 “아래(below)“에 있는 컴포넌트에만 영향을 미칩니다.

컴포넌트 트리를 props 의 폭포라고 상상해보면,
각 컴포넌트의 상태는 임의의 지점에서 추가되는 물과 비슷하고 또한 아래로 흐릅니다.

모든 컴포넌트가 실제로 격리되어있음을 보여주기 위해,
세 개의 을 렌더링하는 App 컴포넌트를 만들어봅시다.

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

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

각 Clock 은 자체적으로 타이머를 생성하고 독립적으로 바뀝니다.

React 앱에서, 컴포넌트가 state 를 갖고 있는지(stateful) 또는 갖지 않는지(stateless)는
구현 세부 사항으로 간주되어 시간이 지남에 따라 바뀔 수 있습니다.
state 를 가진 컴포넌트 내부에서 state 가 없는 컴포넌트를 사용할 수 있으며, 그 반대 경우도 마찬가지입니다.