TL;DR
React Query 쿼리 키에 tenantId(campusId) 안 넣어서 캐시가 구분이 안 됐다. 넣으니까 바로 해결.
드롭다운은 바뀌는데 데이터는 안 바뀌는 문제
학원 관리 시스템 만들면서 겪은 일이다. 우측 상단에 캠퍼스 선택 드롭다운이 있고, 선택하면 그 캠퍼스 데이터만 보여야 한다.
사용자: 광주캠퍼스 → 서울캠퍼스 (드롭다운 클릭)
화면: 여전히 광주캠퍼스 데이터 표시 😱
사용자: F5 (새로고침)
화면: 이제야 서울캠퍼스 데이터 표시 ✅
React Query 쓰고 있는데 왜 이럴까? 처음엔 “서버가 잘못 보내주나?” 싶었는데, 네트워크 탭을 보니 API 호출 자체가 안 일어나고 있었다.
원인: React Query는 쿼리 키가 신이다
React Query는 쿼리 키가 같으면 캐시된 데이터를 쓴다. 당연한 얘기지만 놓치기 쉽다.
문제가 된 코드
// ❌ 캠퍼스가 바뀌어도 쿼리 키가 그대로
export const useTrashTreeStructure = (type?: string) => {
return useQuery({
queryKey: ['trash', 'tree', { type }], // campusId가 없음!
queryFn: () => trashApi.getTrashTreeStructure(type),
});
};
캠퍼스를 1번에서 2번으로 바꿔도 쿼리 키는 ['trash', 'tree', { type }]로 똑같다. React Query 입장에서는 “같은 데이터 요청이네? 캐시에 있는 거 줄게!” 하는 거다.
서버는 쿠키로 구분하는데…
백엔드는 쿠키로 캠퍼스를 구분하고 있었다:
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if ("selectedCampusId".equals(cookie.getName())) {
campusId = cookie.getValue();
}
}
쿠키는 자동으로 가니까 서버는 문제없다. 하지만 프론트엔드 캐시가 문제였다.
해결: 쿼리 키에 campusId 넣기
간단하다. 쿼리 키에 campusId를 넣으면 된다.
Step 1: 쿼리 키에 campusId 추가
// ✅ campusId가 바뀌면 쿼리 키도 바뀜
import { getCampusCookie } from '@/utils/cookies';
export const useTrashTreeStructure = (type?: string) => {
const campusId = getCampusCookie();
return useQuery({
queryKey: ['trash', 'tree', { type, campusId }], // campusId 추가!
queryFn: () => trashApi.getTrashTreeStructure(type),
});
};
이제 캠퍼스를 바꾸면:
- 쿠키 업데이트:
setCampusCookie(2) - 컴포넌트 리렌더
- 쿼리 키 변경:
{ campusId: 1 }→{ campusId: 2 } - React Query: “새 데이터 fetch!”
Step 2: 명시적으로 헤더 보내기 (선택사항)
쿠키만 쓰면 디버깅할 때 불편하다. 네트워크 탭에서 뭘 보냈는지 안 보인다. 그래서 헤더도 같이 보내기로 했다.
// axios interceptor
axiosInstance.interceptors.request.use(
async (config) => {
const campusId = getCampusCookie();
if (campusId !== null) {
config.headers['X-Campus-Id'] = campusId.toString();
}
return config;
}
);
서버도 헤더를 우선적으로 읽도록 수정:
// 1순위: 헤더
String campusIdParam = request.getHeader("X-Campus-Id");
// 2순위: 헤더 없으면 쿠키 (하위 호환성)
if (campusIdParam == null) {
// 쿠키에서 읽기
}
이제 네트워크 탭에서 X-Campus-Id: 2 헤더가 보인다. 디버깅이 훨씬 쉬워졌다.
모든 훅에 적용
휴지통만 고치면 안 된다. 캠퍼스별로 데이터가 다른 모든 API에 적용해야 한다.
// 학생 상세
export const useStudentDetail = (studentId: number) => {
const campusId = getCampusCookie();
return useQuery({
queryKey: ['students', studentId, { campusId }], // 여기도
queryFn: () => studentApi.getStudentDetail(studentId),
});
};
// 과제 템플릿 목록
export const useTaskTemplates = () => {
const campusId = getCampusCookie();
return useQuery({
queryKey: ['task-templates', { campusId }], // 여기도
queryFn: () => taskApi.getTaskTemplates(),
});
};
패턴이 보이나? 모든 쿼리 키에 campusId를 넣는다.
전체 흐름 정리
[캠퍼스 드롭다운 변경]
↓
setCampusCookie(2) // 쿠키 업데이트
↓
컴포넌트 리렌더
↓
getCampusCookie() → 2 // 새 값
↓
쿼리 키: { campusId: 1 } → { campusId: 2 }
↓
React Query: "새 쿼리네? fetch!"
↓
axios: X-Campus-Id: 2 헤더 추가
↓
서버: 캠퍼스 2 데이터만 반환
↓
화면 즉시 업데이트 ✨
결과
Before
- 캠퍼스 변경 → F5 필요
- 사용자: “버그인가요?”
- 개발자: “새로고침하세요…”
After
- 캠퍼스 변경 → 즉시 반영
- 캠퍼스별 캐시 분리 (성능도 좋아짐)
- 네트워크 탭에서 헤더로 확인 가능
배운 점
1. React Query 쿼리 키는 데이터의 모든 의존성을 담아야 한다
// ❌ Bad
queryKey: ['students'] // 어느 캠퍼스? 어떤 필터?
// ✅ Good
queryKey: ['students', { campusId, grade, status }] // 명확
2. 멀티테넌트는 처음부터 설계하자
나중에 추가하려면 모든 훅을 다 고쳐야 한다. 처음부터:
- 쿼리 키에 tenantId 포함
- axios interceptor로 헤더 자동 추가
- 서버에서 자동 필터링
3. 쿠키 vs 헤더
| 쿠키 | 헤더 |
|---|---|
| 자동 전송 | 명시적 |
| 디버깅 어려움 | 네트워크 탭에서 보임 |
| CORS 복잡 | CORS 설정 필요 |
둘 다 쓰면 좋다. 헤더 우선, 쿠키 백업.
CORS 주의사항
커스텀 헤더 쓸 거면 CORS 설정 필수:
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Campus-Id" // 이거 빼먹으면 preflight에서 막힘
));
다음 개선 아이디어
지금은 매번 쿠키를 읽는데, Context로 관리하면 더 깔끔할 듯:
// 미래
const { currentCampusId } = useCampus(); // Context
queryKey: ['students', currentCampusId]
핵심: React Query로 멀티테넌트 앱 만들 때는 쿼리 키에 tenantId를 꼭 넣자. 안 그러면 나처럼 “왜 안 바뀌지?” 하면서 한참 헤맨다. 🤦♂️
Comments