이 글은 Tanstack Query와 SSE를 사용하는 프로젝트에서 상태 동기화 문제를 경험한 분들을 위한 사례 공유입니다.
커리어비에서 알림 도메인을 개발하면서 정말 많은 트러블 슈팅이 있었다.
오늘은 그 중 하나인 “알림 아이콘 읽음 처리 업데이트” 에 대해서 이야기해보겠다.
문제 상황
먼저 커리어비에서는 SSE를 사용하여 새로운 알림이 생기면 그 여부를 boolean으로 받아온다. 그러면 이를 header의 알림 아이콘에 업데이트 해준다.
이슈는 여기에서 발생한다.
사용자는 알림을 보기 위해 알림 아이콘을 클릭한다.
클릭 시 /notification 으로 이동한다.
이동 후, header의 알림 아이콘이 일반 아이콘으로 되돌아가야 한다.
클라이언트와 서버의 관점 차이
여기서 백엔드와의 로직이 정확히 일치하지 않는다는 점이 이슈의 시발점이었다.
우선 커리어비에서는 “알림 페이지에 진입 함” = “알림을 읽음” 으로 간주하였다.
🎨 클라이언트 입장
- 알림 페이지에 진입 시 바로 header의 알림 아이콘이 정상으로 돌아와야 한다.
🗄️ 서버 입장
- 새 알림들의 ID를 PATCH 요청을 통해 받고, 해당 알림들의 읽음 여부를 '안읽음' -> '읽음' 으로 업데이트한다.
- 알림테이블에서 각 유저들의 알림과 읽음 여부를 함께 저장하고 있기 때문이다
hasNewAlarm의 T/F 변환 시점에 대해서 클라이언트와 서버 사이의 입장 차이가 발생했다.
하지만 이것 때문에 백엔드에게 데이터필드를 추가하라고 할 수는 없는 노릇이기에 나는 두 가지 방법을 생각해냈다.
해결 방안 후보
[방법 1] useEffect에서 setQueryData로 즉시 false 처리
useEffect(() => {
queryClient.setQueryData(['userinfo'], (oldData: any) => {
if (!oldData) return oldData;
if (oldData.hasNewAlarm === false) return oldData;
return {
...oldData,
hasNewAlarm: false,
};
});
}, []);
이런 식으로 강제로 쿼리 데이터를 설정한다.
❌ 단점
- PATCH 요청이 완료되기 전 userInfo 쿼리가 stale되어 refetch되면:
- 서버에서는 아직 hasNewAlarm: true 상태라 다시 true로 돌아옴
- 아이콘이 깜빡였다 다시 켜질 수 있음
- 일관성이 깨질 가능성
[방법 2] PATCH 완료 후 refetchQueries
await PATCH(...);
queryClient.refetchQueries(['userInfo'])
PATCH가 성공하면 서버에서 변경된 userInfo 데이터를 refetch 한다.
❌ 단점
- POST 응답이 느린 경우, 사용자가 알림 페이지 들어갔는데 헤더 알림 아이콘이 한참 후에 사라짐
- 즉시 피드백이 없음
- 체감상 UX가 딜레이된다고 느낄 수 있음
- 네트워크 레이턴시 의존
UI 깜빡임이 일어날 수 있는 상황을 정리해보자면 아래와 같다.
- 네트워크 상태가 느린 환경 (예: 모바일 3G)
- 백엔드 응답 지연 (예: 1~2초 이상)
- 알림을 읽음 처리하는 POST 요청과, userInfo 쿼리가 둘 다 활성화 상태라 refetch 타이밍이 겹침
하지만 최대한 낙관적 업데이트 패턴을 적용하는게 UX 측면에서 좋을 것이라고 판단했다.
따라서 아래의 방법을 생각했다.
[방법 3] 뮤테이션 사용
// PATCH 요청 중에는 refetch 하지 않게
const mutation = useMutation(markNotificationsAsRead, {
onMutate: async () => {
// 알림 즉시 꺼지기
queryClient.setQueryData(['userInfo'], (old) => ({ ...old, hasNewAlarm: false }));
// 기존 쿼리 취소 (중간 갱신 방지)
await queryClient.cancelQueries(['userInfo']);
},
onSuccess: () => {
queryClient.invalidateQueries(['userInfo']);
},
});
100% 데이터의 일관성을 유지할 수 있는 방법이 맞다.
하지만 실사용에서 알림 상태 깜빡임이 500ms 이하에 그쳤고, 사용자 피드백에서 혼란을 느낀 사례가 없었다.
따라서 뮤테이션은 오버스펙이라는 결론을 지었다.
[방법 4] setQueryData와 refetchQueries 모두 사용 + 롤백
말 그대로 위의 방법을 모두 적용한다. 이렇게 하면 데이터 무결성도 충분히 보장될 수 있다.
이제 완벽한 낙관적 업데이트 패턴을 적용하기 위해서 롤백 로직만 구현하면 된다.
롤백 로직은 아래처럼 작성하였다.
const previousUserInfo = queryClient.getQueryData(['userInfo']);
try {
await PATCH(...);
queryClient.refetchQueries({ queryKey: ['userinfo'] });
} catch (error) {
// 롤백
queryClient.setQueryData(['userInfo'], previousUserInfo);
}
회고
이번 경험을 통해 실시간 데이터 동기화는 단순히 UI에 표시되느냐 의 문제가 아니라, 데이터 일관성과 사용자 체감 속도, 그리고 개발 비용까지 함께 고려해야 하는 영역이라는 걸 다시 느꼈다.
처음에는 그냥 어차피 금방 갱신될 거니까 refetch만 해도 되겠지 라고 생각했다. 그런데 막상 테스트를 해보니, 네트워크가 느린 환경이나 저사양 기기에서는 알림 아이콘이 깜빡거리는 순간이 보였다. 이런 작은 부분도 신경쓰지 않을 수 없다.
그리고 중요한 건 어떤 선택을 하든 트레이드오프가 있다는 사실이다. 낙관적 업데이트를 쓰면 사용자 입장에서는 빠르게 반응하는 것처럼 보여서 좋지만, 만약 서버 처리가 실패했을 때는 롤백까지 책임져야 한다. 생각할게 많아지지만 서비스는 견고해진다.
수많은 기술들이 있지만 어디에 어떻게 적용하느냐는 나의 선택하고, 구현 방안도 수두룩하지만 그중 무엇을 선택하느냐는 나의 판단에 의한다. 그 근거들도 있어야 한다.
이번 이슈를 통해 단순히 알림 상태를 처리하는 작업을 넘어, 기능의 복잡도를 어디까지 가져갈 것인지, 그리고 사용자 경험과 개발 효율의 균형을 어떻게 잡을지를 고민할 수 있었다. 앞으로도 파이팅!!
'🖌️Frontend' 카테고리의 다른 글
[Figma] 피그마 (0) | 2025.02.20 |
---|