IT

[영어로 배우는 React]5. 컴포넌트 상태와 생명주기

생각파워 2022. 4. 11. 22:23

공식 페이지 : https://reactjs.org/docs/state-and-lifecycle.html

 

이번장에서는 컴포넌트 상태와 생명주기에 대해 알아보겠습니다. 

이전장에서 살펴봤던 똑딱 시계 예에서 UI를 업데이트할 때 root.render()를 사용했었습니다. 

const root = ReactDOM.createRoot(document.getElementById('root'));
  
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);
  }

setInterval(tick, 1000);

이렇게 사용했었습니다. 

이번 장에서는 똑딱 시계를 재사용 가능하고, 캡슐화된 Clock 컴포넌트로 만들어볼 예정입니다. Clock 컴포넌트는 타이머를 내장하고, 매 초마다 자신을 업데이트합니다. 아래는 Clock을 캡슐화한 것입니다. 

const root = ReactDOM.createRoot(document.getElementById('root'));

function Clock(props) {
  return (
    <div>
    	<h1>Hello, world!</h1>
        <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>  
  );
}

function tick() {
  root.render(<Clock date={new Date()} />);}

setInterval(tick, 1000);

여기서 중요한 요구사항을 놓치고 있습니다. Clock이 타이머를 세팅하고, 매초 UI를 업데이트하는 것은 Clock의 상세 구현이어야 한다는 것입니다. 이상적으로 우리는 한번 쓰고, Clock이 스스로 업데이트하기를 원합니다. 

root.render(<Clock />);

이렇게 말이죠.

이것을 구현하기 위해서 우리는 'state'라는 것을 Clock 컴포넌트에 추가할 겁니다. state는 props와 비슷하지만, 독립적이고, 컴포넌트에 의해 완전히 컨트롤됩니다. 

 

 

함수 컴포넌트를 클래스 컴포넌트로 변경하기

Clock과 같은 함수 컴포넌트는 다섯 가지 단계를 거쳐 클래스 컴포넌트로 변경할 수 있습니다. 

 

1. 함수 컴포넌트와 같은 이름으로 React.Component를 상속하여 ES6 클래스 생성

2. 클래스 안에 내용이 없는 render() 메서드 추가

3. 함수 컴포넌트 내의 내용을 render() 메소드 안으로 이동

4. 소스내에 props를 this.props로 변경

5. 함수 컴포넌트 부분을 삭제

 

위 과정을 거치면 Clock 컴포넌트가 

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

이렇게 만들어집니다. 이제 Clock은 함수 컴포넌트가 아니라 클래스 컴포넌트입니다.

render 메서드는 업데이트가 발생할 때마다 호출되지만, 동일한 DOM 노드로 랜더링 하는 한 Clock의 싱글 인스턴스가 사용됩니다. 이것 때문에 로컬 state나 생명주기 메서드와 같은 추가적인 기능을 사용할 수 있습니다.

 

클래스에 로컬 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>
    );
  }
}

2. 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를 인자로 기본 생성자를 호출합니다. super(props) 처럼요.

 

3. Clock 앨리 먼트에 date 삭제

root.render(<Clock />);

결과는 다음과 같습니다. 

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

위 프로그램을 실행시키면 아래와 같이 화면에 시간이 출력됩니다. 새로고침을 하면 새로고침 한 시간이 출력되고요. 단지 Clock을 한번 호출해서 그 호출한 시간을 보여주는 겁니다. 

 

그런데, 우리가 원하는 건 이게 아니죠? 매 초마다 시간 값이 업데이트되는 시계를 원하는 거죠. 계속 진행해 보겠습니다. 

 

 

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

많은 컴포넌트를 가진 애플리케이션에서는 컴포넌트가 파괴될 때 그들이 가져간 자원을 잘 해제시키는 것이 중요합니다. 우리는 처음 Clock이 DOM으로 랜더링 되었을 때 타이머가 세팅되기를 원합니다. 그래서 그때부터 똑딱똑딱 돌아가길 원하죠. 이것을 리액트에서는 마운팅(mounting)이라고 부릅니다. 또, Clock에 의해 생성된 DOM이 제거될 때마다 해당 타이머를 지우길 원합니다. 이것을 언마운팅(unmounting)이라고 부릅니다. 

이제, 컴포넌트 클래스가 마운트 되거나 언마운트될 때 코드를 실행하는 특별한 메서드를 선언해보겠습니다. 

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() 메서드를 생명주기 메서드라고 부릅니다.

메서드명에서 알 수 있듯이 componentDidMount() 메서드는 component did mount(컴포넌트가 (이미) 마운트 됐다)니까 컴포넌트 마운트 이후에 실행되는 메서드일 것입니다. 반대로, componentWillUnmount() 메서드는 component will unmount(컴포넌트가 (미래에) 언마운트 될것이다)니까, 컴포넌트가 언마운트 되기 전에 한번 실행되는 메서드 일 것입니다. 이름이 아주 직관적이네요.

 

componentDidMount() 메서드는 컴포넌트의 출력이 DOM으로 랜더링 된 이후에 실행되니까, 여기가 타이머를 세팅하기에 좋은 곳입니다.  

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

( ) => this.tick() 구문은 ES6문법을 사용한 함수 선언입니다. 매개변수 없이 tick() 함수가 몸체인 함수입니다.  this.timerID함수는 tick() 함수를 1000ms(1초) 마다 한 번씩 실행시키는 함수를 선언한 거죠. 이제 1초마다 한 번씩 tick() 메서드가 호출되면서 화면의 date값을 경신시킬 것입니다.

 

이걸 중지시키려면 어떻게 할까요? this.timerID에 설정된 setInterval을 제거해야 될 겁니다. interval을 제거시키는 것은 clearInterval 함수를 이용해 처리할 수 있습니다. 언제 호출하면 좋을까요? 컴포넌트가 언마운트 되기 전에 호출하면 되겠죠? 컴포넌트가 살아있는 동안에는 계속 시간을 보여줘야 할 테니까요. 그래서 아래와 같이 componentWillUnmount() 메서드에서 Interval을 제거해 주는 겁니다. 

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

 

마지막으로 Clock 컴포넌트에서 매초 실행하는 tick() 메서드를 구현해볼 겁니다. 

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

 

여기에서 this.setState() 메서드를 사용하게 될 건데, 여기에서 state의 값을 변경해 줄 것입니다. 여기가 핵심입니다. state값이 변경되면, 리액트는 render() 메서드를 다시 실행합니다. interval 설정을 통해 매초 tick() 메서드가 호출되고, tick() 메서드에서는 state의 date값을 매초 변경하게 되니까, 매초 render() 메서드가 호출되겠네요. 그래서 매초 마다 변경된 시간 값이 브라우저에 표현되는 겁니다. 

전체 코드를 한번 보겠습니다. 

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

여기서 제일 핵심적인 부분은 state값을 변경하면 render함수를 호출한다라는 내용일 것 같습니다. 

위 코드가 어떻게 작동되는지 다시 한번 정리해보겠습니다. 

 

1. root.render함수로 Clock 컴포넌트가 전달되면 리액트는 Clock 컴포넌트의 생성자(constructor()) 호출. 생성자에서는 this.state에 현재 Date 값을 초기화

2. 리액트는 Clock 컴포넌트가 render() 메서드 호출하여 스크린에 어떤 값이 보여야 하는지 체크하고 Clock 컴포넌트의 render() 메서드 결괏값을 DOM으로 업데이트

3. Clock 컴포넌트의 결괏값이 DOM으로 업데이트되면, 리액트는 componentDidMount() 생명주기 메서드 호출하여, 매 초마다 tick() 메서드를 호출하는 타이머를 세팅

4. 매초 브라우저는 tick() 메서드 호출. tick() 메서드에서 setState() 메서드를 이용하여 this.state값을 변경. setState() 메서드가 호출되면, 리액트는 state값이 변경되었다는 것을 인지하여, 화면에 어떤 내용을 출력할지 처리하기 위해 render() 메서드 호출. render() 메소드 결괏값이 DOM으로 적용

5. Clock 컴포넌트가 DOM에서 제거되면 리액트는 componentWillUnmount() 메서드를 호출하여 타이머를 중지시킴.

 

 

state 올바르게 사용하기

setState()에 대해 알아야 할 세 가지가 있습니다.

 

state를 직접 변경시키지 말라.

 다음 예와 같이 사용하면 컴포넌트가 다시 랜더링 되지 않습니다. 

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

대신 setState() 메서드를 사용하여 변경합니다. 

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

다만, constructor()에서는 state를 할당해서 바꿀 수 있습니다.

 

state 업데이트는 비동기적으로 이뤄질 수도 있다.

리액트는 퍼포먼스를 위해 단일 업데이트로 setState()를 배치 처리한다. this.props와 this.state는 비동기적으로 업데이트될 수도 있기 때문에, 다음 state계산에 기존 값을 참조해서는 안됩니다. 예를 들어, 아래 코드는 counter 업데이트에 실패합니다. 

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

이것을 바로잡기 위해, 오브젝트보다 함수를 받아들이는 setState() 폼을 사용하세요. 이 함수는 첫 번째 인자로 이전 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 업데이트는 병합된다.

우리가 setState()를 호출할 때, 리액트는 우리가 제공한 객체를 현재 state로 병합합니다. 예를 들면 아래 코드는 state에 여러 개의 독립적인 값들이 포함되어 있습니다. 

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

그래서 setState()를 여러 개로 나눠서 독립적으로 각각의 state값을 업데이트할 수 있습니다. 

  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는 완전히 업데이트합니다. 

 

데이트는 아래로 흐른다.

부모나 자식 컴포넌트는 특정 구성요소가  stateful인지  stateless인지 알 수 없고, 그들이 함수로 정의됐는지, 클래스로 정의됐는지 신경 쓸 필요가 없습니다. 이것이 state가 로컬 또는 캡슐화라고 부르는 이유입니다. 이를 소유하고 설정하는 구성요소 외에는 state에 접근할 수 없습니다. 컴포넌트는 자식에게 state를 전달할지 선택해야 할 수도 있습니다. 

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

FormattedDate 컴포넌트는 props로 date를 받을지 모릅니다. 그리고 그것이 Clock의 state로부터 왔는지, props에서 왔는지, 손으로 타이핑됐는지 알지 못합니다. 

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

이것은 흔히 탑다운이나 단방향 데이터 흐름이라고 불려집니다.  어떤 state 든 항상 구체적인 컴포넌트에 의해 소유되고, 그 state로부터 파생된 모든 data 및 UI는 트리에서 아래 구성요소에만 영향을 줄 수 있습니다. 당신이 props의 폭포로 컴포넌트 트리를 상상한다면, 각각의 컴포넌트 state는 폭포와 합류되는 추가적인 수자원과 같습니다. 그러나 또한 아래로 흐릅니다.

모든 컴포넌트가 실제로 독립적이라는 걸 보여주기 위해, Clock 컴포넌트를 세 개 랜더링 하는 App 컴포넌트를 만들 수 있습니다.

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

각각의 Clock은 자신의 타이머를 셋업하고 독립적으로 업데이트합니다. 

 

리액트 앱에서 컴포넌트가 stateful든지 stateless든지 시간이 지남에 따라 변동될 수 있는 디테일한 구현을 고려해야 합니다. 우리는 stateless 컴포넌트 안에 statefull컴포넌트를 사용할 수 있고, 그 반대로 할 수도 있습니다. 

반응형