들어가기 전에

리액트 공식문서로 배워보자 #5, State와 라이프사이클!

이 페이지에서는 리액트 컴포넌트 내부의 state와 라이프사이클에 대해 설명할 것입니다. 상세한 컴포넌트 API 레퍼런스는 여기서 볼 수 있습니다.

이전 섹션에서 똑딱거리는 시계 예제를 기억하시나요? 리액트 엘리먼트에서, 우리는 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);

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

CodePen에서 직접 해보기

하지만, 위 코드에서는 결정적인 요구사항 하나를 놓쳤습니다. Clock이 타이머를 세팅하고 UI를 매초마다 업데이트 하는 것은 Clock 구현의 세부사항이어야 합니다.

우리는 이상적으로 Clock을 한번 작성하고, Clock이 스스로 업데이트되길 원합니다. 다음 코드와 같이요.

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

이것을 구현하기 위해, 우리는 Clock 컴포넌트에 "state"를 추가해주어야 할 필요가 있습니다.

state는 props와 비슷합니다. 근데 private한 특성을 갖고 있고, 컴포넌트에 의해 완전히 컨트롤됩니다.

함수를 클래스로 변환하기

다음 5단계를 거치면 Clock과 같은 함수형 컴포넌트를 클래스형 컴포넌트로 변환할 수 있습니다.

  1. 같은 이름으로 React.Component를 상속받는 ES6 클래스를 만듭니다.
  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>
    );
  }
}

CodePen에서 직접 해보기

Clock은 이젠 함수가 아닌 클래스로 정의되어 있습니다.

render 메소드는 업데이트가 일어날 때마다 호출될 것입니다. 하지만 우리가 <Clock />을 같은 DOM node안에 렌더링하는 한, Clock 클래스의 하나의 인스턴스만이 사용되겠죠. 이러한 일은 우리가 local state나 라이프사이클 메소드와 같은 추가적인 기능을 사용할 수 있도록 해줍니다.

클래스에 Local State 추가하기

우리는 3단계에 거쳐 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. 초기 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을 <Clock /> 엘리먼트에서 제거합니다.
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')
);

CodePen에서 직접 해보기

다음으로, 우리는 Clock이 자체 타이머를 갖고 스스로 매 초마다 업데이트 하도록 만들겠습니다.

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

많은 컴포넌트를 가진 어플리케이션에서, 컴포넌트가 제거될 때, 컴포넌트에 의해 묶여진 리소스를 풀어주는 것은 매우 중요합니다.

우리는 Clock이 DOM에 처음으로 렌더링 될 때마다, 타이머를 세팅하길 원합니다. 이러한 순간을 리액트에서 "mounting"이라고 표현합니다.

또 우리는 Clock에 의해 생성된 DOM이 제거될 때마다, timer를 clear하길 원합니다. 이러한 순간을 리액트에서 "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 method)"라 부릅니다.

componentDidMount() 메소드는 컴포넌트의 출력 결과물이 DOM에 랜더링 된 이후에 동작합니다. 그래서 여기는 타이머를 세팅하기 좋은 지점입니다.

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

우리가 this에 어떻게 타이머 ID를 바로 저장했는지 기억해두세요.

this.props는 리액트 자체에서 설정되고, this.state는 특별한 의미를 가지지만, 만일 데이터 플로우에 참여하지 않는 무언가를 저장하길 원한다면 (timer ID와 같은), 추가적인 필드를 클래스에 언제든 수동으로 추가할 수 있습니다.

componentWillUnmoun() 라이프사이클 메소드 내에서 우리는 타이머를 끌(Clear) 수 있습니다.

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

마지막으로 Clock 컴포넌트를 매 초마다 실행시킬 tick() 메소드를 구현할 것입니다.

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

CodePen에서 직접 해보기

이제 매 초마다 Clock이 똑딱거릴 것입니다.

무슨 일이 일어나는 건지 그리고 메소드가 호출되는 순서에 대해서 빠르게 한번 되짚어봅시다.

  1. <Clock />ReactDOM.render()로 넘겨졌을 때, 리액트는 Clock 컴포넌트의 생성자를 호출합니다. Clock 컴포넌트는 현재 시간을 보여줄 필요가 있기 때문에 현재 시간을 포함하는 오브젝트로 this.state를 초기화합니다. 우리는 나중에 this.state를 업데이트 하게 될 것입니다.
  2. 이후 리액트는 Clock 컴포넌트의 render() 메소드를 호출하게 됩니다. render() 메소드를 통해 리액트는 화면에 무엇을 보여줘야 할지에 대해서 알게됩니다. 다음으로 리액트는 Clock의 render 출력에 맞추기 위해 DOM을 업데이트합니다.
  3. Clock의 출력이 DOM에 삽입됐을 때, 리액트는 componentDidMount() 라이프사이클 메소드를 호출합니다. 내부에서, Clock 컴포넌트는 브라우저에게 tick() 메소드를 매 초마다 호출하기 위해 타이머를 세팅해달라고 요청합니다.
  4. 매 초마다 브라우저는 tick() 메소드를 호출합니다. 내부적으로, Clock 컴포넌트는 현재 시간을 담고 있는 오브젝트를 setState() 메소드를 통해 계속 할당함으로써, UI 업데이트를 스케쥴링합니다. setState()를 호출하는 덕분에, 리액트는 state가 바뀐 것을 알게됩니다. 그리고 render() 메소드가 다시 스크린에 보여줄 내용을 재설정하기 위해 호출됩니다. 이번에는 render()메소드 내부의 this.state.date가 달라집니다 그리고 그래서 렌더링되는 출력에도 업데이트된 시간이 포함됩니다. 리액트는 DOM을 이에 따라 업데이트합니다.
  5. 만일 Clock 컴포넌트가 DOM으로부터 제거된다면, 리액트는 componentWillUnmount() 라이프사이클 메소드를 호출하고 타이머는 멈추게 됩니다.

State 올바르게 사용하기

setState()에 대해 알아야 하는 3가지 내용이 있습니다.

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

예를 들면, 이러한 방식은 컴포넌트를 재렌더링하지 못합니다.

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

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

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

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

State 업데이트는 아마 비동기적입니다.

리액트는 성능을 위해서 여러개의 setState() 호출을 하나의 업데이트에 배치(batch)합니다.

this.propsthis.state는 아마 비동기적으로 업데이트되기 때문에, 다음 상태에 대한 연산을 하기 위해서 이러한 값들에 의존하면 안됩니다.

예를 들면, 이 코드는 카운터를 업데이트하는데 실패할 수 있습니다.

// 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의 업데이트들은 병합(Merge)됩니다.

setState()를 호출할 때, 리액트는 우리에게서 받은 오브젝트를 현재 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
      });
    });
  }

병합은 얕은(shallow) 수준에서 이뤄집니다. 그래서 this.setState({comments})this.state.posts를 그대로의 상태로 납둡니다. 하지만 this.state.comments를 완전히 대체합니다.

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

부모 자식 컴포넌트 둘 다 특정 컴포넌트가 state가 있는 형태인지(stateful) state가 없는 형태인지(stateless) 알 수 없습니다. 그리고 함수형 컴포넌트인지 클래스형 컴포넌트인지에 대해 신경쓰지도 않습니다.

state가 지역적(local) 혹은 캡슐화(encapsulated)되어있다고 불리는 이유가 바로 이것입니다. 그 state를 가지고 있고 세팅한 컴포넌트 이외에는 어떠한 다른 컴포넌트도 그 state에 접근할 수 없습니다.

컴포넌트는 state를 props를 통해 자식 컴포넌트로 내려보낼 수도 있습니다.

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

사용자정의(user-defined) 컴포넌트에서도 물론 이 방법은 동작합니다.

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

FormattedDate 컴포넌트는 props에 date를 받을 것입니다. 그리고 이 dateClock의 state에서 왔는지, props에서 왔는지, 아니면 그냥 손으로 타이핑된 건지는 알 수 없을 겁니다.

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

CodePen에서 직접 해보기

이건 일반적으로 "탑다운(top-down)" 혹은 "단방향성(unidirectional)" 데이터 플로우라고 불립니다. 어떤 state는 특정한 컴포넌트에 의해 소유됩니다. 그리고 어떤 그 컴포넌트의 state로부터 끌어낸 데이터나 UI는 트리에서 오직 그 "하위(below)"에 있는 컴포넌트에게만 영향을 미칠 수 있습니다.

모든 컴포넌트가 독립되어 있다는 것을 보여주기 위해, 3개의 <Clock>을 렌더링하는 App 컴포넌트를 만들 수도 있습니다.

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

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

CodePen에서 직접 해보기

각각의 Clock은 자신이 가진 타이머를 세팅하고 독립적으로 업데이트 됩니다.

리액트 어플리케이션에서, 컴포넌트가 상태가 있는지 상태가 없는지(stateful or stateless)는 매번 변할 수 있는 컴포넌트의 구현 디테일로 고려됩니다. stateful 컴포넌트 내부에 stateless 컴포넌트를 사용할 수도 있고 반대도 가능(vice versa)합니다.