useState 이것만은 알고 쓰자!

박민우·2023년 12월 14일
1

🎉 React

목록 보기
2/4
post-thumbnail

리액트에서 가장 많이 사용하는 hook은 높은 확률로 useState일 것입니다. 기본적인 hook인 만큼, 사용하면서 주의해야할 점들을 정리해보았습니다💡


📌 상태값 변경시 리렌더링 발생

useState에서 반환된 배열의 두번째 값인 setter 함수를 호출하면 상태 값을 변경할 수 있고, 상태 값이 변경되면 해당 컴포넌트는 다시 렌더링됩니다.

=> 컴포넌트가 마운트되었을 때는, 기본적으로 useState(초기값)에서 인자로 전달된 값을 상태의 초기값으로 사용합니다. 하지만 이후 setter 함수에 의해 상태의 값이 변경되었다면, 다음 렌더링에서는 그 상태를 유지합니다.

setState 호출 => 상태 변경 => 리렌더링(변경된 상태값 사용)


📌 setState는 비동기로 동작한다! 그 이유는 ?

useState의 setter 함수를 호출한 이후 바로 다음 줄에서 해당 state값을 참조하면 아직 바뀌기 이전 state가 참조됩니다. 이는 useState의 setter 함수가 비동기적으로 동작하기 때문입니다.

function App() {
  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);

  return (
    <div>
      <div>나이: {age}</div>
      <button
        onClick={() => {
          setCount(count + 1); // 나중 실행
          if (count < 3) { // 먼저 실행 
            setAge(age + 1);
          }	
        }}
      >
        카운트: {count}
      </button>
    </div>
  );
}

위 코드 상에서 버튼을 누르면 다음과 같은 일이 발생합니다.

  1. 버튼을 누르면 count를 +1 해줍니다.
  2. 그리고 만약에 count가 3보다 적으면 age도 +1 해줍니다.

따라서 버튼을 계속 클릭하면, age는 20에서 22가 되면 더 이상 증가하지 않고 멈춰야합니다. 하지만 실제로는 age는 23까지 증가합니다. 즉, count가 3일 때에도 age +1를 해주고 있는 것입니다.

이는 state 변경함수가 비동기적으로 동작하는 함수이기 때문입니다. setCount(count+1) 동작으로 인해 count가 3이 되기 전에 if(count < 3) {} 에 먼저 진입하기 때문에 age가 23까지 증가하는 것입니다.


비동기로 동작하는 이유는?

만약 한 컴포넌트 안에서 여러 state 값을 연속으로 변경하는 일이 생긴다면 여러 번 비교하고 다시 그리는 알고리즘이 실행(렌더링이 계속 일어남)됩니다.

만약 이 과정이 동기적으로 실행된다면 바뀌지 않아도 되는 불필요한 리렌더링이 다수 발생해 비효율적이고 성능도 안 좋을 것입니다.

❓ 따라서 한번에 모아서 batch 처리를 해주기 위해 setState가 비동기적으로 동작한다는 건가?


setState 함수는 비동기적으로 동작하지만, 동기함수이다?

리액트의 setState가 동기적 함수이고 마치 비동기 함수처럼 보이는 이유는 리엑트의 리렌더링 원리가 비동기적으로 작동하기 때문입니다.

리엑트는 렌더링 함수를 호출하여 가상 돔을 업데이트합니다.

setState가 비동기 함수처럼 보이는 이유는 setState 함수 그 자체가 비동기 함수여서가 아니라 리액트가 가상돔을 사용하게 설계되어있기 때문입니다.

❓ 그렇다면, setState 함수 자체는 동기함수이므로, 호출 자체는 동기적으로 되는데, 그 변경으로 인한 가상돔을 통한 리렌더링이 비동기적으로 이루어져서 setState가 비동기적으로 작동하는 것처럼 보이는 것????

더 알아보기

https://velog.io/@jay/setStateisnotasync


setState 이후 특정 작업을 순차적으로 실행해주고 싶다면 ?

=> useEffect와 useEffect의 의존성 배열을 활용

=> 어떤 상태값이 바뀐 후 동작해야하는 코드가 있다면, useEffect를 사용해 해당 값을 감시하다가 상태가 변하면 동작하도록 해줄 수 있습니다.


📌 짧은 시간안에 setter 함수를 연속적으로 호출 시 렌더링은 1번만 진행

만약 다음과 같이 setter 함수를 연속적으로 호출 했을 경우 렌더링은 1번만 진행됩니다.

"use client"
 
import { useEffect, useState } from "react";
 
export default function Page() {
  const [number, setNumber] = useState(0);
 
  useEffect(() => {
    setNumber(prev => prev + 1);
    setNumber(prev => prev + 1);
    setNumber(prev => prev + 1);
    setNumber(prev => prev + 1);
    setNumber(prev => prev + 1);
  }, []);
 
  useEffect(() => {
    console.log(`[${performance.now()}] 렌더링 완료됨. number => ${number}`);
  }, [number]);
 
  return (
    <>
      { 
        (function() {
          console.log(`[${performance.now()}] component return 됨.`);
          return <></>;
        })() 
      }
      <div className="w-full relative">
        현재 number 값 : { number }
      </div>
    </>
  );
}

리액트에서는 배치 상태 업데이트라고 하여 짧은 시간 이내에 setter 함수가 연속적으로 호출 되었을 경우에는 호출 된 값 또는 업데이터 함수들을 큐에 집어 넣어 놓고 대기합니다.

그리고 그 짧은 시간이 지난 후 큐에 집어 넣은 것들을 순차적으로 일괄적으로 호출(적용) 하기 시작합니다. 이러한 배치 상태 업데이트 방식을 채택하고 있으므로, setter 함수를 연속으로 호출해도 렌더링은 1번만 발생시키는 것이 가능합니다.


📌 업데이터 함수로 상태를 변경해야 하는 경우

setter 함수의 인자에는 값을 넣을 수도 있지만, 업데이터 함수를 넣을 수도 있습니다. 업데이터 함수는 인자에 prev 값이 넘어오면서 next state 값이 반환되게 되어 있도록 정의된 함수입니다. 업데이터 함수를 사용해야 하는 경우는 바로 이전 값을 참조해야 하는 경우입니다.

const [number, setNumber] = useState(0);
 
useEffect(() => {
  setNumber(prev => prev + 1);
  setNumber(prev => prev + 1);
  setNumber(prev => prev + 1);
  setNumber(prev => prev + 1);
  setNumber(prev => prev + 1);
}, []);

위 코드를 실행해보면 number 는 5로 바뀌어 있는 것을 확인 할 수 있습니다. 업데이터 함수 자체도 인자에 이전 상태 값을 받아올 수 있지만, 업데이터 함수들 끼리도 이전에 호출된 업데이터 함수에서 반환된 상태 값을 이전 상태 값으로 참조할 수 있습니다. 즉 이전 값이 반드시 필요한 상태 업데이트 같은 경우에는 업데이터 함수를 사용해야 합니다.


📌 state의 불변성을 지켜야한다.

리액트에서 state의 불변성을 지킨다는 것은, 상태 변경 시 기존의 상태 객체나 배열을 직접 수정하지 않고, 새로운 객체나 배열을 생성하여 상태를 업데이트하는 것을 의미합니다.

불변성을 지켜야 하는 이유는 무엇인가요?

리액트가 상태를 업데이트 할 때, 얕은 비교를 수행하기 때문입니다.

예를 들어 상태가 배열이라면, 배열의 요소 하나하나를 다 비교하는 것이 아니라, 이전 배열의 참조값과 현재 배열의 참조값만을 비교하는 것입니다.

따라서 배열에 특정 요소가 push 되었더라도, 배열의 참조값이 바뀌지 않으면 리액트는 상태 변화를 감지하지 못합니다. 따라서 새로운 배열을 생성하여 상태를 업데이트 해줘야 합니다.

예를 들어, useEffect 훅은 의존성 배열의 원소들 중 하나라도 이전 렌더링 시의 그것과 다른 값을 가질 경우 콜백을 실행합니다.

여기서 useEffect 훅의 의존성 배열 원소들의 비교는 Object.is 메서드를 사용합니다. 원소들이 객체일 경우, 참조값이 서로 같으면 변경되지 않았다고 판단합니다.

따라서 state의 불변성을 지킴으로써 리액트는 상태변화를 감지할 수 있는 것입니다.


불변성을 유지하는 예시

새로운 객체나 배열을 생성함으로써 불변성을 유지할 수 있습니다. ..., map, filter, slice, reduce 등등 새로운 배열을 반환하는 메소드들을 활용하면 됩니다.

const [item, setItems] = useState(['item1', 'item2']);
const addItem = newItem => {
    setItems([...items, newItem]); // 새로운 객체를 생성 ?
}

📌 여러 data를 객체로 묶어서 state 관리하기

관리해야 할 data가 여러개라면, 비슷한 data 끼리는 객체로 묶고, 공통점이 없는 data들은 분리해서 각 상태의 책임을 분산하는 것이 좋습니다. 이렇게 한다면, 모듈화도 쉬워지고 불필요한 큰 객체 생성도 방지할 수 있습니다.

여기서의 책임은 변화를 의미합니다. 서로 같이 업데이트되는 상태끼리는 하나의 객체로 묶어도 된다는 의미입니다. 예를 들어 마우스의 x좌표, y좌표 같은 상태는 별도로 관리하는 것보다는 하나의 객체로 묶는 것이 더 효율적일 것입니다.


🙇🏻‍♂️ 참고

state 변경함수 사용할 때 주의점 : async

setState가 내 마음처럼 동작하지 않는 이유(feat.비동기)

useState 에 대해 알아봅시다

profile
꾸준히, 깊게

0개의 댓글