React 공식 문서 해석하며 공부하기 : State and LifeCycle

배지로·2021년 9월 1일
0
post-thumbnail

해석하며 공부하는 것을 목적으로 하기 때문에 다수의 의역, 오역이 있음을 미리 밝힙니다.
원본 : https://reactjs.org/docs/state-and-lifecycle.html

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

이번 섹션에서는, 재사용할 수 있고 캡슐화되어있는 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);

그러나, 중요한 요구사항을 빠트렸습니다 : Clock에 타이머가 세팅되어 있고, 매 초마다 UI가 업데이트된다는 사실은 Clock의 세부 구현이여야만 합니다.

코드는 아래와 같이 작성되어서Clock이 자체적으로 업데이트되는 것이 가장 이상적입니다:

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

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

State는 props와 비슷하지만, 비공개이고 컴포넌트에 의해 완전히 제어됩니다.


함수를 클래스로 전환하기

다섯가지 단계로 Clock과 같은 함수 컴포넌트를 클래스로 전환할 수 있습니다:
1. 같은 이름의 ES6 클래스를 만들어서 React.Component를 상속합니다.
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>
    );
  }
}

Clock는 이제 함수가 아니라 클래스로 정의되었습니다.

render메소드는 업데이트가 일어날 때마다 호출될 것이지만, 같은 DOM노드에서 <Clock/>이 렌더링되는 한, 오직 한 개의Clock 클래스 인스턴스만 사용될 것입니다. 이것은 로컬 state와 생명주기 메소드와 같은 추가적인 기능을 사용해야 한다는 것을 의미합니다.


클래스에 로컬 State 추가하기

세 단계로 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 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')
);

다음으로, 우리는 Clock에 타이머를 세팅하고 그 자체로 매 초마다 업데이트되도록 만들 예정입니다.


클래스에 생명주기 메소드 추가하기

많은 컴포넌트가 포함된 어플리케이션에서, 컴포넌트가 삭제될 때 컴포넌트가 사용 중이던 자원을 해방하는 것이 매우 중요합니다.

Clock이 처음 DOM에 렌더링될 때마다 타이머를 세팅하려고 합니다. 리액트에서는 이것을 "마운팅"이라고 합니다.

또한 Clock에 의해 만들어진 DOM이 제거될 때마다 타이머를 없애려고 합니다. 리액트에서는 이것을 "언마운팅"이라고 합니다.

컴포넌트 클래스에서 특별한 메소드를 선언하여 컴포넌트가 마운트되거나 언마운트될 때 일부 코드를 실행할 수 있습니다.

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() 메소드는 컴포넌트 결과가 DOM에 렌더링된 후에 실행됩니다. 이 메소드는 타이머가 세팅되기에 좋은 장소입니다:

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

this(this.timerID)에서 어떻게 타이머 ID를 저장하는지 주의해주세요.

this.props가 리액트에 의해 설정되고 this.state가 특수한 의미를 지녔음에도 timer ID와 같은 데이터 흐름에 참여하지 않는 무언가를 저장해야 한다면, 직접 클래스에 부가적인 공간을 더해줘도 됩니다.

생명주기 메소드 componentWillUnMount()로 타이머를 분해해보도록 하겠습니다:

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

마침내, Clock컴포넌트가 매 초 실행하는tick() 메소드를 구현해보도록 하겠습니다.

this.setState()를 사용해서 컴포넌트의 로컬 state를 업데이트할 예정입니다.

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(){
     <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

이제 시계는 매초 똑딱거립니다.

메소드가 어떤 순서로 호출되는지에 대해서 빠르게 요약해보도록 하겠습니다:

  1. <Clock/>ReactDOM.render()로 전달되면, 리액트는 Clock 컴포넌트의 생성자를 호출합니다. Clock이 현재 시간을 보여줘야하기 때문에, 현재 시간을 포함한 오브젝트 this.state를 초기화합니다. 추후, 이 state를 업데이트합니다.
  2. 그후 리액트는 Clock 컴포넌트의 render()메소드를 호출합니다. 이것이 리액트가 스크린에 보여줘야 하는 것을 습득하는 방법입니다. 그러고나서 리액트는 Clcok의 렌더링 결과에 해당하는 DOM 요소를 업데이트합니다.
  3. Clock 결과물이 DOM에 삽입될 때, 리액트는 생명주기 메소드 componentDidMount()를 호출합니다. 이 메소드 안에, Clock 컴포넌트가 브라우저에 컴포넌트의 tick()메소드를 매초 실행할 타이머를 설정해달라고 요청합니다.
  4. 매 초 브라우저는 tick() 메소드를 호출합니다. 메소드 안에는 Clock 컴포넌트가 현재 시간을 포함한 오브젝트로 setState()를 호출하여 UI 업데이트를 예정합니다. setState()호출 덕분에, 리액트는 state가 변했다는 알게 되고, 어떤 것이 스크린에 있어야하는지 알기 위해서 render() 메소드를 다시 호출합니다. 이 단계에서 render() 메소드 안의 this.state.date가 달라지며, 렌더링 결과물에 업데이트된 시간이 포함됩니다. 그에 따라 리액트는 DOM을 업데이트합니다.
  5. Clock 컴포넌트가 DOM에서 삭제되지 않는 한, 리액트는 생명주기 메소드 componentWillUnmout()를 계속 호출하기 때문에 타이머는 멈추지 않습니다.

State를 정확하게 사용하기

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

State를 직접 수정하면 안된다

아래의 예시는 컴포넌트를 다시 렌더링하지 못할 것입니다 :

//오답
this.state.comment='Hello';

대신에 setState()를 사용하세요:

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

this.state를 할당할 수 있는 유일한 곳은 생성자입니다.

State는 비동기적으로 업데이트된다

리액트는 성능을 위해 여러 가지의 setState() 호출을 하나의 업데이트로 묶습니다.

this.propsthis.state는 비동기적으로 업데이트되기 때문에, 다음 state를 계산할 때 이전 값들에 의존해서는 안됩니다.

아래의 코드는 counter에 업데이트 하는 것을 실패할 것입니다:

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

이것을 수정하기 위해서, 오브젝트보다는 함수를 받아들이는 두번째 형식의 setState()를 사용합시다. 아래의 함수는 첫번째 인자로 이전 state를 받아오고, 그 때 업데이트가 적용된 props가 두번째 인자로 받아옵니다:

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

위의 예시에서는 화살표 함수를 사용했지만 정규 함수식으로도 표현할 수 있습니다:

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

State 업데이트는 통합된다

setState()를 호출할 때, 리액트는 현재 state로 제공된 오브젝트를 합칩니다.

예를 들어, state는 여러가지 독립된 변수들을 포함할 수 있습니다:

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

이러한 state들을 분리된 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는 완전하게 대체됩니다.


데이터는 아래로 흐른다

부모 컴포넌트도 자식 컴포넌트도 state가 있는지 없는지에 대해서 알기 힘들고 함수로 정의되었는지 클래스로 정의되었는지에 대해서 신경쓰지 않습니다.

이 때문에 state는 종종 로컬 state이라고도 불리고 캡슐화 state라고도 불립니다. state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없습니다.

컴포넌트는 컴포넌트의 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>;
}

이것은 일반적으로 "하향식"이나 "단방향"의 데이터 흐름이라고 불립니다. 모든 state는 항상 특정한 컴포넌트가 소유하고, 그러한 state로부터 파생된 어떤 데이터나 UI는 컴포넌트 하위 트리에만 영향을 미칩니다.

컴포넌트 트리구조를 props의 폭포수같은 형태로 상상해본다면, 각 컴포넌트의 state는 임의의 지점에서 만나지만 아래로 흐르는 또 다른 수원(water source)라고 볼 수 있습니다.

모든 컴포넌트가 정말로 독립되었다는 것을 보여주기 위해서, 세 개의 <Clock>을 렌더링하는 App 컴포넌트를 만들었습니다.

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

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

Clock은 각자의 타이머가 설정되어 있고, 독립적으로 업데이트합니다.

리액트 애플리케이션에서는 컴포넌트의 state 유무와 관계없이 매 순간 컴포넌트에 변화가 일어난다는 구현 사항을 고려합니다. state가 있는 컴포넌트 안의 state가 없는 컴포넌트를 사용할 수 있고, 그 반대로의 경우도 있을 수 있습니다.

profile
웹 프론트엔드 새싹🌱

0개의 댓글