State and Lifecycle

Yeom Jae Seon·2021년 2월 2일
0

React공식문서 공부

목록 보기
4/11
post-thumbnail

지금까지


지금까진 엘리먼트(화면에 어떻게 표시할지 기술된 일반 객체)를 통해 DOM을 구성하기 위해서는 루트 돔 노드(<div id='root'></div>)에 엘리먼트를 넣어서, 좀더 자세히는 ReactDOM.render()함수의 인자로 엘리먼트와 루트 돔 노드를 넣어서 React DOM이 엘리먼트를 보고 DOM을 구성하게하여 엘리먼트를 렌더링했다.(DOM을 효율적으로 구성하겠지? VDOM을 통해서.)

ex)

import React from "react";
import ReactDOM from "react-dom";

let count = 0;

const Greeting = () => {
  const element = (
    <div>
      <h1>인사한 횟수</h1>
      <p>{++count}</p>
    </div>
  );
  ReactDOM.render(element, document.getElementById("root"));
};
setInterval(Greeting, 1000);

이제는 인사한 횟수를 출력하는 컴포넌트를 하나의 컴포넌트로 만들겠다.
(저번시간에 컴포넌트 배웠으닌까)

import React from "react";
import ReactDOM from "react-dom";

let count = 0;

const GreetingCount = (props) => {
  return (
    <div>
      <h1>인사한 횟수</h1>
      <p>{++count}</p>
    </div>
  );
};

const Greeting = () => {
  ReactDOM.render(<GreetingCount />, document.getElementById("root"));
};
setInterval(Greeting, 1000);

위 코드는 단순히 DOM 태그인 엘리먼트를 ReactDOM.render()에 연결하던걸 컴포넌트로 하나를 만들어서 넣었을 뿐이다.

그런데 GreetingCount라는 컴포넌트는 자체적으로 count를 증가시키는게 아닌 해당 scope밖에 있는 let count = 0의 count를 가져다 쓴다.
인사한 횟수를 1초에 1씩 증가하고 횟수를 알려주는 기능을 가진 GreetingCount컴포넌트(GreetingCount컴포넌트는 그러한 기능을 가지고 있다고 정의하고 시작하자. 이 부분은 공식문서처럼 Clock으로 하는게 더 이해가 쉬울듯하다..) 내에서 해당 count를 정의하고 +1시키는게 더 적절하지 않을까?
그렇게 하려면 GreetingCount컴포넌트자체에서 count를 가지고있어야하고 1초에 1씩 해당 컴포넌트 자체에서 증가를 시키는게 적절하겠다.

일단 이것은 우리의 목표이다.
하나씩 해나가보자.

우리의 목표에 달성하기 위해선 먼저 GreetingCount컴포넌트 내에서 State라는 녀석을 사용해야한다.

State는 props와 유사하지만 컴포넌트 내에서, 해당 컴포넌트에 의해 완전히 제어되는 녀석이다.

  • 컴포넌트 자체에서 데이터를 가지고 있고 해당 데이터를 통해 자체적으로 업데이트 하기 위해선 State가 있어야한다.
  • State는 컴포넌트 내에서 존재하여 비공개이며 해당 컴포넌트에 의해 완전히 제어된다.

함수컴포넌트에서 클래스컴포넌트로 변환하기


함수컴포넌트에서는 state를 사용할수가 없다. (React Hooks 가 존재하긴하지만 일단은 못하는걸로 알자)
그래서 일단은 클래스 컴포넌트로 변환을 해야한다.

class GreetingCount extends React.Component {
  render() {
    return (
      <div>
        <h1>인사한 횟수</h1>
        <p>{++count}</p>
      </div>
    );
  }
}
  • 이런식으로 바꾸면된다.

이제는 GreetingCount컴포넌트는 클래스로 표현이되었다.

  • 컴포넌트 자체적으로 state를 사용하기 위해선 일단 클래스컴포넌트로 변환한다.
    (React HOOKS 모른다 가정)

클래스에 로컬 State 추가하기


이젠 클래스컴포넌트로 작성했으니 count변수를 GreetingCount컴포넌트내에서 state로 만들어보자. (위에서 얘기했던 state이다.)

지금 우리가 뭘하는지 다시 상기하자면 GreetingCount컴포넌트의 기능을 컴포넌트 내부에서, 자체에서 모두 동작하게 끔하기위한 것이다.(캡슐화, 컴포넌트의 독립성, 앞의 두가지가 좋아지면 재사용성도 높아지겠다.)

클래스에 맞게 적절히 this를 사용해가며 state를 추가하면된다.

import React from "react";
import ReactDOM from "react-dom";

class GreetingCount extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count : 0};
  }
  render() {
    return (
      <div>
        <h1>인사한 횟수</h1>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

const Greeting = () => {
  ReactDOM.render(<GreetingCount />, document.getElementById("root"));
};
setInterval(Greeting, 1000);

외부 scope에 존재하던 let count = 0을 삭제해서 GreetingCount클래스 컴포넌트 로컬 데이터로 옮겼다.(state로)
그리고 render()함수 내부에서 ++count하는 부분을 삭제했다.

당연히 state(count)를 증가하는 부분이 없으니 눈으론 count 증가가 보이지 않을 것이다. 대신 setInterval은 계속해서 실행되는중.

그런데 이렇게 계속 setInterval이 실행되면 좋을까? 좋지않다. 왜냐면 불필요하게 리소스가 낭비되고 있기 때문이다. 해당 컴포넌트가 생성될때 setInterval로 콜백등록해주고 해당 컴포넌트 삭제되면 clearInterval로 콜백함수 등록해제하면 좋을듯 싶다.

  • 해당 컴포넌트 로컬데이터로 state를 컴포넌트 내에서 정의했다.

생명주기 메소드를 클래스에서 사용하기


GreetingCount컴포넌트가 처음 DOM에 렌더링 되는 것을 마운팅이라고 한다.
반대로 GreetingCount컴포넌트의 생성된 DOM이 삭제되는 것을 언마운트이라고 한다.
GreetingCount마운팅됐을 때 setInterval을 등록하고 언마운트됐을 때 clearInterval로 등록한 함수 해제해주자. (불필요한 리소스 낭비를 막기위해)

이때 컴포넌트가 마운팅, 언마운트 될때마다 동작하게하는 특별한 함수가 존재한다.
전자는 componentDidMount()이고 후자는 componentWillMount()이다.

이러한 메소드들을 생명주기 메소드라고한다.

componentDidMount()메소드는 DOM에 컴포넌트가 렌더링이 된후에 실행된다.(좀더 자세히 말하면 컴포넌트가 리턴하는 엘리먼트를 ReactDOM이 보고 DOM을 구성한 뒤에) 그러므로 최초의 count = 0을 DOM에 랜더링 시키고 setIntervalcomponenetDidMount()에 등록해서 1초에 1씩 증가하면 좋겠다.

참고로 state나 props같은 일련의 데이터흐름(setState => rerendering - 아직 안배움)에 상관없는 지역변수를 클래스 컴포넌트에 정의하는건 상관없다.(함수 컴포넌트도 마찬가지)
그치만 이경우에 컴포넌트 리랜더링 될 때마다 불필요하게 재정의되어 메모리 낭비되고 있는건 아닌지 확인하자.!

그리고 componentWillMount()clearInterval를 통해서 등록함수 해제하자.

그리고 클래스 내부에서 클래스 내부의 state를 업데이트 시키는 함수인 setState를 사용해보자.
참고로 setState는 컴포넌트의 로컬 state를 업데이트하기 위해 사용된다.
(setState는 좋은 기능이지만 어렵다.. 일단은 setState로 로컬 데이터인 state를 업데이트 시켜야 한다고만 알아놓자)

import React from "react";
import ReactDOM from "react-dom";

class GreetingCount extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  componentDidMount() {
    this.GreetingId = setInterval(() => {
      this.greeting();
    }, 1000);
  }
  componenetWillMount() {
    this.GreetingId = clearInterval();
  }
  greeting() {
    this.setState({ count: this.state.count + 1 });
  }
  render() {
    return (
      <div>
        <h1>인사한 횟수</h1>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

ReactDOM.render(<GreetingCount />, document.getElementById("root"));

와우,
컴포넌트 내부에서 state를 만들고 state를 자체적으로 업데이트 시키는 로직도 만들었다.
흠. 그런데 이전에는 render()함수 내부에서 ++count로 증가를 시켰는데 지금은 render()함수 내부에서 this.state.countstate만 랜더하고있네..

여기서 알아야 할건 greeting()메소드 내에서 실행되고 있는 this.setState()이다.!

React는 setState()를 통해서 state를 업데이트 하면 "아 state가 업데이트 되었구나 리렌더링 시켜야지~"라고 깨닫게 된다.

그러므로 1초마다 this.greeting()호출하고 해당 함수 내부에서 setState함수가 호출되니 1초마다 render()함수가 호출되어 setState에서 변경된 state에 맞게 리랜더링 되고있는 것이다.
(이전에는 ReactDOM.render()함수를 1초마다 호출했지만 지금은 단 한번 호출하고 1초마다 1씩 인사하는 기능을 가진 GreetingCount컴포넌트 내부에서 로컬 state를 만들고 해당statesetState()로 업데이트 시킴으로써 UI를 업데이트 시키고있다.)

동작하는 과정을 살펴보자.
1. ReactDOM.render함수에 <GreetingCount />컴포넌트가 전달되고 전달되면 React는 해당 컴포넌트의 constructor를 호출해서 state를 초기화한다.
2. React는 GreetingCount컴포넌트의 render() 메소드를 호출하고 render()메소드는 엘리먼트를 리턴하기 때문에 엘리먼트를 보고 DOM을 업데이트한다.
3. 이제 DOM이 업데이트 되면 그제서야 생명주기 함수중 마운팅을 담당했던 componentDidMount() 생명주기 메소드가 호출되고 그안에서 this.greeting()메소드가 호출된다.
4. this.greeting()메소드는 this.setState()함수를 호출하는데 이 함수를 통해서 React는 "아.. state업데이트 되었구나!"를 깨닫게 되고 render()함수를 다시 호출한다(리렌더링 과정). 이때 this.setState()로 state는 달라졌고 변경된 {this.state.count}render() 시킴으로써 UI를 업데이트 한다. 즉, React는 리턴된 엘리먼트를 보고 효율적으로 DOM을 업데이트한다.
5. GreetingCount컴포넌트가 삭제된다면 componenetWillUnmount()생명주기 함수를 호출한다.

  • 이제 컴포넌트 내에서 상태를 관리할수 있고 (setState로)상태가 변함에 따라 React는 DOM을 다시 구성하여 컴포너트 자체에서 UI업데이트가 가능하게 되었다.
  • Greeting 컴포넌트 자체에서 자신의 기능을 구현할수있게 되었다. 캡슐화가 되었다.
  • React는 setState를 보면 render()함수를 호출시켜 리랜더링 한다.

State 올바르게 사용하기


컴포넌트 자체에서 로컬데이터인 state를 사용해서 컴포넌트 내부에서 데이터를 관리하고 state를 변경하는 함수인 setState를 사용해서 state를 업데이트 시키면 React는 state가 변경된걸 깨닫고 re rendering하게 된다는걸 배웠다.

이런 좋은 state와 setState로직을 사용하기 위해선 몇가지 알아야할것들이 있다.

1. 직접 state수정하지 말것

bad 😅
setState를 사용하지않고
this.state.count = 100 이렇게 직접 변경하면 리렌더링이 발생하지 않는다. 무적권 setState사용하여 state 업데이트하라!

good 😀
this.setState({count : 100})
참고로 state는 불변성 지켜줘야한다.

2. State업데이트는 비동기적일수 있다.

React는 성능을 위해 setState()의 여러 호출에 대해서 한번에 업데이트하는둥의 비동기적으로 동작한다.

bad 😅

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

새로운 state로 업데이트할때 바로 직전의 state값이 전의 값이라 믿고 이런식으로 사용하면 안된다.

good 😀

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

이전의 state와 props에 대해서 업데이트가 이루어지는 상황에는 이렇게 setState(newState)방식보다는 setState(prevState => newState)처럼 인자로 함수를 받아서 사용하자.
이렇게 사용하면 이 함수의 첫번째 인자는 이전 state를 받아올것이고 두번째 인자는 이전 props를 인자로 받아와서 이전 state와 props에 의존하여 업데이트하는 경우 적절히 업데이트가 될수있다.

3. State 업데이트는 병합된다.(useState의 setState는 아님.)

import React, { useState } from "react";
import ReactDOM from "react-dom";

const FuncComponent = () => {
  const [me, setMe] = useState({ name: "jaeseon", age: 10 });

  return (
    <>
      <button
        onClick={() => {
          setMe({ name: "minseon" });
        }}
      >
        name
      </button>
      <button
        onClick={() => {
          setMe({ age: 26 });
        }}
      >
        age
      </button>
    </>
  );
};

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: "jaeseon", age: 10 };
  }
  render() {
    return (
      <>
        <button
          onClick={() => {
            this.setState({ name: "minseon" });
          }}
        >
          name
        </button>
        <button
          onClick={() => {
            this.setState({ age: 26 });
          }}
        >
          age
        </button>
      </>
    );
  }
}

ReactDOM.render(<ClassComponent />, document.getElementById("root"));

클래스 컴포넌트에서 setState를 하면 this.state는 병합이된다.
그러나 함수 컴포넌트에선 그렇지 못하다.

  • setState를 통해서 state를 업데이트해야 리액트는 state 업데이트된걸 알고 리랜더링 한다.
  • setState는 비동기적으로 동작한다. 그러므로 이전 state나 props에 의존해서 state가 변경될땐 setState((prevState, prevProps) => newState);를 이용해야한다.
  • 클래스 컴포넌트에서 this.setState로 state의 업데이트는 병합된다.

데이터는 아래로 흐른다.


state는 로컬 또는 캡슐화라고 불린다. 왜냐면 컴포넌트는 독립적, 고립적이기 이고 state는 해당 state를 가지고있는 컴포넌트외에는 접근할수가 없다.

그치만 컴포넌트는 자신의 state를 자식 컴포넌트에게 props로 전달할수 있다.

자식컴포넌트는 단지 props로 받을 뿐이지 props의 출처를 알지못한다. 예를들면 props가 부모컴포넌트 state인지, 부모컴포넌트가 받은 props인지... 모른다

이를 하향식, 단방향식 데이터흐름이라고한다.
모든 state는 특정 컴포넌트가 가지고 있고 그 state로부터 파생된 UI또는 데이터는 오직 트리구조에서 바로 자기 아래에 있는 자식 컴포넌트에만 영향을 미친다.

공식문서에서는 트리구조가 props들의 폭포라고 상상하면 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로흐르는 water source라고 한다.

이뜻이 뭘까 처음에는 이해가 안됐지만 나 나름대로 이해한 내용을 적어보겠다.
컴포넌트의 state는 해당컴포넌트밖에 접근 할수가 없다. 대신 stateprops로 바로 자기 아래! 자식컴포넌트에게 전달할수 있다. 그러나 정작 props로받은 자식컴포넌트는 props가 부모컴포넌트의 state인지, props인지, 수동으로 입력받은 데이터인지 알지못한다. 다만! 부모컴포넌트에서 state가 업데이트가 일어난다면 re rendering이 일어날 것이고 해당 부모 컴포넌트를 기점으로 자식 컴포넌트들은 변경된 state를 props를 전달받고 (별다른 최적화안하면) 자식 컴포넌트들도 re rendering이 일어날 것이다.
즉, 수원이 water source란말은 state를 가지고있는 부모컴포넌트를 기점으로 아래에있는, 자식컴포넌트들에게 props로 전달이 가능하단 소리이다.

  • state는 해당 컴포넌트만 알고 있지만 바로 자기 자식 컴포넌트에겐 props로 해당 state를 props로 전달할수 있다.
  • 자식 컴포넌트는 부모 컴포넌트로 받은 props의 출처를 알지못하고 content만 알게된다.
  • 위 두 과정을 단방향식 데이터흐름이라 한다.

위 내용은 React 공식문서를 보고 공부한 내용입니다.
https://ko.reactjs.org/docs/state-and-lifecycle.html

0개의 댓글