4.States and Lifecycle

(React 공식문서의 main concepts 번역 글입니다.)

함께보면 이해가 쏙쏙

  1. https://youtu.be/xJSNDGEvMwE
  2. https://youtu.be/jAVTHS3YGNI
  3. https://youtu.be/isCp8-dP050

state는 component의 상태이다.
props가 component에게 주어지는 data라면
state는 이 props의 값이 변할 때
변하는 props의 data를 component가 스스로 업데이트하여
UI로 표시할 수 있게 한다.

lifecycle은 component가
만들어지고 렌더되고 사라지는 일련의 과정이다.

이 lifecycle과 state는 어떠한 연관이 있을까?
아래 계속 변하는 props를 가진 tick component 예제를 통해
lifecycle과 state의 관계
component가 스스로 업데이트하는 과정 에 대해 알아보자

변하는 props값을 가진 tick component

우리는 props로 주어지는 data 값의 변화를
UI에 업데이트하는 방법을 1가지 배웠다.

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

그 방법은 아래의 과정을 거친다.

  1. 바뀐 props를 argument로 주어 component를 실행한다.
  2. component가 return한 새로운 element를 ReactDOM.render()에게 argument로 전달한다.
  3. ReactDOM.render()를 다시 실행한다.(하이라이팅된 부분)

값이 변할때마다 위 과정을 모두 거치는 것은 비효율적이다.
우리는 component가 변하는 props값을 스스로 업데이트하길 원한다.
어떻게 가능할까?
props 값이 변하는 것을 UI에 업데이트하는 방법은 1가지가 있다. 아래 tick component를 통해 그 방법을 알아보자.

encapsulating tick component

tick component는 Clock component를 extract하여
렌더링 함수와 react element을 분리할 수 있다.

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 component 즉, tag가 포함된 react element를 extract했다. 그 결과, UI요소인 tag들이 encapsulated 되어
어떤 tag를 포함하고 있는지 보이지 않게 되었다.
기존에 tick component에는 렌더링 함수만 남아있게 되었다.

하지만 아직 해결하지 못한 문제가 있다.
Clock component에게 props로 date 값이
고정적으로 주어지고 있으므로(하이라이팅된 부분)
매초마다 바뀌는 date 값을 표시하기 위해 setInterval로 tick을 계속 다시 실행해야하는 문제가 남아있다.

Clock component가 스스로 업데이트하길 원해

우리는 Clock component에게 date값을 props로 주지 않아도
Clock component가 스스로 업데이트 하길 원한다. 마치 아래 코드에서 하이라이팅된 부분처럼!

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

위 코드처럼 변하는 props를 반복해서 주지 않아도
Clock component가 스스로 업데이트 하기 위해서는 Clock component에게 state를 주어야한다.

state는 props와 비슷하지만 private하고 component에 의해 완전하게 controll 된다.

state를 주기 위해서는 Clock component가 function에서 class로 바뀌어야 한다.

function component를 class component로 바꾸기

아래와 같은 과정을 통해 function에서 class로 바꿀 수 있다.

  1. class keyword를 사용해서 같은 이름의 component를 선언한다 (이때 해당 component는 React.component에게 inheritance 받는다)
  2. render() method를 class 안에서 선언한다
  3. render할 element를 return해주는 부분을 render()안으로 옮긴다
  4. render()안에서 props를 this.props로 바꾼다
  5. 기존의 function component를 지운다
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

위에 render() method는 업데이트 될때마다 실행된다.
하지만 같은 DOM node에서 Clock을 render한다면
Clock class는 한개의 instance를 만들고
그 instance가 계속 사용되어 진다.

그 결과 state, lifecycle method 등의
추가기능을 사용할 수 있다.

class component에 state 추가하기

아래 과정을 통해 date라는 props를 state로 바꿀 수 있다.

  1. 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>
        );
      }
    }
  2. class constructor를 선언하고 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에 props를 상속한다.
    (class component는 항상 super(props)로
    부모 class로부터 props를 상속 받아서 시작한다)

    constructor(props) {
      super(props);  this.state = {date: new Date()};
    }
  3. 기존 Clock component에 props로 주었던 date를 지운다.

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

    나중에 바뀌는 시간을 표시하기 위한 코드를
    Clock component에 직접 추가한다.

최종적인 결과는 아래와 같다.

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 component는 스스로 업데이트 할 수 있게 될 것이다.

class component에 lifecycle method 추가하기

Clock component가 DOM에 반복해서 render되는 순간을 캐치하기위해
타이머를 설정해야 한다.
component가 DOM에 렌더되는 순간을 mounting 라고 한다.

또한, 이 Clock component가 만든 DOM이 삭제될 때
타이머도 같이 삭제되어야 한다.(메모리 관리를 위해)
component가 DOM에서 삭제되는 순간을 unmounting 라고 한다.

lifecycle method를 활용하면
이 타이머가 울릴 때, 실행되어야 할 코드를 추가할 수 있다.

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와 componentWillUnmount라는
lifecycle method를 위와 같이(하이라이팅된 부분) 추가했다.

componentDidMount

component가 return한 element가 DOM에 render된 후
실행되는 method이다.

아래 코드를 추가해주면
Clock component가 DOM에 render된 후
tick을 반복 실행하는 타이머가 설정된다.

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

이때, timerID는 바로 this.timerID로 저장할 수 있다.

data flow와 연관 있는 것들(props, state)이 아닌 다른 것들(timerID)은 얼마든지 this에 바로 저장할 수 있다.

componentWillUnmount

component가 return한 element가 DOM에서 제거된 후
실행되는 method이다.

componentWillUnmount를 활용하면 설정했던 타이머를 제거할 수 있다.
(component가 제거되면 관련된 resource(timer 등)를
함께 제거해야 메모리 낭비가 없다.)

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

tick component를 Clock component안에 넣기

tick은 기존에 렌더링 함수를 분리한 것이었다.
하지만 state 값, 즉 바뀌는 시간을 수정하는 method가 되어야 한다.

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

setState라는 function을 통해 state값을 수정할 수 있다.
(setState에 대한 자세한 설명은 아래에서 추가로 하겠다)

완성된 Clock component(state & lifecycle method 추가)

우선 완성된 clock component의 코드를 보자

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

ReactDOM.render()가 아예 밖으로 빠졌다.
Clock component는 class로 선언되어
state와 lifecycle method가 추가될 수 있었다.

위 코드가 실행되는 과정은 아래와 같다.

  1. ReactDOM.render()가 실행되면 React는 <Clock />를 call한다.
  2. Clock component의 constructor가 실행된다.
  3. Clock이 return하는 element, 즉 Clock class의 instance는
    super(props)를 통해 Clock의 props를 상속받는다.
  4. this.state에게 현재 시각(new Date()의 return 값)이 할당된다.
    (나중에 이 state 값은 업데이트 된다)
  5. React가 Clock component의 render()를 call한다.
    (이 과정을 통해 React는 무엇을 화면에 표시해야하는지 알아차린다)
  6. React는 Clock component의 render()의 return값과
    일치하도록 DOM을 업데이트 한다.
  7. Clock component의 render()의 return값이 DOM에 소속되면
    즉, DOM이 업데이트되면 React는 componentDidMount()를 실행한다.
  8. componentDidMount() 안에서 Clock component는 브라우저에게
    tick function을 1초에 1번 call하도록 타이머 설정을 요청한다
  9. 1초에 1번 브라우저는 tick function을 call한다.
    tick function 안에서 Clock component는
    새로운 현재 시간 obejct를 argument로 받아 setState를 call한다.
  10. setState를 call했기 때문에 React는 state 값이 변했음을 안다.
    state가 변하면 React는 Clock component의 render()를 다시 call한다.
    (무엇을 화면에 표시해야하는지 그 차이를 React가 알아차리기 위해)
  11. 이번에는 render() 안에 this.state.date 값이 달라질 것이다.
    따라서 render()의 return값이 state의 변화를 반영하여 업데이트 되고
    그 결과, React도 render()의 return값의 변화를 반영하여 DOM을 업데이트한다.
  12. 만약 Clock component가 DOM에서 제거된다면
    React는 componentWillUnmount를 실행하고 타이머 역시 제거된다.

state에 대해 주의해야 할 부분들

state를 수정하는 유일한 방법

반드시 setState()를 사용하자.

절대 state에 수정된 값을 직접 할당하지 마라

state에 값을 직접 할당할 수 있는 곳은
constructor안에서 초기값을 할당할 때 뿐이다.

setState는 비동기이다

리액트는 같은 object를 argument로 받은 setState를
여러번 call하면 한번만 실행한다.(performance를 위해서)

this.props와 this.state는 비동기적으로 업데이트 되기 때문에
다음 state값을 계산할 때 현재 props와 state값을 사용하면 안된다.

아래 코드는 state를 완벽하게 업데이트할 수 없다.

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

이 문제를 해결하기 위해서는 setState의 argument로
object대신 function을 주어야 한다.

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

이 function은 바뀌기전 state값과 업데이트 될 때 props값을 argument로 받는다.

state의 업데이트는 merge된다

setState()를 call하면 React는 setState()의 argument로 받은 object를 현재 state 값에 merge한다.

아래처럼, state값이 몇개의 key-value 페어로 이루어져 있다면

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

setState()를 여러번 call해서 state안에 각 key-value 페어를 수정할 수 있다.

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

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

이때 merge는 shallow copy이다.
따라서 this.setState({comment})
오직 comments만 수정한다.(posts는 수정하지 않는다.)

Data 는 한방향(부모에서 자식)으로만 전달 된다

부모 component나 자식 component 모두
어떤 특정 component가 stateful한지 stateless한지,
혹은 function component인지 class component인지 알 수 없다.

이것이 바로

state는 local하다 혹은 encapsulated하다 라고 말하는 이유이다.

component는 다른 component의 state에 접근하거나 출처를 알 수 없다.

유일하게 접근하는 방법이 있다. component는 자신의 상태를 하위 component에게 props로 전달할 수 있다.

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

또한 사용자 정의 component에게도 props로 전달할 수 있다.

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

FormattedDate component는 date를 props로 받는데
이 date에 부모 component의 state 값이 할당되어 있으므로
접근할 수 있다. 하지만 FormattedDate가 state의 출처를 알 수는 없다.

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

그저 props로 받아서 값에 접근할 수 있을 뿐이다.

이것을 Top-down data flow라고 부른다.

React는 아래와 같은 특징을 가진다.

  • 어떤 state는 항상 특정 component에게 소유되어져 있다.
  • 그 state로부터 나온 어떠한 data 혹은 UI들은 자식 component들에게만 영향을 줄 수 있다.

props를 부모 component에서 자식 component로 흐르는 폭포라고 가정하면 state는 중간 중간 component들로부터 추가되는 물줄기라고 할 수 있다.

모든 component들이 독립적으로 구성되어져 있다.
아래 App component를 보자.

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

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

총 3개의 Clock이 있다.
각 Clock은 각자의 타이머가 설정되며 독립적으로 업데이트 된다.
React 앱에서 component가 stateful한지 혹은 stateless한지는 중요하지 않다. 왜냐하면 바뀔 수 있기 때문이다. 또한 Stateful한 component 내부에서 stateless한 component를 사용할 수 있으면 그 반대도 가능하다.


[david hwang]
Written by@[david hwang]
깊게 이해하고 넓게 소통합니다.

GitHubMedium