Front-End/React.js

React Hook: useState란?

주유소짜글이 2022. 3. 7. 11:23

useState의 의미와 사용 방법


이전 글을 통해 React Hook이 만들어진 가장 큰 이유 두 가지가 바로 상태 관리와 사이드 이펙트 구현이라고 설명했다. 리액트는 state에 설정된 데이터, 즉 렌더링에 영향을 미치는 값을 감시하고 이 데이터가 변동되면 리렌더링 한다. 기존 클래스형 컴포넌트에서는 constructor 메소드를 통해 state를 구현했는데 이를 함수형 컴포넌트에서도 쓸 수 있도록 만든 것이 바로 useState다.

 

다음은 리액트 공식 홈페이지 문서에서 가져온 클래스형 컴포넌트의 state 구현 방식이다.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

 

그리고 아래 코드는 위의 클래스형 컴포넌트에서 구현한 state를 hook의 useState 메소드를 통해 함수형 컴포넌트에서 구현한 것이다. 이 또한 리액트 공식 홈페이지 문서의 예시 코드다.

import React, { useState } from 'react';

function Example() {
  // "count"라는 새로운 상태 값을 정의합니다.
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

 

위에서 알 수 있듯이 함수형 컴포넌트에서 state를 구현한 방법은 다음과 같다.

const [count, setCount] = useState(0);

 

여기서 count는 state 값이 저장될 변수, setCount는 state 변수를 갱신할 함수, 그리고 useState 안에 들어가 있는 값은 앞서 선언한 변수의 초기 값을 뜻한다. 조금 더 이해하기 쉽게 풀어서 작성해보도록 하겠다. (만일 다음과 같은 코드 구현이 익숙지 않다면 '구조 분해 할당'에 관하여 공부해보도록 하자)

const ["state 변수", "state 변수 갱신 함수"] = useState("state 변수 초기값")

 

위 코드에 따르면 count라는 이름의 state변수는 초기값 0을 가지고 선언되었다. 배열의 두 번째 인자인 setCount는 state 변수 갱신 함수라고 했는데, state 변수 값을 수정해줄 때 쓰는 함수를 지정해놓은 것이다. 위 코드에서 버튼을 클릭할 시, setCount(count + 1)가 실행되도록 설정해뒀다. 이는 state 변수 갱신 함수를 쓰는 아주 쉬운 예다. state 변수를 수정할 때에는 state 변수를 직접 건드는 것이 아니라 state 변수 갱신 함수를 이용해야 한다. 다음 예를 살펴보자.

// Wrong
count++;
count += 1;
count = (count + 1);

// Correct
setCount(count + 1)

 

위의 첫 세 코드는 count를 직접 수정하는 방식이기 때문에 모두 틀린 방식이다. 마지막 setCount를 이용한 방법만이 제대로 된 방법인데 이는 state 변수의 역할 때문이다. state 변수는 리액트가 변화를 감지하고 이에 따라 렌더링을 수행할 수 있도록 하는 변수다. 하지만 state 변수 갱신 함수를 이용하지 않고 직접 state 변수를 수정한다면 리액트가 state 변수의 변화를 감지하지 못한다. 그렇다면 state 변수는 state 변수로서 의미가 없어진다.

 

이는 함수형 컴포넌트에 국한된 이야기가 아니다. 클래스형 컴포넌트에서도 count 변수를 수정하기 위해선 this.setState({count: this.state.count+1})라는 긴 코드를 작성해야 한다. 여기에 비하면 setCount 하나로 끝나는 함수형 컴포넌트는 선녀 아닌가?

 

어쨌든 위와 같은 state 변수 선언을 이용하면 함수형 컴포넌트에서도 쉽게 상태 관리를 구현할 수 있다. 적재적소에 활용하도록 하자.

 

 

 

useState와 비동기 동작 그리고 Batch


위의 코드를 잠깐 수정해서 살펴보도록 하자.

import React, { useState } from 'react';

function Example() {

  const [count, setCount] = useState(0);

  const onClickHandler = () => {
    setCount(count + 1);
    console.log("count:", count)
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={onClickHandler}>
       Click me
      </button>
    </div>
  );
}

 

위 코드와 사실상 같은 코드다. 달라진 점이 있다면 버튼을 클릭했을 때, count 변수에 1이 추가될 뿐만 아니라 콘솔 창에도 현재 count값이 찍힌다는 것. 여기서 버튼을 세 번 클릭하면 어떤 값이 나오게 될까? 지금까지 공부한 내용을 토대로 유추해보면 count는 3이 될 것이고 콘솔 창에는 "count: 1", "count: 2", "count: 3" 이렇게 세 값이 찍힐 것이다. 한번 확인해보도록 하자.

 

예상과 다르지 않는가? 우리 예상과 다르게 콘솔 창에는 1, 2, 3이 아니라 0, 1, 2가 찍혀있다. 그렇다고 아예 state 값 수정이 안된 것은 아니다. 옆에 렌더링 된 화면에서 값이 3으로 변한 것을 볼 수 있기 때문에 바로 확인할 수 있다. 이게 어떻게 된 일일까?

 

이런 일이 발생하는 이유는 바로 이벤트 핸들러 내에선 useState 함수가 비동기식으로 동작하기 때문이다. 그럼 왜 이벤트 핸들러 내의 useState함수가 비동기식으로 동작할까? 리액트의 장점 중 하나인 Virtual DOM에 관하여 생각해보자. 리액트가 가상 DOM을 사용하는 이유를 기억하는가? 기억 못 하는 사람들이 민망해 할 수 있으니 빠르게 이야기해보자면, 리액트는 불필요한 렌더링을 줄이기 위해 가상 DOM을 통해 변경사항을 따로 저장해 두고 이를 종합적으로 처리해 실제 DOM에 보낸다. 여기서 중요한 것은 '불필요한 렌더링을 줄이는 것'이다!

 

setCount를 그때그때 바로 처리한다면 state값이 변하고, state값이 변하면 그때마다 '불필요한' 렌더링이 일어날 것이다. 하지만 이처럼 이벤트 핸들러 함수 이후 일괄 업데이트를 한다면 불필요한 렌더링을 줄여 더 빠르고 효율적으로 동작할 수 있게 된다. 이런 이유로 이벤트 핸들러 내의 useState함수는 비동기식으로 동작한다. 그리고 이처럼 실시간으로 업데이트하는 것이 아니라 마지막에 일괄 처리하는 것을 'Batch'라 한다.

 

const onClickHandler = () => {
  setCount(count + 1);			// 요청은 들어갔지만 아직 반영이 되진 않은 상태
  console.log("count:", count)          // +1이 반영되지 않은 기존 count값을 불러오는 중
}

다시 예시 코드를 살펴보자. setCount(count+1)은 onClickHandler 함수, 즉 이벤트 핸들러 함수 내부에 있다. 그 이야기는 실질적으로 setCount로 인해 state 값이 변하는 시기는 onClickHandler 함수 호출이 끝난 이후다. 하지만 console.log는 onClickHandler함수 내부에 있다. 즉, concole.log에서 출력하려고 하는 count 변수는 상태 값이 변하기 전 값을 가지고 있다. 때문에 위 그림과 같이 출력되는 것이다.

 

하지만 '내 목에 칼이 들어와도 console.log에 변동된 state 값을 찍어봐야겠다!' 하는 사람들도 물론 있을 것이다. 이때 가장 좋은 방법은 이벤트 핸들러 함수 내에서 console.log를 찍는 것이 아니라 useEffect를 사용하는 것이다.

import React, { useState, useEffect } from 'react';

function Example() {

  const [count, setCount] = useState(0);

  const onClickHandler = () => {
    setCount(count+1);
  }

  useEffect(() => {
    console.log("count:", count)
  }, [count])

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={onClickHandler}>
       Click me
      </button>
    </div>
  );
}

이렇게 구현한다면 위 그림처럼 count 값이 변할 때마다 useEffect에 의해 console.log가 실행된 것을 확인할 수있다.(useEffect는 첫 렌더링 때 무조건 한번 실행되기 때문에 'count: 0'도 함께 찍혔다)