[React Zustand] useStore와 store.getState() 의 차이

[React Zustand] useStore와 store.getState() 의 차이

상태 관리하다 마주친 문제

최근 개인 프로젝트를 진행하면서 서버시간을 기준으로 클라이언트의 시간을 동기화 하기 위해 Zustand의 useStore에 저장하다가 고민하게 되며 알게된 내용이다.

  1. 페이지 접속 후 서버의 현재시간을 axios요청으로 받아와 state에 저장합니다.
  2. 받아온 시간을 기준으로 매초를 더하여 현재시간의 state를 갱신합니다.

위 내용은 최상위 레이아웃에 작성되어 있었기 때문에 어떠한 페이지를 접속해도 서버의 시간을 가져와 state를 저장할 수 있었다.

그러나 리액트를 처음 접해보고 상태관리에 대한 개념이 얕은 탓에 하위 컴포넌트들이 매초마다 리렌더링 되고 있었다.

오류가 발생한 코드

  const { setCurrentDatetime } = useCurrentDatetimeStore();
  useEffect(() => {
    const updateDateTime = async () => {
      const serverTime = await getServerTime();
      setCurrentDatetime(serverTime);
    };
    updateDateTime();
  }, [])

초기 useStore 사용 코드

위 코드는 useCurrentDatetimeStore에 setCurrentDatetime을 구조분해 할당으로 가져온 뒤
useEffect 내부에서 서버시간을 응답받아 상태를 저장한다.
setCurrentDatetime은 저장된 데이터를 기준으로 setInterval을 사용하여 서버시간에 매초를 더하였다.
그리고 useEffect에 빈배열을 넣으면 컴포넌트가 마운트될 때 한번만 실행한다 하였다.

실제로 useEffect는 한번만 실행된다. 그러나 해당 컴포넌트 및 하위 컴포넌트들 모두 매초마다 렌더링 되고 있었다.

useCurrentDatetimeStore를 통째로 가져오고 있어서 내부의 상태가 변경되면 리렌더링 되고있었다.

개선된 코드

const setCurrentDatetime  = useCurrentDatetimeStore((state) => state.setCurrentDatetime);
  useEffect(() => {
    const updateDateTime = async () => {
      const serverTime = await getServerTime();
      setCurrentDatetime(serverTime);
    };
    console.log("useEffect")
    updateDateTime();
  }, [])

첫번째 개선된 코드

Zustand 공식문서에서도 위와같이 필요한 상태만 불러와서 사용하라고 나와있었다.
그런데 useState를 처음 배울때 익숙해진 탓인지 공식문서의 내용을 무시하고 저장소객체를 통째로 가져와 사용하여 내부의 상태변화가 생길때 마다 모든 컴포넌트들이 리렌더링 되었던 것이다.

위와 같이 개선하고 나선 모든 컴포넌트가 리렌더링 되지 않았다.

그러나 또 다른 문제가 발생했다.

또 다른 문제

리액티브 상태

const currentDatetime  = useCurrentDatetimeStore((state) => state.currentDatetime);

리액티브 구독

해당 코드를 추가하니 또 다시 모든 컴포넌트가 리렌더링 되고 있었던 것이다.

위 코드는 현재의 상태를 구독한다. 즉 매초마다 currentDatetime의 상태가 변경되고 있기때문에 리렌더링 된다.

비리액티브 상태

나는 값만 읽어와 유효성검사만 하려 했기 때문에 해당코드를 아래와 같이 변경했다.

const { currentDatetime } = useCurrentDatetimeStore.getState();

비리액티브 구독

위 코드를 컴포넌트 내부에 작성하고 console 출력을 해보니 한번만 나왔다.
호출 시점에 한번만 나오게 된 원하던 결과다.

state와 getState()의 차이에 대한 의문점

그리고 난 의문이 들었다.
set만 사용할 땐 둘 다 같은 결과를 보였는데 왜 currentDatetime을 추가하면 한쪽은 리렌더링 되었을까?

1. const setCurrentDatetime = useCurrentDatetimeStore((state) => state.setCurrentDatetime);
2. const { setCurrentDatetime } = useCurrentDatetimeStore.getState();

위의 두 코드는 컴포넌트 내에서 같은 기능을 한다. 그러나 같은 형태로 currentDatetime이 추가되면
1번 코드는 리렌더링 되고 2번 코드는 리렌더링 되지 않는다.

일단 1번의 경우 Zustand의 공식문서에 나와있는 선택적 구독이다. 상태가 변경되면 감지하여 리렌더링 된다. 그런데 setCurrentDatetime은 함수이다. 함수의 상태는 변경될 수 없다고 생각한다.

그리고 2번의 경우 현재 상태만 읽기 때문에 비리액티브 접근이라고 한다.

아래 링크에 나와 같은 생각을 하던 사람이 있었다.

Difference between `useStore` and `store.getState()` · pmndrs zustand · Discussion #2194
Introduction I have been using useStore with zustand/shallow as the comparator to trigger update state. However, I find that I can get the state by store.getState() too. My store has a data field o…

set함수의 경우 상태 구독을 해봤자 함수의 상태는 변경되지 않으니 의미 없는 거 아닌가?
그럼 실제로 currentDatetime의 상태는 변경을 감지하여 리 렌더링 했으니 감지하는 로직이 내부적으로 있을 것이다 라고 생각한 것 같다.

그래서 질문에도 getState()를 사용하여 set 함수를 추출하여 사용하는 것이 실제로 state를 사용하여 접근하는 것보다 성능적인 이점이 있을 것 같다 라고 추측하였고 거기에 대한 답변이 맞다고 달렸다.

결론

  const { setCurrentDatetime } = useCurrentDatetimeStore.getState();
  useEffect(() => {
    const updateDateTime = async () => {
      const serverTime = await getServerTime();
      setCurrentDatetime(serverTime);
    };
    updateDateTime();
  }, [])

최종적으로 개선된 코드

set함수는 변경되지 않고, 시간이 외부에 렌더링 되지 않고, 내부적으로 상태만 존재하면 되었기에 나는 최종적으로 위와 같이 작성하여 사용하기로 했다.