Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

useState 똑똑하게 사용하기 본문

🎨 프론트엔드/React.js

useState 똑똑하게 사용하기

Chamming2 2021. 5. 10. 01:51

아마 리액트에서 가장 많이 사용되는 훅을 꼽아보라 하면 useState가 주인공이 될 것 같은데요, 오늘은 useState로 상태를 변경할 때 주의할 점에 대해 다뤄보려 합니다.

const [state, setState] = useState();

useState 훅은 상탯값과 상태를 변경할 수 있는 함수(※ action, setter 함수 등으로 부르는데, 여기서는 세터 함수라고 부르겠습니다.)를 제공하는데요, 세터 함수를 사용하면 원하는 값으로 상탯값을 변경할 수 있습니다.

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <div>{count}</div>
      {/* 버튼을 클릭하면 세터 함수가 호출됩니다. */}
      <button onClick={() => setCount(count - 1)}>+</button>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
};

export default Counter;

그런데 이 세터 함수에는 한 가지 함정이 숨어 있는데요, 바로 동일한 블럭에서 사용할 때는 렌더링 성능 향상을 위해 상태가 변경될 때마다 업데이트를 수행하는 대신 상태값의 변경사항을 모아뒀다가 한꺼번에 변경한다는 점입니다.

 

오늘은 이로 인해 생길 수 있는 몇 가지 상황과 해결 방법에 대해 다뤄보도록 하겠습니다. 😄

🔁 첫 번째 상황. 반복문 내에서 상태값 변경하기

import { useState } from "react";

function App() {
  const [state, setState] = useState([]);

  const setStateWithArray = () => {
    [1, 2, 3, 4, 5].forEach((value) => {
      setState([...state, value]);
    });
  };

  console.log(state);

  return (
    <div className="App">
      <div>
        {state?.map((value) => (
          <div>{value}</div>
        ))}
      </div>
      <button onClick={() => setStateWithArray()}>Run</button>
    </div>
  );
}

export default App;

코드가 조금 어려울 수 있는데, 여기서는 버튼을 눌렀을 때 호출되는 setStateWithArray 함수에만 집중해주시면 됩니다.

setStateWithArray 함수는 forEach를 따라 배열의 원소를 상탯값 배열에 추가하고 있는데, 과연 state에는 어떤 값이 담겼을까요?

버튼을 클릭한 결과 - 5만 상탯값 배열에 추가된 모습

forEach를 통해 [1, 2, 3, 4, 5]를 순회하면서 각 값을 상탯값 배열에 추가하는 듯 했지만, 실제로는 5만 배열에 포함되어 있었습니다.

 

원인은 바로 동일한 블럭 내에서는 세터 함수를 사용하더라도 상태값이 즉시 업데이트되지 않기 때문인데요, 따라서 반복문 내에서 상태값을 업데이트할때는 작업 결과를 임시로 저장해두고, 모든 작업이 끝난 후 저장해둔 임시 값을 상탯값으로 업데이트해야 합니다!

import { useState } from "react";

function App() {
  const [state, setState] = useState([]);

  const setStateWithArray = () => {
    const tempArray = [];
    [1, 2, 3, 4, 5].forEach((value) => {
      tempArray.push(value);
    });
    setState(tempArray);
  };

  console.log(state);

  return (
    <div className="App">
      <div>
        {state?.map((value, idx) => (
          <div key={idx}>{value}</div>
        ))}
      </div>
      <button onClick={() => setStateWithArray()}>Run</button>
    </div>
  );
}

export default App;

이렇게 반복문 내에서 업데이트를 수행할 때는 각 반복문에서 처리한 결과물을 임시로 저장한 뒤, 작업을 마친 다음 저장해둔 결과물을 세터 함수로 전달해주면 됩니다.

버튼을 클릭한 결과 - 1, 2, 3, 4, 5가 모두 상탯값 배열에 추가된 모습

2️⃣ 두 번째 상황. 동일한 블럭에서 상태를 여러번 업데이트하기

import React, { useState } from "react";

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

  async function fetchData() {
    setCount(1);
    await timer();
    setCount(2);
  }

  const timer = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("");
      }, 5000);
    });
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={fetchData}>실행</button>
    </div>
  );
}

export default App;

버튼을 누르면 카운트가 1이 되고, 5초 후에 2가 됩니다.

 

그런데, 세터 함수에 들어가는 인자를 조금 바꾸면 이상한 일이 일어납니다.

import React, { useState } from "react";

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

  async function fetchData() {
    setCount(count + 1);
    await timer();
    setCount(count + 1);
  }

  const timer = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("");
      }, 5000);
    });
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={fetchData}>실행</button>
    </div>
  );
}

export default App;

5초가 변해도 카운터가 변하지 않는 모습인데요, 왜 첫 번째 setCount는 동작하고 두 번째 setCount는 동작하지 않은 걸까요?

 

사실 두 setCount 모두 동작했습니다.

fetchData 부분을 리액트가 인식하는대로 작성하면 다음과 같이 됩니다.

import React, { useState } from "react";

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

  async function fetchData() {
    setCount(0 + 1); // count의 초깃값은 0입니다. 
    await timer();
    setCount(0 + 1); // count는 갱신된 상태값이 아닌 클로저를 통해 이전 맥락의 값을 계속 참조합니다.
  }

  const timer = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("");
      }, 5000);
    });
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={fetchData}>실행</button>
    </div>
  );
}

export default App;

이렇게 두 번째 setCount는 동작하지 않은 것이 아니라, 클로저에 의해 이전 값을 계속 참조하고 있었기 때문이라는 사실을 알 수 있는데요!

 

따라서, 동일한 블럭 내에서 이전 상태값을 참조해 상태를 업데이트할때는 세터 함수를 다음과 같이 사용해야 합니다.

setState(state + 1) 
setState(state + 1) // ❌, state는 이전 맥락의 값을 계속 참조함.

setState((state) => state + 1) 
setState((state) => state + 1) // ✅, 업데이트된 state를 참조함.

세터 함수 내에서 함수를 정의하면 해당 함수의 인자에는 이전 상태가 전달되고 업데이트할 상태값을 반환합니다. 

이러면 두 번째 세터 함수에서는 클로저의 영향으로 갱신되기 이전 상태를 참조하지 않고, 첫 번째 세터 함수에서 업데이트한 상태값을 제대로 참조할 수 있습니다! 😁

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆