Zustand 리렌더링의 조건 (트러블슈팅)

2025. 4. 12. 17:58·Develop/Frontend

흔히들 zustand는 state가 바뀔 경우, 해당 state를 구독하고 있는 컴포넌트에서만 리렌더링이 일어날 것이라고 생각하는데 이는 반은 맞고 반은 틀린 말입니다. 즉 zustand에서 store를 만들고 해당 store에 있는 값(state 혹은 function) 일부를 select해서 가져오면 가져온 값에 대해서만 리렌더링이 일어날 것이라고 착각하곤 하는데 이를 잘 잡고 가야 합니다.

 

 

결론부터 말하자면, Zustand는 Store의 참조가 바뀔 때 리렌더링을 유발합니다.

제가 겪은 트러블 슈팅을 소개하면서 간단히 이해해봅시다.

 

 

트러블 슈팅 과정 (기존)

아래는 기존의 store 선언입니다. createStore API가 아닌 create API를 사용해 store 하나를 만들어두었습니다. 

import { create } from 'zustand'

const INITIAL_MAP_MOVED_STATE = false

export const useMapMoved = create<{
  mapMoved: boolean
  setMapMoved: (mapMoved: boolean) => void
  resetMapMoved: () => void
}>((set) => ({
  mapMoved: INITIAL_MAP_MOVED_STATE,
  setMapMoved: (mapMoved: boolean) => set({ mapMoved }),
  resetMapMoved: () => set({ mapMoved: INITIAL_MAP_MOVED_STATE }),
}))

 

그리고 아래와 같이 사용했습니다.

//컴포넌트 내부

const { setMapMoved } = useMapMoved((s) => ({
    setMapMoved: s.setMapMoved,
  }))
  
 const handleMapClick = () => {
 	setMapMoved(true);
 }
 
//어디선가 handleMapClick 를 이벤트 핸들러로 연결

 

 

그런데 setMapMoved가 호출될 때마다, 컴포넌트가 리렌더링 되는 현상이 발생했습니다. 

지도를 다루고 있기에 onTouchEnd, onTransitionEnd, onMoveEnd(맵 관련 커스텀 핸들러) 를 차근 차근 들여다보면서 분명 setMapMoved(true)가 호출될 때마다 리렌더링이 발생한다는 건 확실히 인지하고 있었어요.

 

제가 파악하지 못하는 의존성이 있을거라 생각해서 파일을 더욱 많이 찾아보기도 하고, zustand도 한번 더 들여다보면서 selector없이 zustand의 store를 사용한다면 모든 값을 가져오기에 리렌더링된다는 것도 다시 한번 알게 되었고요. 하지만 코드 어디에서도 mapMoved라는 상태를 직접적으로 사용하고 있는 곳은 없었습니다. 

 

결국 주석 처리를 해보면서 확인해보다가, 다른 컴포넌트에서 특이사항을 발견했습니다.
store를 아예 사용하지 않으면 리렌더링이 발생하지 않는다는걸요! zustand의 공식 문서도 바로 찾아봤지만, 명확한 해답은 없었습니다.

 

 

해결 과정

천천히 생각해보면 zustand에서 store의 특정 상태값을 갱신하려면, 반드시 객체 단위로 업데이트를 하곤 합니다. 여기서 힌트를 얻었어요. 결국 해답은 객체의 참조가 바뀌면서 새로운 객체로 인식되고 리렌더링이 유발된다는 것이었습니다. 생각해보면 당연한건데, 단순하게 구독한 state가 바뀌면 리렌더링 된다는 말을 믿어버린 것이 트러블슈팅을 길어지게 만들었던 것 같습니다. 역시 절차적으로 원인의 후보들과 블랙박스들을 찾아보면 명확한 해답이 나오는데 말이죠!

 

이제 회고는 여기까지 하고, 해결한 코드를 아래에 제시하겠습니다. 값이 바뀌었을 때 store 객체의 참조를 유지할지 말지 정하는 로직입니다.

 

import { create } from 'zustand'

const INITIAL_MAP_MOVED_STATE = false

export const useMapMoved = create<{
  mapMoved: boolean
  setMapMoved: (mapMoved: boolean) => void
  resetMapMoved: () => void
}>((set) => ({
  mapMoved: INITIAL_MAP_MOVED_STATE,
  setMapMoved: (mapMoved: boolean) =>
    set((prev) => {
      if (prev.mapMoved === mapMoved) return prev
      return {
        ...prev,
        mapMoved,
      }
    }),
  resetMapMoved: () =>
    set((prev) => ({ ...prev, mapMoved: INITIAL_MAP_MOVED_STATE })),
}))

 

 

QED.

'Develop > Frontend' 카테고리의 다른 글

개발자도구 Element 탭 강조 표시의 기준  (1) 2025.04.18
requestAnimationFrame 에 대한 이해  (4) 2025.04.17
zod 유효성 검사에서 겪은 트러블 슈팅  (0) 2025.02.04
배포를 위한 PAT  (0) 2025.01.18
버셀에 커스텀 도메인 연결하기  (0) 2025.01.14
'Develop/Frontend' 카테고리의 다른 글
  • 개발자도구 Element 탭 강조 표시의 기준
  • requestAnimationFrame 에 대한 이해
  • zod 유효성 검사에서 겪은 트러블 슈팅
  • 배포를 위한 PAT
ocahs
ocahs
개발 내용을 담습니다.
  • ocahs
    ocahs 개발 블로그
    ocahs
  • 전체
    오늘
    어제
    • 분류 전체보기 (47)
      • Develop (47)
        • Frontend (25)
        • Javascript (7)
        • Algorithm (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    line sweeping
    Working Set Model
    vite
    Promise
    번들러
    promise reject
    재귀타입
    비트 연산자 활용 예시
    요청의 역사
    JS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ocahs
Zustand 리렌더링의 조건 (트러블슈팅)
상단으로

티스토리툴바