본문 바로가기
트러블슈팅

Zustand 상태 변화와 useEffect 의존성 누락으로 인한 이슈

by stonage 2026. 3. 27.
이후 리팩토링이 이뤄지며 최종 코드와 다를 수 있음. 

 


⚠️ 문제상황

 

WebSocket에서 JOIN 이벤트를 수신한 뒤 Zustand store의 상태를 업데이트하고, 매칭 화면에서 useEffect로 해당 상태 변화를 감지해 채팅방 화면으로 이동하도록 구현했다.


matchingStore.tsx

handleMessage: (data) => {
  if (data.event === "JOIN") {
    set({
      matchingStatus: "COMPLETE",
      chatRoomId: data.payload.chatRoomId
    });
  }
};

 

index.tsx (matching)

useEffect(() => {
  if (matchingStatus === 'COMPLETE') {
    router.replace(`/(main)/${chatRoomId}`);
  }
}, [matchingStatus]);

하지만 실제로는:

- WebSocket 이벤트는 정상적으로 수신 됨

- store의 matchingStatus도 "COMPLETE"로 정상 변경됨

- useEffect도 실행됨

- 그런데 router.replace()에 사용되는 chatRoomId가 null이어서 화면 전환이 일어나지 않음

 


🔎 원인

문제의 원인은 근본적으로는 useEffect 의존성 설계 미스였다. 

 

Zustand에서 두 상태를 set()함수로 한 번에 업데이트 하기 때문에 useEffect 실행 시점에는 matchingStatus와 더불어 chatRoomId도 함께 반영되어 있을 것이라고 가정하였고 따라서 useEffect 내부에서 두 상태를 모두 사용하면서도 한 상태만 의존성 배열에 넣는 안일한 코드를 작성하였다. 

 


☑️ 해결

useEffect에서 사용하는 두 변수 모두의 변화를 감지하도록 chatRoomId도 의존성 배열에 추가한다.

useEffect(() => {
  if (matchingStatus === 'COMPLETE' && chatRoomId) {
    router.replace(`/(main)/${chatRoomId}`);
  }
}, [matchingStatus, chatRoomId]);

 

또는 useEffect의 실질적인 실행 트리거를 명확히 해서 문제를 해결할 수도 있다. 문제 상황이 발생했을 당시 코드에서는 라우팅에 실질적으로 필요한 상태는 chatRoomId의 상태변화였다. 

⚠️상태와 실제 행동 조건이 분리되어 있던 상황이었다.

따라서 matchingStatus가 아닌 chatRoomId의 상태변화를 관찰하게 하면 원하는 대로 동작하게 된다. 

useEffect(() => {
  if (chatRoomId) {
    router.replace(`/(main)/${chatRoomId}`);
  }
}, [chatRoomId]);