9th 코드 로그 · React Hooks: useShallowState

허정석·2025년 7월 22일

TIL

목록 보기
9/19
post-thumbnail

useShallowState

🖥️ 구현 코드

초기 코드

import { useState } from "react";
import { shallowEquals } from "../equals";
import { useRef } from "./useRef";

export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
  // 1. useState 를 사용하여 실제 상태 관리.
  const [currentState, originSetState] = useState(initialValue);
  // 2. 반환할 originSetState 함수 정의.
  const customSetState = (newVal: unknown) => {
    // 새롭게 전달 받은 값을 계산. 함수인 경우를 고려하여 새로운 값을 도출
    const actualNewVal = typeof newVal === "function" ? newVal(currentState) : newVal;
    // 얕은 비교 사용하여 새로운 값과 현재 값을 비교 (상태 변경을 감지)
    if (!shallowEquals(actualNewVal, currentState)) {
      // 비교 값이 다르다면, origin 을 호출하여 상태 값 업데이트
      originSetState(actualNewVal);
    }
  };
  // 3. 화면이 여러번 그려져도 참조를 고정
  const callbackRef = useRef(customSetState);
  return [currentState, callbackRef.current];
};

변경 코드

import { useState } from "react";
import { shallowEquals } from "../equals";
import { useCallback } from "./useCallback";

export const useShallowState = <T>(initialValue: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void] => {
  // 1. useState를 사용하여 실제 상태 값과 원본 setState 함수를 관리.
  const [state, originalSetState] = useState(initialValue);

  // 2. useShallowState가 외부에 반환할 setState 함수를 useCallback으로 감싸서 참조를 고정.
  //    이 함수는 originalSetState의 함수형 업데이트를 사용하여 항상 최신 prevState를 받습니다.
  //    따라서 이 함수 자체는 외부 스코프의 'state' 변수에 의존할 필요가 없습니다.
  const customSetState = useCallback((newState: T | ((prevState: T) => T)) => {
    originalSetState((prevState) => {
      // <-- 변경점 1: originalSetState를 함수형으로 호출
      // newState가 함수 형태일 경우, 최신 prevState를 사용하여 실제 새 값을 계산합니다.
      const actualNewValue =
        typeof newState === "function"
          ? (newState as (prevState: T) => T)(prevState) // <-- 변경점 2: prevState 사용
          : newState;

      // 최신 prevState와 계산된 actualNewValue를 shallowEquals로 비교합니다.
      if (!shallowEquals(actualNewValue, prevState)) {
        // <-- 변경점 3: prevState 사용
        // 두 값이 다르면, actualNewValue로 상태를 업데이트합니다.
        return actualNewValue;
      }
      // 두 값이 같으면, 이전 상태를 그대로 반환하여 상태 업데이트를 건너뛰고 리렌더링을 방지합니다.
      return prevState;
    });
  }, []); // <-- 변경점 4: 의존성 배열을 비움

  // 현재 상태 값과 안정적인 setState 함수를 반환합니다.
  return [state, customSetState]; // <-- 변경점 5: customSetStateRef.current 대신 customSetState 반환
};

📘 work-log

work-log


🤖 Gemini 설명문

변경점 및 설명

  1. useRef 임포트 제거 및 useCallback 임포트 추가:

    • 이전: import { useRef } from "./useRef";
    • 이후: import { useCallback } from "./useCallback";
    • 설명: setState 함수의 참조 동일성을 유지하는 역할을 useCallback으로 대신 함.
  2. customSetState 함수를 useCallback으로 감쌈:

    • 이전: const customSetState = (newVal: unknown) => { ... };
    • 이후: const customSetState = useCallback((newState: T | ((prevState: T) => T)) => { ... }, []);
    • 설명: customSetState 함수 전체를 useCallback으로 감쌌습니다.
      이렇게 함으로써 customSetState 함수는 컴포넌트의 첫 렌더링 시에만 생성되고,
      그 이후로는 항상 동일한 함수 인스턴스를 재사용하게 됩니다.
  3. originalSetState의 함수형 업데이트 사용 (변경점 1, 2, 3):

    • 이전: originSetState(actualNewVal);

    • 이후: originalSetState(prevState => { ... return actualNewValue; });

    • 설명: customSetState 함수 내부에서 originalSetState를 호출할 때,
      직접 actualNewValue를 전달하는 대신 함수(콜백 함수)를 전달하도록 변경했습니다.

      • React는 이 콜백 함수를 호출할 때, 인자로 현재 시점의 최신 상태 값을 prevState라는 이름으로 전달해 줍니다.
      • 이 prevState를 사용하여 actualNewValue를 계산하고 ((newState as (prevState: T) => T)(prevState)), shallowEquals 비교를 수행합니다 (shallowEquals(actualNewValue, prevState)).
      • 이렇게 함으로써 customSetState 함수가 useCallback으로 인해 한 번만 생성되더라도, 그 내부 로직은 항상 최신 상태 값(prevState)을 기반으로 작동하게 되어 오래된 클로저 문제를 완벽하게 해결합니다.
  4. useCallback의 의존성 배열 비움 (변경점 4):

    • 이전: useRef(customSetState); (useRef는 의존성 배열이 없음)
    • 이후: useCallback((...) => { ... }, []);
    • 설명: customSetState 함수 내부에서 originalSetState의 함수형 업데이트를 사용하기 때문에, customSetState 함수 자체는 외부 스코프의 state 변수에 직접 의존할 필요가 없습니다. 따라서 useCallback의 의존성 배열을 비워두어 이 함수가 컴포넌트의 생명주기 동안 단 한 번만 생성되도록 할 수 있습니다.
  5. 반환 값 변경 (변경점 5):

    • 이전: return [currentState, customSetStateRef.current];
    • 이후: return [state, customSetState];
    • 설명: useCallback으로 감싼 customSetState 함수 자체가 이제 안정적인 참조를 가지므로, 더 이상 useRef를 통해 참조를 가져올 필요 없이 customSetState를 직접 반환합니다. currentState는 state로 이름을 변경하여 일관성을 유지했습니다.

💡 useShallowState는 setState 함수의 참조 동일성을 유지하면서도,
항상 최신 상태 값을 기반으로 shallowEquals 비교를 수행하여 불필요한 리렌더링을 방지하는
목표를 달성하게 됩니다.

0개의 댓글