[주요 개념] State and Lifecycle

jiseung·2021년 12월 9일
0

React

목록 보기
5/5

여기서는 React 컴포넌트의 state와 생명주기의 개념을 설명한다.
여기서 상세 컴포넌트 API를 볼 수 있다.

지난번에 똑딱이는 시계 예제를 보았다. Rendering Elements에서 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);

Try it on CodePen

여기서 Clock 컴포넌트를 재사용 가능하고 캡슐화 되도록 만드는 방법을 배웠다. 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);

Try it on CodePen

그런데 여기서는 중요한 조건 하나를 놓치고 있다: Clock이 타이머를 설정하고 매초 UI를 갱신한다는 사실은 Clock의 구현 세부사항이어야 한다.

이상적으로는 이것을 한번만 작성하고 Clock이 스스로 갱신하도록 해야한다.

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

이것을 구현하기 위해서, "state"Clock 컴포넌트에 추가해야 한다.

State는 props와 비슷하지만 private하고 완전히 컴포넌트에 의해 컨트롤된다는 점이 다르다.


함수를 클래스로 전환하기

Clock같은 함수형 컴포넌트를 클래스로 바꾸는 5가지 단계이다:
1. React.Component를 extends하는 같은 이름의 ES6 class를 생성한다.
2. render()라고 부르는 비어있는 메서드를 추가한다.
3. 함수 내용을 render() 메서드로 옮긴다.
4 render() 안에 있는 propsthis.props로 대체한다.
5. 남아있는 빈 함수 선언을 삭제한다.

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

Try it on CodePen

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

render 메서드는 업데이트가 일어날 때마다 호출된다. 하지만 같은 DOM노드에 <Clock />을 렌더링하면 Clock클래스에서 오직 하나의 인스턴스만 사용된다. 이건 local state와 lifecycle 메서드 같은 부가적인 특징을 사용할 수 있게 해준다.

Local State를 Class에 추가하기

date를 props에서 state로 세단계에 걸쳐 이동해보겠다:
1.render()메서드에서 this.props.datethis.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 생성자를 추가한다:
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. <Clock /> element로부터 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')
);

Try in on CodePen

다음으로, Clock이 매초 자신의 타이머를 설정하고 스스로를 갱신하도록 만들어보자.

클래스에 Lifecycle 메서드 추가하기

여러 컴포넌트들로 이루어진 애플리케이션들에서, 컴포넌트가 없어질 때 컴포넌트가 차지하고 있던 자원을 해제하는 것이 중요하다.

Clock이 DOM에 처음 렌더링될 때마다 타이머를 설정하고자 한다. 이것을 React에 "mounting"한다고 한다.

또한 Clock에 의해 생성된 DOM이 제거될 때마다 타이머를 클리어하고자 한다. 이를 React에 "unmounting"한다고 한다.

컴포넌트를 mount하고 unmount할 때 코드를 실행하기 위해 컴포넌트 클래스에 특별한 메서드들을 선언할 수 있다:

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

이러한 메서드들을 "lifecycle methods"라고 부른다.

컴포넌트 output이 DOM에 렌더링된 후에 componentDidMount() 메서드를 실행한다. 여기서 타이머를 설정하는 것이 좋겠다:

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

어떻게 타이머 ID를 this(this.timerID)에 저장하는지 보자.

this.props가 React 자체에 설정되고 this.state가 특수한 의미를 갖지만, timer ID처럼 데이터 플로우에 해당되지 않는 무언가를 저장하고 싶다면, 수동으로 클래스에 부가적인 field를 편하게 추가해도 된다.

componeneWillUnmoun() lifecycle 메서드에서 타이머를 없애보자:

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

이제 Clock 컴포넌트가 매초 동작하는 tick()이라는 메서드를 구현해보자:

컴포넌트 로컬 state 업데이트를 위해 this.setState()를 사용할 것이다:

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

Try it on CodePen

이제 시계는 매 초 똑딱인다.

빠르게 이게 어떻게 진행되는지 그리고 어떤 순서로 메서드가 호출되는지 뜯어보자:

  1. <Clock />ReactDOM.render()로 넘겨질 때, React는 Clock 컴포넌트의 생성자를 호출한다. Clock이 현재 시간을 보여주고 싶어하기 때문에, 이것은 현재시간을 포함하는 객체로 this.state를 초기화한다. 이 state는 나중에 업데이트할 것이다.
  2. React는 그리고 Clock 컴포넌트의 render() 메서드를 호출한다. 이것이 React가 화면에 무엇을 보여주어야 할 지 알게 되는 방법이다. React는 그리고 Clock의 출력 렌더링과 일치하게 DOM을 업데이트한다.
  3. Clock 출력이 DOM에 삽입될 때, React는 componeneDidMount() lifecycle 메서드를 호출한다. 이 안에서, Clock컴포넌트는 브라우저가 컴포넌트의 tick() 메서드를 매초 호출하기 위해 타이머를 설정하도록 요청한다.
  4. 매초 브라우저는 tick() 메서드를 호출한다. 이 안에서 Clock 컴포넌트는 현재시간을 포함하는 객체로 setState()를 호출함으로써 UI 업데이트를 진행한다. setState()를 호출함으로써, React는 state가 변했다는 것을 알게 되고 무엇이 화면에 반영되어야하는지 알기 위해 render()메소드를 재호출한다. 이때, render() 메서드 안의 this.state.date가 바뀌고, 렌더링 출력은 업데이트된 시간을 포함한다. React는 DOM을 일치하게 업데이트한다.
  5. Clock 컴포넌트가 DOM으로부터 삭제된 적이 있다면, React는 ComponeneWillUnmount() lifecycle 메서드를 호출해서 타이머가 멈추도록 한다.

State를 알맞게 사용하기

setState()에 대해 알아야 할 3가지가 있다.

State를 직접 수정하지 않는다.

예를 들어, 이것은 컴포넌트를 리렌더링하지 않는다.

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

대신 setState()를 사용해야 한다:

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

this.state는 생성자에서 유일하게 할당할 수 있다.

State는 비동기적으로 업데이트될 것이다.

React는 성능을 위해 여러개의 setState() 호출해 단일 업데이트를 진행할 수 있다.

this.props 그리고 this.state는 비동기적으로 업데이트되기 때문에, 다음 state를 계산하기 위해 그 값에 의존해서는 안된다.

예를 들어, 이 코드는 counter를 업데이트하는데 실패한다:

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

이를 고치기 위해, 객체가 아닌 함수를 받아들이는 setState()의 두번째 form을 사용하자. 함수는 첫번째 인자로 이전 state를 받고, 업데이트 시점의 props를 두번째 인자로 적용된다:

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

위에서 화살표 함수를 사용했지만, 일반 함수로도 작동한다.

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

State Updates are Merged

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로 대체된다.


The Data flows Down

부모, 자식 컴포넌트 둘다 어떤 컴포넌트가 stateful한지 stateless한지 알지 못한다. 그리고 그것이 함수인지 클래스인지도 신경쓰지 않는다.

이것이 state가 종종 local로 호출되거나 캡슐화되는 이유이다. 이것은 그 소유이거나, 설정되지 않은 어떤 컴포넌트의 접근도 허용하지 않는다.

컴포넌트는 그 state를 props로서 자식 컴포넌트에게 넘겨줄지 선택한다:

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

FormattedDate 컴포넌트는 date를 props로 받고, 이것이 Clock의 state로부터 왔는지, Clock의 props로부터 왔는지 손으로 쓰여진 것인지 알지 못한다:

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

Try it on CodePen

이것을 보통 "top-down"혹은 "unidirectional" data flow라고 부른다. 어떤 state든지 어떤 특정한 컴포넌트에 소속되어있고, 그 state로부터 전달된 어떤 데이터나 UI든지 오직 그것이 있는 트리 "아래"에 있는 컴포넌트에만 영향을 줄 수 있다.

컴포넌트 트리를 props의 폭포라고 가정해본다면, 각각의 컴포넌트의 state는 마치 임의의 점에서 합쳐져 함께 아래로 흐르는 부가적인 수자원이다.

모든 컴포넌트가 완전히 독립되어있음을 보기 위해, 세개의 <Clock>을 렌더링하는 App컴포넌트는 생성해보자.

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

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

Try it on CodePen

각각의 Clock은 자체 타이머를 설정하고 독립적으로 업데이트 한다.

React 앱에서, 컴포넌트가 stateful한지 stateless한지 시간이 흐르면서 변화하는 컴포넌트 디테일 구현에서 고려된다. stateless 컴포넌트를 stateful 컴포넌트에서 사용할 수 있고, 그 반대도 마찬가지이다.

profile
Frontend Web Developer

0개의 댓글