React) 3. State와 Lifecycle

divedeepp·2022년 2월 2일
0

React

목록 보기
3/11

들어가는 말

이전 글에서 구현한 시계 예시를 다시 살펴보자.

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

이번 글에서는 component를 완전히 재사용 가능하게하고 캡슐화하는 방법을 다룰 것이다.

위 시계 예시로 Clock component를 만들고, setInterval() 함수의 도움없이 스스로 타이머를 설정하고 매 초 스스로 렌더링을 업데이트하는 기능을 구현할 것이다.


State

우선, 지금까지 배운 내용을 토대로 시계를 생성하는 기능을 component로 캡슐화하는 것부터 시작하자.

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

다음은 타이머를 설정하고 매 초 UI를 업데이트하는 것도 Clock component의 세부기능으로 구현하는 것인데, 지금까지 배운 내용들로는 불가능하다.

우리는 아래 코드처럼 render() 메서드를 한 번만 호출하고도 Clock component가 스스로 시간을 업데이트 하도록 만들어야 한다.

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

이 기능을 구현하기 위해서는 Clock component에 state를 추가해야한다. state는 props와 유사하지만, private하고 component에 의해 완전히 제어된다.

state를 추가하기 위해서는 클래스 component를 사용해야한다. 함수 component를 클래스 component로 변환하는 방법을 살펴보자.

함수에서 클래스로 변환하기

네 단계로 Clock과 같은 함수 component를 클래스 component로 변환할 수 있다.

  1. React.Component를 extend하는, 함수 component와 동일한 이름의 클래스를 생성한다.
  2. render() 라고 부르는 빈 메서드를 추가한다.
  3. 함수의 body부분의 코드를 render() 메서드 안으로 옮긴다.
  4. render() 메서드의 body 안에 있는 props를 this.props로 변경한다.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

이것은 local state와 lifecycle 메서드와 같은 부가적인 기능을 사용할 수 있게 해준다.

클래스에 local 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를 정의하는 클래스 생성자를 추가한다. 클래스 component는 항상 props로 기본 생성자를 호출해야 한다.
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>
    );
  }
}
  1. <Clock /> element에서 date attribute를 삭제한다.
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이 스스로 타이머를 설정하고 매초 스스로 업데이트 하도록 구현해보자.

lifecycle 메서드를 클래스에 추가하기

많은 component가 있는 애플리케이션에서 component가 삭제될 떄, 해당 component가 사용 중이던 리소스를 확보하는 것이 중요하다.

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

또, Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제하려고 한다. 이것은 React에서 unmounting이라 한다.

클래스 component에서 특별한 메서드를 선언하여 component가 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>
    );
  }
}

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

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

componentDidMount() 메서드는 component 출력값이 DOM에 렌더링된 후에 실행된다. 이 메서드에 타이머를 설정한다.

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

componentWillUnmount() 메서드 안에 타이머를 해제하는 기능을 구현해보자.

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

마지막으로 Clock component가 매 초 작동하도록 하는 tick() 메서드를 구현해보자. local 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() {
    setInterval(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. <Clock />가 ReactDOM.render()로 전달되었을 때 React는 Clock component의 생성자를 호출한다. 시계는 현재 시각을 표시해야 하기 때문에 생성자는 현재 시각이 포함된 객체로 this.state를 초기화한다.
  2. React는 Clock component의 render() 메서드를 호출한다. 이를 통해, React는 화면에 표시되어야할 내용을 알게 된다. 그 다음 React는 시계의 렌더링 출력값을 일치시키기위해 DOM을 업데이트한다.
  3. 시계 출력값이 DOM에 삽입되면, React는 componentDidMount() 메서드를 호출한다. 그 안에서 Clock component는 매 초 component의 tick() 메서드를 호출하기 위해 타이머를 설정하도록 브라우저에 요청한다.
  4. 매 초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock component는 setState() 메서드에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 한다. setState() 호출 덕분에 React는 state가 변경된 것을 인지하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함한다. React는 이에 따라 DOM을 업데이트한다.
  5. Clock component가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 메서드를 호출한다.

State를 올바르게 사용하기

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

state를 직접 수정하면 component는 다시 렌더링되지 않는다.

this.state.comment = 'Hello';

대신에 setState() 메서드를 사용한다.

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

this.state를 지정할 수 있는 유일한 순간은 생성자이다.

state의 업데이트는 비동기적일 수도 있다.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

따라서, this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당값에 의존해서는 안된다.

아래의 코드는 카운터 업데이트를 실패할 수도 있다.

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

이를 방지하기 위해 객체보다는 함수를 인자로 사용하는 setState()를 사용한다. 그 함수는 이전 state를 첫 번째 인자로 사용하고, 업데이트가 적용된 시점의 props를 두 번째 인자로 사용할 것이다.

this.setState((state, props) => ({
  counter: state.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는 완전히 대체된다.


데이터는 아래로 흐른다.

부모 component나 자식 component 모두 특정 component가 stateful한지 stateless인지 알 수 없고, 그들이 함수 component인지 클래스 component인지에 대해서 관심을 가질 필요도 없다.

이 때문에 state는 종종 local 또는 캡슐화라고 부른다. state는 소유하고 설정한 component 이외에는 어떠한 component에도 접근할 수 없다.

대신, component는 자신의 state를 자식 component에 props로 전달할 수 있다.

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

FormattedDate component는 date를 자신의 props로 받을 것이고, 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못한다.

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

일반적으로 이를 하향식 또는 단방향식 데이터 흐름이라고 한다. 모든 state는 항상 특정한 component가 소유하고 있으며, 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 아래에 있는 component에만 영향을 미친다.

모든 component가 완전히 독립적이라는 것을 보여주기 위해 아래의 예시를 살펴보자. 각 Clock은 자신만의 타이머를 설정하고 독립적으로 업데이트 한다.

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

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
profile
더깊이

0개의 댓글