SSAFY에서 프로젝트를 시작하면서 몇몇 질문을 받았습니다. 특히, 리액트에서 의도한 동작이 수행되지 않는 경우에 대한 질문이 많았습니다. 그런 질문의 대부분이 useState 훅에 대한 이해가 부족하였기 때문에 벌어진 일이었습니다. 그래서 이 문제가 발생하는 원인을 설명하고 간단한 해결책을 알려드리겠습니다.

useState의 set 함수는 상태를 즉시 변경하지 않습니다.

useState로 만든 상태 변수를 읽을 때, set 함수를 통해 상태를 업데이트 하였지만 최신값을 가지고 있지 않기 때문에 문제가 발생합니다. 다음 코드를 보시죠.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  const onClick = () => {
    setCount((currentCount) => currentCount + 1);
    setCount2(count);
    console.log("버튼 클릭");
  };

  return (
    <>
      <button onClick={onClick}>{count}</button>
      <p>onClick 함수 내부의 count값: {count2}</p>
    </>
  );
}

export default App;

button 태그를 클릭할 때마다 count가 1씩 증가합니다. 그리고 count2는 곧 바로 count와 같은 숫자로 설정이 되는 것처럼 보입니다. 하지만 과연 그럴까요? 해당 컴포넌트를 실행 시키고 count를 10번 눌러 봅시다.

count는 10인데 count2는 그보다 1이 작은 9가 되었습니다. 다시 코드 돌이가 console.log로 "버튼 클릭"이 아닌 count의 값을 출력해보도록 해보면 클릭할 때마다 console.log로 출력된 값은 count가 증가하지 전의 값을 가집니다. 왜 이런 현상이 발생했을까요?

 

State 변수는 읽고 쓸 수 있는 일반 자바스크립트 변수처럼 보일 수 있습니다. 하지만 state는 스냅샷처럼 동작합니다. state 변수를 설정하여도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 발동됩니다.

 

공식 문서에는 위와 같은 표현을 사용하고 있습니다. 이게 무슨 말일까요? 리액트의 컴포넌트는 렌더링 된 후 상태를 마치 사진을 찍듯이 그 순간의 상태 값들을 보게 됩니다. 다시 말해, 리액트 컴포넌트 내부에서 생성된 상태들은 렌더링이 완료된 직후 값들로 고정되어 있는 것입니다. 그래서 다시 렌더링이 일어나 새로운 스냅샷을 찍기 전까지는 처음 그대로의 고정된 값들을 가지는 것이죠. 위의 코드를 하나씩 다시볼까요?

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

App 컴포넌트가 가진 모든 상태들입니다. 단 두 개의 상태만을 가지고 있죠. 최초에 렌더링되면 count와 count2 모두 0입니다. 그리고 이 값은 스냅샷처럼 찍혀 고정되어 있습니다. 앞으로 다시 렌더링이 일어나기 전까지 count와 count2 모두 0인 것에 주의해주세요.

  const onClick = () => {
    setCount((currentCount) => currentCount + 1);
    setCount2(count);
    console.log("버튼 클릭");
  };

클릭 이벤트가 발생합니다. setCount를 통해 count를 현재의 값에 1을 더한 값으로 바꿀 것으로 예약합니다. 여기서는 1이 되겠죠. 하지만 앞서 말했듯이 리액트 컴포넌트는 다음 렌더링 전까지 상태는 가장 최근의 렌더링 시에 정해진 값으로 고정되어 있습니다. 아직 실행해야될 코드가 남아 있기 때문에 컴포넌트는 리렌더링이 일어나지 않습니다. setCount2(count)가 실행될 때 count는 여전히 0입니다. 그렇다면 setCount2(count)는 count2를 0으로 바꾸겠죠? 그런 다음 console.log가 호출되며 콘솔 창에 "버튼 클릭"이 출력되고 onClick함수가 종료됩니다. 이제 리액트는 리렌더링을 시작합니다. set 함수로 인해 상태가 변경되었기 때문이죠. 그러면 다시 컴포넌트(App 함수)가 실행됩니다. 두 번째 렌더링이 시작되는 것이죠.

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

useState의 인자는 최초 렌더링시에만 상태로 할당하고 이후에는 이 값을 무시합니다. 그래서 두 번째 렌더링인 지금은 이전에 정해졌던 count의 값인 1이 들어가게 됩니다. 그리고 count2 역시 이전에 정해졌던 값인 0이 들어가게 됩니다. 그래서 count와 count2의 값이 차이가 생기게 되는 것입니다.

useState 흔한 실수 해결하기

다음은 위와 같은 이유로 발생한 문제에 대해 제가 받은 질문들을 토대로 간단하게 재구성한 함수입니다.

function App() {
  const [myData, setMyData] = useState(null);

  useEffect(() => {
    const ressponse = getSomthing(url); // HTTP 요청

    setMyData(ressponse.data); // myData를 받은 데이터로 설정

    doSomthing(myData); // 받은 데이터로 무언가를 하려고 시도함
  }, []); // 최초 렌더링 시 한번만실행되도록 함

  return;
  <></>;
}

위의 코드에서 문제점이 무엇인지 바로 발견하셨을 것입니다. 문제점은 바로 바로 useEffect 내부에서 myData는 초기 값인 null로 고정되어있다는 것입니다. 그래서 doSomthing이라는 함수는 요청으로 받아온 데이터를 사용하지 못하고 있습니다. 이를 해결할 방법은 두 가지가 있습니다.

해결법

HTTP 요청을 보내는 대신 1이라는 값을 응답 받았다고 가정하고 해결법을 소개하겠습니다.

 

해결법은 아주 간단합니다. 응답으로 온 데이터를 직접 사용하여 doSomething함수(여기선 console.log())를 실행하는 것입니다.

  useEffect(() => {
    const ressponse = 1;

    setMyData(ressponse);

    console.log(ressponse);
  }, []);

이렇게 사용하면 컴포넌트가 다시 렌더링 되기 전이라도 원하는 값을 확실하게 사용할 수 있습니다. 상태 변경이 된 이후의 코드라도 리렌더링이 일어나지 않는다면 이후의 모든 코드는 변경되기 전 상태를 사용한다는 것을 꼭 기억하신다면 이런 문제를 피할 수 있습니다.

+ Recent posts