React Query 캐시 전략으로 PR 댓글 API 요청을 10회에서 0회로 줄인 과정
React Query 캐시 전략으로 PR 댓글 API 요청을 10회에서 0회로 줄인 과정
CodeMate의 PR 댓글은 실시간 이벤트가 자주 발생하는 화면이었다.
댓글 작성, 수정, 삭제가 반복될 때마다 화면은 즉시 반응해야 했고, 사용자는 지연 없이 변화를 봐야 했다.
처음에는 invalidateQueries로 안전하게 다시 받아오는 방식이 중심이었지만, 이 구조는 이벤트 수가 늘수록 네트워크 비용이 계속 커졌다.
그래서 나는 변경분만 반영하는 setQueryData 중심 구조로 전환했다.
핵심 판단은 하나였다.
이 화면에서 필요한 것은 “항상 최신 상태를 다시 받는 것”이 아니라 “바뀐 부분만 정확히 갱신하는 것”이었다.
1. 문제 상황
PR 댓글은 한 번에 한 건씩만 바뀌지 않았다.
새 댓글이 추가되고, 일부 댓글은 수정되거나 삭제됐다.
알림도 함께 갱신됐다.
이런 화면에서 매번 전체 목록을 다시 불러오면, 사용자 입장에서는 실시간처럼 보이더라도 내부적으로는 반복적인 요청이 쌓였다.
기존 방식은 단순했다.
- 이벤트 발생
invalidateQueries- 전체 댓글 목록 refetch
- 화면 재렌더링
이 방식은 구현이 안전하고 직관적이다.
하지만 댓글처럼 변경 범위가 작은 데이터에 대해선 과했다.
실시간 이벤트가 몇 번만 반복돼도 같은 리스트를 계속 다시 받게 되기 때문이다.
2. 원인 분석
문제의 본질은 캐시를 “변경 가능한 상태”가 아니라 “무조건 다시 받아야 하는 상태”로 다뤘다는 점이었다.
invalidateQueries는 서버 상태를 다시 검증하는 데 유리하다.
데이터가 복잡하거나, 서버 계산 결과가 많이 바뀌는 화면이라면 좋은 선택이다.
하지만 PR 댓글은 조금 달랐다.
- 새 댓글은 기존 리스트 뒤에 붙는다.
- 수정은 특정 댓글 하나만 바뀐다.
- 삭제는 해당 항목만 제거하면 된다.
즉, 전체를 다시 가져올 필요가 없는 변경이 대부분이었다.
그런데도 invalidateQueries를 쓰면 매 이벤트마다 네트워크 요청이 발생한다.
이건 정확하지만 비효율적인 방식이었다.
실무에서 중요한 건 “항상 안전한 방식”이 아니라 “현재 데이터 특성에 맞는 방식”을 고르는 것이다.
이 화면은 후자가 더 맞았다.
3. 기존 방식의 한계
invalidateQueries에는 분명 장점이 있다.
서버와 클라이언트 상태 불일치를 줄이기 쉽고, 구현도 단순하다.
하지만 이 프로젝트에서는 다음 한계가 있었다.
- 이벤트 1건마다 전체 목록을 다시 가져와야 했다.
- 변경 폭이 작은데도 요청 비용은 항상 동일했다.
- 댓글 수가 늘수록 refetch로 인한 중복 비용이 누적됐다.
- 네트워크가 느린 환경에서는 체감 지연이 더 커졌다.
이 방식은 “정확성”에는 강하지만, “네트워크 비용”에는 약했다.
특히 실시간 댓글처럼 사용자 행동이 연속적으로 이어지는 화면에서는 더 불리했다.
또 하나의 문제는 개발 경험이었다.
화면이 커질수록 “이 변경은 refetch가 필요한가, 아닌가”를 계속 고민해야 했고, 모든 이벤트를 동일한 방식으로 처리하면 성능 최적화 포인트를 놓치기 쉬웠다.
4. 해결 방법
나는 캐시 갱신 전략을 명확히 나눴다.
- 서버 재검증이 필요한 변경은
invalidateQueries - 변경분만 안전하게 반영 가능한 경우는
setQueryData
댓글 화면에서는 대부분 후자였다.
새 댓글, 수정, 삭제 모두 캐시 일부만 바꾸면 충분했다.
그래서 이벤트가 들어오면 전체 재조회 대신 setQueryData로 해당 항목만 바로 반영했다.
이 방식의 핵심은 “변경된 데이터만 바꾼다”는 점이다.
전체 리스트를 다시 불러오지 않기 때문에, 요청 자체를 없앨 수 있다.
즉, 네트워크 비용을 줄이면서도 사용자에게는 즉시 반영되는 것처럼 보이게 할 수 있다.
구조적으로도 장점이 있었다.
캐시를 직접 갱신하는 부분과 재검증이 필요한 부분을 분리하니, 각 변경 유형의 책임이 선명해졌다.
이제 개발자는 모든 이벤트를 같은 방식으로 처리하지 않아도 됐다.
실제로 적용한 전략은 다음과 같다.
- 댓글 이벤트 수신
- 변경 유형 확인
- 안전하게 반영 가능한 경우
setQueryData - 서버 재계산이 필요한 경우에만
invalidateQueries
이렇게 하면 캐시 일관성과 요청 비용 사이의 균형을 맞출 수 있다.
5. 성능 개선 결과
측정은 댓글 캐시 비교 페이지에서 진행했다.
동일한 시나리오를 기준으로 invalidate/refetch 방식과 setQueryData 방식을 비교했다.
| 항목 | invalidate/refetch | setQueryData |
|---|---|---|
| 댓글 이벤트 10회 기준 API 요청 수 | 10회 | 0회 |
| 총 소요 시간 | 21102ms | 1235ms |
| 단축률 | - | 약 94.2% |
추가로 실시간 이벤트 반영 측정에서는:
- 평균 latency:
3.57ms - p95 latency:
7.2ms
이 결과는 단순 체감이 아니라 실제 측정값이다.
가장 중요한 변화는 요청 수였다.
댓글 이벤트가 10번 발생해도 API를 다시 부르지 않게 되면서, 네트워크 비용이 사실상 사라졌다.
숫자로 보면 판단이 더 명확하다.
invalidateQueries는 안전했지만, 이 화면에서는 10회 요청과 21초가 넘는 비용을 만들었다.
반면 setQueryData는 변경분만 반영하면서 같은 시나리오를 1.2초대로 줄였다.
6. 배운 점
이 작업에서 가장 크게 배운 건, React Query의 핵심은 “데이터를 가져오는 도구”가 아니라 “상태를 어떻게 다룰지 결정하는 도구”라는 점이다.
invalidateQueries는 안전하다.setQueryData는 빠르다.- 둘 중 무엇이 맞는지는 데이터의 변경 범위가 결정한다.
즉, 캐시 전략은 기술 선택이 아니라 판단의 문제였다.
전체 재조회가 필요한지, 변경분만 반영해도 되는지 먼저 구분해야 했다.