- 발행일
리액트 쿼리 적용이라고 다섯 번 적었는데 찜 화면은 끝내 Recoil과 useEffect였다 — 한 달 뒤 내가 쓴 회고와 어긋나는 코드
리액트 쿼리 적용이라고 다섯 번 적었는데 찜 화면은 끝내 Recoil과 useEffect였다
이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.
직짱건강은 스위그 3기에서 만든 건강 설문·병원 추천 서비스입니다. 프런트 둘 중 제가 온보딩·회원·마이페이지·설문·찜 화면을 맡았고, 팀원이 지도·검색·캘린더를 맡았어요. 2024년 3월 15일부터 17일 새벽까지 제 커밋 제목은 전부 이렇게 시작합니다.
af6610a 03-15 17:45 리액트 쿼리 적용 (1차)
b4ab9e6 03-15 23:44 리액트 쿼리 적용 (2차) : 프로필 사진 부분 까지 추가 적용
22a2d65 03-16 00:16 리액트 쿼리 적용 (3차) : 기본 작동 확인 <accessToken ~ error>
5d321d3 03-16 19:15 리액트 쿼리 작동 : 찜화면 제외 QA 1차 테스트
5c914f8 03-17 02:13 리액트 쿼리 작동 : alert 처리 일부 변경
넷째 커밋 제목에 이미 답이 있습니다. "찜화면 제외." 그리고 다섯째 커밋에서도 찜 화면은 제외된 채로 끝났어요.
무엇이 리액트 쿼리로 갔나
1차 커밋(af6610a, 30개 파일 +327/−330)이 src/hooks/react-query/ 폴더에 훅을 만들었습니다. 17일 새벽 기준으로 제 훅 다섯 파일에 든 호출을 세면 이렇습니다.
| 훅 | useMutation | useQuery |
|---|---|---|
useAuth.ts (로그인·로그아웃·가입·이메일 인증) | 5 | 0 |
useChangePassword.ts | 2 | 0 |
useProfileImage.ts | 2 | 0 |
useFind.ts (아이디·비밀번호 찾기) | 2 | 0 |
useSurvey.ts | 1 | 1 |
useLikeHospitalInfo.ts (찜) | 0 | 0 |
쓰기 요청은 전부 useMutation으로 갔습니다. 서버에서 목록을 읽어 와 화면에 그리는 건 찜 화면 하나였는데, 그 하나가 useQuery를 안 씁니다. 같은 폴더 이름이 react-query인데 그 안에 리액트 쿼리를 안 쓰는 훅이 하나 들어 있는 거예요.
찜 화면의 실제 모양
17일 새벽 useLikeHospitalInfo.ts는 이렇게 시작합니다.
const useHospitalInfo = () => {
const [hospitalFirstData, setHospitalFirstData] = useRecoilState<HospitalBookmark[]>(hospitalFirstDataState);
const [hospitalInfo, setHospitalInfo] = useRecoilState<HospitalDetail[]>(hospitalInfoState);
const [isLoading, setIsLoading] = useRecoilState<boolean>(isLoadingState);
const [isHospitalDetailsLoaded, setIsHospitalDetailsLoaded] = useRecoilState<boolean>(isHospitalDetailsLoadedState);
const [isHospitalInfoLoaded, setIsHospitalInfoLoaded] = useRecoilState<boolean>(isHospitalInfoLoadedState);
useEffect(() => {
const initializeHospitalInfo = async () => {
setIsLoading(true);
try {
const response = await fetchHospitalInfo();
Recoil 아톰 다섯 개. 서버에서 온 북마크 목록(hospitalFirstData), 그걸 Google Places로 채운 상세(hospitalInfo), 그리고 로딩 플래그 셋. 서버 응답을 useEffect 안에서 직접 fetch해서 아톰에 넣습니다. 리액트 쿼리가 대신 해 주는 일을 손으로 다 하고 있는 모양이에요.
시도는 있었습니다. 3월 13일 커밋(0e89494)에 fakeuseLikeHospitalInfo.ts라는 파일이 있어요.
// @ts-ignore
const { data:response, isLoading, isError, error } = useQuery(['hospitalInfo'], fetchHospitalInfo);
@ts-ignore가 붙은 이유는 지금 보면 분명합니다. useQuery(키배열, 함수) 꼴은 v4 문법이고, 이 프로젝트의 @tanstack/react-query는 5.24였어요. v5는 객체 하나만 받습니다. 타입이 막으니 무시하고 넘어간 건데, 그때 그걸 알았는지는 커밋에 없습니다. 파일 이름에 fake를 붙인 채 이틀 있다가 1차 커밋에서 지웠고, 찜 페이지는 처음부터 fake 아닌 쪽을 import하고 있었습니다.
왜 찜만 못 갔나
커밋에 이유는 없습니다. 코드로 읽히는 건 하나예요. 찜 목록은 서버 응답 한 번으로 끝나지 않습니다. 서버가 주는 건 googleMapId와 찜한 날짜뿐이고, 병원 이름·주소·거리는 브라우저에서 Google Places getDetails를 병원 수만큼 불러 콜백으로 받아야 해요. 그 콜백들을 Promise.allSettled로 모아 두 번째 아톰에 넣는 구조라, "서버 요청 하나 = 쿼리 하나"로 떨어지지 않았습니다. 마이페이지의 useMutation처럼 함수 하나를 넘기면 끝나는 자리가 아니었어요.
같은 목록을 팀원 화면은 리액트 쿼리로 읽고 있었습니다. hooks/react-query/index.ts의 useQuery_BookmarkList는 팀원이 3월 7일에 만든 훅이고, 검색 목록과 지도 상세가 찜 여부를 표시할 때 씁니다. 같은 /bookmark 응답이 팀원 화면에서는 리액트 쿼리 캐시에, 제 찜 화면에서는 Recoil 아톰에 따로 있었던 셈이에요.
같은 커밋에서 한 다른 일
17일 새벽 커밋은 alert()를 MUI Snackbar로 바꾸는 일도 같이 했습니다. src 안의 alert( 호출이 48곳에서 37곳으로 줄었어요. 찜·비밀번호 변경·프로필·설문 목록 네 화면입니다. 비밀번호 변경 화면에는 이런 코드가 들어갔습니다.
setOpenSnackbar(false);
setTimeout(() => {
setSnackbarMessage(response.message);
setSnackbarSeverity('error');
setOpenSnackbar(true);
}, 100);
닫고 100ms 뒤에 다시 여는 코드예요. 같은 메시지가 연달아 오면 스낵바가 안 다시 뜨는 걸 이렇게 우회했습니다. 왜 100인지는 커밋에 없습니다.
한 달 뒤 회고와 어긋난다
3월 31일에 쓴 직짱건강 회고2024-03-31직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법직장인 대상 건강 설문과 위치 기반 병원 추천을 묶은 헬스케어 서비스입니다. 6인 협업 프로젝트에서 프런트엔드로 참여했고, 백엔드 API가 없는 상태에서 JSON 목업으로 설문 플로우를 먼저 완성해 병렬 작업이 가능하게 만든 게 핵심 기여였어요. 2절은 "병원 북마크 — 클라이언트 상태와 서버 상태를 나눠 관리"입니다. 표까지 그려서 Recoil은 클라이언트 상태, TanStack Query는 "북마크 목록 조회·동기화, 캐싱"이라고 적었어요. 그리고 이렇게 썼습니다.
서버에서 온 데이터를 전역 스토어에 복사해두면 두 벌이 되고 그 순간부터 동기화 문제가 시작됩니다.
찜 화면이 정확히 그 모양이었습니다. 서버 응답을 Recoil 아톰에 복사해 두고, 팀원 화면의 리액트 쿼리 캐시와 두 벌로 갖고 있었어요. 회고는 원칙을 썼고, 코드는 그 원칙의 반대편에 있었습니다. 그 글은 그대로 두고, 이 글이 그 자리를 정정합니다.
검증
그날 확인한 것:
- 커밋 제목대로 "찜화면 제외 QA". 마이페이지·설문·비밀번호 흐름을 손으로 돌렸습니다. 기록은 제목 한 줄뿐이에요.
이 글을 쓰며 확인한 것:
- 제 훅 다섯 파일의
useMutation·useQuery호출 수는 17일 새벽 커밋(5c914f8)에서grep -c로 셌습니다. 찜 훅은 둘 다 0입니다. fakeuseLikeHospitalInfo.ts는0e89494에서 생겨af6610a에서 지워졌고, 찜 페이지의 import는 그 사이에도fake아닌 파일이었습니다.useQuery_BookmarkList를 쓰는 곳은 팀원의Search/Child/SearchList.tsx와Map/[id]/Child/InfoTitleView.tsx두 파일이고, 두 줄 다git blame이 팀원 3월 7일 커밋을 가리킵니다.alert(48→37은git grep으로 셌습니다. 2026년 9월 HEAD에서는 43입니다. 뒤에 다시 늘었어요.
남은 것 · 한계
- 찜 화면은 끝내 리액트 쿼리로 못 갔습니다. 3월 21일 커밋(
474fd62)은 찜 훅에 "처음 진입하면localStorage에 표시를 남기고 한 번router.reload()"하는 코드를 넣었고, 그 코드가 지금 HEAD에도 있습니다. 첫 렌더에서 목록이 안 그려지는 문제를 새로고침으로 덮은 건데, 왜 안 그려졌는지는 커밋에 없습니다. - 같은 데이터가 두 벌인 것을 그때는 몰랐습니다. 팀원이 리액트 쿼리로 같은 목록을 읽고 있다는 걸 이 글을 쓰며
grep으로 알았어요. 그때 알았다면 찜 화면도useQuery_BookmarkList를 쓰고 Places 상세만 따로 붙였을 텐데, 그 선택지를 본 기록이 없습니다. - 회고와 코드의 어긋남을 한 달 동안 몰랐습니다. 회고를 쓸 때 코드를 다시 열어 보지 않은 거예요. 이후 TripTune에서는 반대 방향으로 갔습니다. 서버 상태 라이브러리를 화면별로 걷어내는 쪽으로요. 그 경위는 일정 관리와 데이터 동기화 문제 해결하기2025-01-11일정 데이터 동기화 문제 해결하기 — 그리고 이 해결책은 나중에 뒤집혔다여행 루트 목록에서 과거 데이터 병합 오류·삭제 데이터 미반영이 반복돼 병합 로직을 재작성했던 기록입니다. 당시엔 해결이라고 썼는데, 이 방식은 결국 뒤집혔어요 — 병합 자체가 문제의 근원이라 react-query를 이 화면에서 걷어내는 것으로 끝났습니다. 중간 기록으로서의 가치가 있어 결말과 함께 남깁니다. (원래 2023-09로 잘못 적혀 있던 날짜를 커밋 기준 2025-01로 교정했습니다.)에 있고, 그 글도 나중에 뒤집혔습니다.
swr이package.json에 설치돼 있고 import는 0건입니다. 리액트 쿼리와 같이 깔아 두고 하나만 쓴 채 끝났어요.
관련 글: 직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법2024-03-31직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법직장인 대상 건강 설문과 위치 기반 병원 추천을 묶은 헬스케어 서비스입니다. 6인 협업 프로젝트에서 프런트엔드로 참여했고, 백엔드 API가 없는 상태에서 JSON 목업으로 설문 플로우를 먼저 완성해 병렬 작업이 가능하게 만든 게 핵심 기여였어요. · 무한 스크롤에서는 왜 이슈가 생긴 걸까 — 상태의 주인이 둘일 때2024-09-25무한 스크롤에서는 왜 이슈가 생긴 걸까 — 상태의 주인이 둘일 때무한 스크롤을 붙인 화면마다 이슈가 났는데, 돌아보니 라이브러리 문제가 아니라 전부 "누적된 목록의 주인이 누구인가"가 흐릿해서 생긴 문제였습니다. react-query 캐시와 zustand 스토어가 같은 목록을 두고 싸우다 한쪽을 걷어낸 이야기, 페이지 번호를 컴포넌트가 들고 있다가 캐시에 넘긴 이야기를 정리했습니다. · 일정에 담은 장소를 컴포넌트 state에서 스토어로 옮겼다2024-11-20일정에 담은 장소를 컴포넌트 state에서 스토어로 옮겼다 — userId를 nickname으로 바꾸던 날일정 만들기 화면에서 담은 장소 목록은 컴포넌트 안의 state였고, 서버 응답과 부모 props 두 곳에서 그 state를 덮어쓰고 있었습니다. 서버가 userId 대신 nickname을 주기로 한 날, 그 목록을 zustand 스토어로 옮겼어요. 상태의 주인을 하나로 만든 건 맞는데, 같은 커밋에서 드래그 항목 컴포넌트를 부모 함수 안으로 옮겼고 스토어 훅 이름을 이미 있는 것과 똑같이 지었습니다. 둘 다 지금까지 남아 있습니다. · 일정 관리와 데이터 동기화 문제 해결하기2025-01-11일정 데이터 동기화 문제 해결하기 — 그리고 이 해결책은 나중에 뒤집혔다여행 루트 목록에서 과거 데이터 병합 오류·삭제 데이터 미반영이 반복돼 병합 로직을 재작성했던 기록입니다. 당시엔 해결이라고 썼는데, 이 방식은 결국 뒤집혔어요 — 병합 자체가 문제의 근원이라 react-query를 이 화면에서 걷어내는 것으로 끝났습니다. 중간 기록으로서의 가치가 있어 결말과 함께 남깁니다. (원래 2023-09로 잘못 적혀 있던 날짜를 커밋 기준 2025-01로 교정했습니다.)