저는 지난 8월, 신한 해커톤 with SSAFY에서 3위를 수상한 '돈기부여' 프로젝트에 참여했습니다. 돈기부여는 신한은행 예치금을 걸고 금융 목표에 도전하는 SOL 인앱 챌린지 서비스로, 사용자의 이체 내역을 자동으로 추적하여 커피/술/배달비 줄이기 챌린지, 행운의 777 적금, 매일 금융 퀴즈 등 다양한 금융 챌린지를 제공하는 서비스였습니다.
프로젝트가 성공적으로 마무리되고 시연까지 완료했지만, 개발 과정에서 겪었던 한 가지 문제가 계속 머릿속을 맴돌았습니다. 바로 React Query의 query key 관리 문제였죠.
이번 포스팅에서는 규모가 있는 프로젝트에서 효율적으로 Query Key를 관리하기 위한 React Query팀의 추천 라이브러리에 대해 소개하고, 적용 과정에서 얻은 인사이트를 공유하고자 합니다.
프로젝트에서 마주한 현실
돈기부여 프로젝트는 5명의 팀원이 빠르게 개발해야 하는 해커톤 프로젝트였습니다. 프론트엔드 개발자 3명이 각자 맡은 기능을 구현하다 보니, React Query의 query key가 여기저기 흩어져 있었습니다. 아마 가벼운 마음으로 React Query를 도입한 팀에선 아래와 같은 코드 형태가 익숙하실 수 있습니다.
// A 개발자가 작성한 코드
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: getUserInfo,
})
// B 개발자가 작성한 코드
const { data: userInfo } = useQuery({
queryKey: ['userInfo'], // 같은 데이터인데 다른 키!
queryFn: getUserInfo,
})
// C 개발자가 작성한 코드
const { data: challengeList } = useQuery({
queryKey: ['challengeList'],
queryFn: getChallenges,
})
하지만 해커톤 시연 준비를 하면서 이상한 현상을 발견했습니다. 분명 데이터를 업데이트했는데 화면에 반영되지 않는 버그였습니다. 디버깅해보니 같은 데이터를 서로 다른 query key로 관리하고 있어서 발생한 문제였고, 규모가 크지 않아 문제 파악 후 Query Key를 제대로 변경하여 문제를 해결할 수 있었습니다. 그렇게 시연을 성공적으로 마치고 집으로 귀가하는 길에 문득 이러한 생각이 머릿속에 스쳤습니다.
프론트 엔드 인원이 3명인 팀에서도 이렇게 쿼리 키에 대해서 문제가 발생하는데,
과연 현업에선 어떻게 이 문제를 해결하고 있을까?
비록 시연이 끝난 프로젝트지만, 내가 처음부터 다시 프로젝트를 설계하면 어떻게 쿼리 키를 설계했을지에 대해 고민해봤습니다.
구체적인 문제점들
프로젝트를 진행하면서 발견한 돈기부여 팀의 query key 관리의 문제점을 정리하면 다음과 같았습니다:
- 중복된 키 네이밍: ['user'], ['userInfo'], ['userProfile'] 등 같은 데이터를 가리키는 다른 키들
- 일관성 없는 구조: 어떤 곳은 ['challenge', id], 다른 곳은 ['challengeDetail', id]
- 타입 안전성 부재: 오타나 잘못된 파라미터 전달을 컴파일 타임에 잡을 수 없음
- 캐시 무효화의 어려움: 관련된 모든 키를 수동으로 찾아서 무효화해야 함
해결 과정
첫 번째 시도: 상수로 관리하기
처음에는 단순하게 상수로 관리하려고 했습니다. 아래와 같은 형태가 대표적인 예시입니다.
const QUERY_KEYS = {
USER: 'user',
CHALLENGE_LIST: 'challengeList',
CHALLENGE_DETAIL: (id: string) => ['challenge', id],
}
하지만 이 방법도 여전히 타입 안전성이 부족했고, 계층적 구조를 표현하기 어려웠습니다.
최종 해결책: Query Key Factory 도입
결국 더 좋은 해결 방법에 대해 고민하게 되었고, 마침 Tanstack Query 팀에서 Query Key 관리 시 추천하는 @lukemorales/query-key-factory 라이브러리를 도입하기로 결정했습니다.
@lukemorales/query-key-factory 라이브러리에서 추천하는 키 관리 방식 중에 중앙관리형 방식이 돈기부여 프로젝트의 규모에 적합하다고 판단하였고, 다음과 같이 쿼리 키를 중앙집중화 하여 작성했습니다.
// src/lib/query-keys.ts
import { createQueryKeyStore } from '@lukemorales/query-key-factory'
export const queries = createQueryKeyStore({
// 사용자 관련 쿼리
user: {
all: null,
info: null,
profile: null,
},
// 챌린지 관련 쿼리
challenge: {
all: null,
list: null,
detail: (challengeId: string) => [challengeId],
my: null,
status: null,
result: (challengeId: string) => [challengeId],
ranking: (challengeId: string) => [challengeId],
},
// 계좌 관련 쿼리
account: {
challenge: null,
savingsSeven: null, // 777 적금 전용
all: null,
no: null,
},
// 금융 데이터 쿼리
finance: {
spentMoney: (type?: string) => (type ? [type] : [type || 'default']),
estimateReward: null,
totalConsumption: null,
},
})
이를 반영하여 기존 코드를 수정한 형태는 아래와 같았습니다.
Before:
// 챌린지 목록 조회
const { data: challenges } = useQuery({
queryKey: ['challengeList'],
queryFn: getChallengeList,
})
// 챌린지 상세 조회
const { data: challenge } = useQuery({
queryKey: ['challenge', challengeId],
queryFn: () => getChallengeDetail(challengeId),
})
// 캐시 무효화
queryClient.invalidateQueries({ queryKey: ['challengeList'] })
queryClient.invalidateQueries({ queryKey: ['challenge', challengeId] })
After:
import { queries } from '@/lib/query-keys'
// 챌린지 목록 조회
const { data: challenges } = useQuery({
...queries.challenge.list,
queryFn: getChallengeList,
})
// 챌린지 상세 조회
const { data: challenge } = useQuery({
...queries.challenge.detail(challengeId),
queryFn: () => getChallengeDetail(challengeId),
})
// 전체 챌린지 관련 캐시 무효화 - 한 번에!
queryClient.invalidateQueries({ queryKey: queries.challenge._def })
특히 TypeScript와의 완벽한 통합을 위해 타입을 export하여 다른 곳에서도 활용할 수 있었습니다.
export type QueryKeys = typeof queries
// 다른 파일에서 사용
import { QueryKeys } from '@/lib/query-keys'
const invalidateChallengeQueries = (queryKeys: QueryKeys) => {
queryClient.invalidateQueries({
queryKey: queryKeys.challenge._def
})
}
마치며
지금까지 React Query를 적용한 프로젝트를 리팩토링하며 Query Key를 효율적으로 관리하는 방법에 대해 공유했습니다.
돈기부여 프로젝트에서 겪었던 query key 관리 문제는 작은 문제처럼 보였지만, 프로젝트가 커질수록 큰 기술 부채가 될 수 있었습니다. 이를 Query Key Factory를 도입함으로써 타입 안전성을 확보하고, 유지보수성을 크게 향상시킬 수 있었습니다.
특히 해커톤처럼 빠르게 개발해야 하는 환경에서도, 초기에 조금만 시간을 투자해서 이런 구조를 잡아놓으면 나중에 큰 시간을 절약할 수 있다는 것을 배웠습니다. 작은 규모의 프로젝트에선 불필요하다고 판단할 수 있지만, 빠르게 개발하는 과정에서 예상치 못하게 발생했던 문제들을 미연에 방지할 수 있다는 점에서 소규모의 프로젝트에서도 유용하다고 생각이 듭니다.
여러분도 React Query를 사용하시면서 query key 관리에 어려움을 겪고 계신가요? Tanstack Query 팀이 추천하는 @lukemorales/query-key-factory 라이브러리를 도입 문제를 해결해보시는건 어떨까요 ? 이 글이 Query Key 관리에 대해 고민하는 프론트 엔드 개발자 분들에게 조금이나마 도움이 되었으면 좋겠습니다. 비슷한 경험이 있으시다면 댓글로 공유해주세요!
읽어주셔서 감사합니다.
참고
https://tanstack.com/query/v4/docs/framework/react/community/lukemorales-query-key-factory
Query Key Factory | TanStack Query React Docs
Typesafe query key management with auto-completion features. Focus on writing and invalidating queries without the hassle of remembering how you've set up a key for a specific query! Installation You...
tanstack.com
https://github.com/lukemorales/query-key-factory
GitHub - lukemorales/query-key-factory: A library for creating typesafe standardized query keys, useful for cache management in
A library for creating typesafe standardized query keys, useful for cache management in @tanstack/query - lukemorales/query-key-factory
github.com
'Troble Shooting' 카테고리의 다른 글
Next.js 15에서 TypeScript 파일과 next-sitemap 통합하기 (0) | 2025.06.01 |
---|---|
Next.js 애플리케이션에서 이미지 프록시 API로 외부 도메인 제한 문제 해결하기 (1) | 2025.06.01 |
Next.js 15 성능 최적화 실전기 - Chrome DevTools로 찾아낸 숨겨진 병목들 (0) | 2025.05.31 |