본문으로 바로가기

[React] useState란?

category 1. 웹개발/1_1_5 React JS 2022. 3. 18. 22:31


[useState란?]

useState는 함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅입니다.

const [state, setState] = useState(초기값);

 

useState의 인수로는 사용할 state의 초깃값을 넘겨줍니다. 아무런 값을 넘겨주지 않으면 초깃값은 undefined입니다. useState 훅의 반환 값은 배열이며, 배열의 첫 번째 원소로 state 값 자체를 사용할 수 있고, 두 번째 원소인 setState 함수를 사용해 해당 state의 값을 변경할 수 있습니다.

 

만약 useState를 사용하지 않고 함수 내부에서 자체적으로 변수를 사용해 상태값을 관리한다고 가정해보겠습니다.

function Component() {
  let state = 'hello 코딩병원~';

  function handleBtnClick() {
    state = 'hi 코딩병원!';
  }

  return (
    <>
      <h1>state</h1>
      <button onClick={handleBtnClick} />
    </>
  )
}

 

위 코드에서 버튼을 클릭했는데도 여전히 'hello 코딩병원~'으로 화면이 나옵니다. 그 이유는 무엇일까요? 리액트에서 렌더링은 함수 컴포넌트의 return을 실행한 다음, 이 실행 결과를 이전의 리액트 트리와 비교해 리렌더링이 필요한 부분만 업데이트해 이뤄집니다. 따라서 위의 코드는 리렌더링 조건을 충족하지 못했습니다. 그렇다면 다음과 같이 하면 어떨까요?

function Component() {
  const [, triggerRender] = useState();
  let state = 'hello 코딩병원~';

  function handleBtnClick() {
    state = 'hi 코딩병원!';
    triggerRender();
  }

  return (
    <>
      <h1>state</h1>
      <button onClick={handleBtnClick} />
    </>
  )
}

 

useState 반환값의 두 번쨰 원소를 실행해 리액트에서 렌더링이 일어나게끔 변경하였습니다. 그럼에도 여전히 'hello 코딩병원~'으로 화면은 그대로입니다. state가 업데이트 되고 있는데 왜 렌더링이 되지 않을까요? 그 이유는 리액트의 렌더링은 함수 컴포넌트에서 반환한 결과물인 return의 값을 비교해 실행되기 때문입니다. 즉, 매번 렌더링이 발생될 때 마다 함수는 다시 새롭게 실행되고, 새롭게 실행되는 함수에서 state는 매번 hello로 초기화되므로 아무리 state를 변경해도 다시 hello로 초기화되는 것입니다.

 

함수 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고, 함수 내부의 값은 함수가 실행될 때 마다 다시 초기화됩니다. 그렇다면 useState 훅의 결괏값은 어떻게 함수가 실행돼도 그 값을 유지하고 있을까요?

 

그 정답은 바로 클로저입니다. 클로저는 어떤 함수(useState) 내부에 선언된 함수(setState)가 함수의 실행이 종료된 이후에도(useState가 호출된 이후에도) 지역변수인 state를 계속 참조할 수 있다는 것을 의미합니다. 클로저를 사용함으로써 외부에 해당 값을 노출시키지 않고 오직 리액트에서만 쓸 수 있었고, 함수 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있게 됐습니다.

 

 

[Lazy Initalization(게으른 초기화)]

일반적으로 useState에서 기본값을 선언하기 위해 useState() 인수로 원시값을 넣는 경우가 대부분입니다. 그러나 이 useState의 인수로 특정한 값을 넘기는 함수를 인수로 넣어줄 수도 있습니다. useState에 변수 대신 함수를 넘기는 것을 게으른 초기화(Lazy Initialization)이라고 합니다. 코드를 통해 살펴보겠습니다.

function Component() {
  // 일반적인 useState 사용
  // 바로 값을 집어넣는다
  const [count, setCount] = useState(
    Number.parseInt(window.localStorage.getItem(catcheKey))
  )
  
  // 게으른 초기화
  // 위 코드와의 차이점은 함수를 실행해 값을 반환한다는 것
  const [count, setCount] = useState(() =>
    Number.parseInt(window.localStorage.getItem(catcheKey))
  )
}

 

리액트 공식문서에서 이러한 게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 되어있습니다. 이 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용됩니다. 만약 이후에 리렌더링이 발생한다면 이 함수의 실행은 무시됩니다. 다음 코드를 살펴봅시다.

import { useEffect, useState } from "react";

function App() {
  const [state, setState] = useState(() => {
    // App 컴포넌트가 처음 구동될 때만 실행되고, 이후 리렌더링 시에는 실행되지 않음.
    console.log('복잡한 연산...');

    return 0;
  })

  function handleClick() {
    setState((prev => prev+1))
  }

  return (
    <h1>{state}</h1>
  )
}

 

리액트에서는 렌더링이 실행될 때 마다 함수 컴포넌트의 함수가 다시 실행된다는 점을 명시해야 합니다. 함수 컴포넌트의 useState의 값도 재실행됩니다. 물론 앞서 설명을 통해 내부에는 클로저가 존재하며, 클로저를 통해 값을 가져오며 초깃값은 최초에만 사용된다는 것을 알고 계실겁니다. 만약 useState 인수ㅇ이 많이 드는 경우로 자바스크립트에 많은 비용을 요구하는 작업이 들어가 있다면 이는 계속해서 실행될 위험이 존재할 것입니다. 그러나 우려와 다르게 useState 내부에 함수를 넣으면 이는 최초 렌더링 이후에는 실행되지 않고 최초의 state 값을 넣을때만 실행됩니다.

 

만약 Number.parseInt(window.localStorage.getItem(catcheKey))와 같이 한 번 실행되는 데 어느 정도 비용이 드는 값이 있다고 가정해봅시다. useState의 인수로 이 값 자체를 사용한다면 초깃값이 필요한 최초 렌더링과, 초깃값이 있어 더 이상 필요 없는 리렌더링 시에도 동일하게 계속 해당 값에 접근해서 낭비가 발생합ㄴ디ㅏ. 따라서 이런 경우에는 함수 형태로 인수에 넘겨주는 편이 훨씬 경제적일 것입니다. 초깃값이 없다면 함수를 실행해 무거운 연산을 시도할 것이고, 이미 초깃값이 존재한다면 함수 실행을 하지 않고 기존 값을 사용할 것입니다.

 

그렇다면 게으른 초기화는 언제 쓰는 것이 좋을까요? 리액트에서는 무거운 연산이 요구될 때 사용하라고 합니다. 즉 localStorage나 sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근, 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우에 게으르 초기화를 사용하는 것이 좋습니다.

 

 

 

Reference
react.org-hooks-useState

[Javascript] 불변성이란?

모던 리액트 Deep Dive