- 발행일
서버에서만 안 되는 로딩 — 플래그를 하나 더 만들었고, 사흘 뒤 Promise.allSettled가 진짜 답이었다
서버에서만 안 되는 로딩
이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.
찜 화면은 두 단계로 데이터를 받습니다. 먼저 우리 서버에서 찜 목록(googleMapId와 날짜)을 받고, 그다음 Google Places getDetails를 병원 수만큼 불러 이름·주소·거리를 채웁니다. 3월 6일 밤부터 7일 새벽까지 이 화면 커밋이 다섯 개인데, 그중 둘의 제목이 이렇습니다.
5885c16 03-07 00:10 병원 상세 정보 로딩 및 구글 로딩 추가
5d7329b 03-07 00:26 서버에서만 안되는 로딩 이슈 수정
16분 간격이에요. 로컬에서는 목록이 떴고, Vercel에 올리면 스피너가 돌다가 "찜한 병원이 없네요!"로 떨어졌습니다. 어떤 기기·회선에서 봤는지는 커밋에 없습니다.
00:10 — 플래그 하나로 두 단계를 덮으려 했다
첫 커밋이 isHospitalDetailsLoaded를 만들었습니다.
if (hospitalFirstData.length > 0 && !isLoading) {
loadGoogleMapsScript(() => {
loadPlaceDetails();
setIsHospitalDetailsLoaded(true);
});
}
}, [hospitalFirstData, isLoading]);
const isStillLoading = isLoading || !isHospitalDetailsLoaded;
loadPlaceDetails()를 부르고 바로 다음 줄에서 상세가 로드됐다고 표시합니다. 그런데 loadPlaceDetails는 안에서 navigator.geolocation.getCurrentPosition을 부르고, 그 콜백 안에서 병원마다 service.getDetails를 부르고, 또 그 콜백에서 setHospitalInfo에 하나씩 붙이는 함수예요. 돌아오는 게 아무것도 없습니다. 그러니 "상세 로드 완료" 플래그는 상세가 시작되는 순간 true가 됐습니다.
로컬에서는 Places 응답이 빨라서 스피너가 꺼지는 순간과 목록이 채워지는 순간이 거의 겹쳤을 겁니다. 배포 환경에서는 그 사이가 벌어져 빈 화면이 먼저 보였던 걸로 읽혀요. 다만 이건 지금 코드를 보고 하는 추정이고, 그날 그렇게 진단한 기록은 없습니다.
00:26 — 플래그를 셋으로
16분 뒤 커밋은 플래그를 하나 더 만들었습니다.
const [isHospitalInfoLoaded, setIsHospitalInfoLoaded] = useState(false); // 병원 기본 정보 로딩 상태
...
setHospitalFirstData(response?.data?.data.bookmarkList);
setIsHospitalInfoLoaded(true); // 병원 기본 정보 로딩 완료
...
const isStillLoading = isLoading || !isHospitalInfoLoaded || !isHospitalDetailsLoaded;
isLoading(서버 요청 중), isHospitalInfoLoaded(서버 목록 받음), isHospitalDetailsLoaded(상세 받음). 세 불리언의 AND로 스피너를 판정합니다. 그런데 셋째 플래그가 여전히 상세 시작 직후에 켜지는 건 그대로였어요. 첫 번째 단계를 더 정확히 표시하게 됐을 뿐, 두 번째 단계가 끝났는지는 여전히 아무도 몰랐습니다.
여기서 "이슈 수정"이라고 적고 커밋했습니다. 로컬에서 확인했겠지만 로컬은 원래 됐던 곳이에요.
이튿날 — 판정에서 빼고 의존성으로
3월 8일 저녁 커밋(a36802e, 제목 "찜한 병원 로딩 테스트")이 방향을 바꿨습니다.
- }, [hospitalFirstData, isLoading]);
-
- const isStillLoading = isLoading || !isHospitalInfoLoaded || !isHospitalDetailsLoaded;
+ }, [hospitalFirstData, isHospitalInfoLoaded]);
+
+ const isStillLoading = isLoading || !isHospitalDetailsLoaded;
새로 만든 플래그를 스피너 판정에서 빼고, 상세를 시작하는 useEffect의 의존성으로 옮겼습니다. "서버 목록을 받은 뒤에 상세를 시작한다"는 순서는 이걸로 잡혔어요. 지금 보면 isLoading은 finally에서 꺼지는 값이라 의존성에 두면 요청이 끝나기 전 렌더에도 걸릴 수 있었고, 목록을 실제로 받았다는 플래그가 더 정확한 신호였습니다. 그날 그렇게 적은 기록은 없습니다. 판정 조건은 다시 둘로 돌아갔습니다.
이것도 완전한 답은 아니었습니다. 상세가 끝났는지는 여전히 몰랐으니까요.
사흘 뒤 — 콜백을 모아서 끝을 만들었다
3월 10일 커밋(92bbaee, 제목 "찜하기 목록 업데이트 시도")에서 getDetails 콜백들을 Promise로 감싸고 Promise.allSettled로 모았습니다. 13일에 훅으로 뺀 판(0e89494)은 이렇습니다.
Promise.allSettled(detailsPromises).then((results) => {
const successfulDetails = results
.filter((result): result is PromiseFulfilledResult<HospitalDetail | null> => result.status === 'fulfilled')
.map((result) => result.value)
.filter((detail): detail is HospitalDetail => detail !== null);
setHospitalInfo(successfulDetails);
setIsHospitalDetailsLoaded(true);
})
setIsHospitalDetailsLoaded(true)가 then 안으로 들어갔습니다. 이제야 "상세 로드 완료"가 상세가 끝난 뒤에 켜져요. 7일 새벽의 문제는 플래그가 모자라서가 아니라 끝을 알 수 없는 함수에 끝났다는 표시를 달아 둔 것이었고, 답은 끝을 만들어 주는 것이었습니다. 3일이 걸렸어요.
검증
그날 확인한 것:
- 로컬에서 목록이 뜨는 것. 배포에서 다시 봤는지는 커밋 제목 "로딩 테스트" 외에 기록이 없습니다.
이 글을 쓰며 확인한 것:
- 커밋 시각(00:10, 00:26, 이튿날 21:00)은
git log --format='%ci'로 확인했습니다. setIsHospitalDetailsLoaded(true)의 위치가loadPlaceDetails()바로 다음 줄에서allSettled().then안으로 옮겨진 첫 커밋은git log -S'allSettled'로 찾았습니다. 3월 10일92bbaee입니다.- "로컬은 빨라서 겹쳐 보였다"는 재현하지 않은 추정입니다.
남은 것 · 한계
- 왜 배포에서만 안 됐는지 그날 진단한 기록이 없습니다. 커밋 제목이 "서버에서만"이라고 말할 뿐이에요. 회선 속도인지, Places 응답 지연인지, 다른 원인인지 지금도 확정 못 합니다.
- 플래그 셋 구조는 훅으로 옮겨진 뒤에도 그대로 남았습니다. 3월 17일 찜 훅은 Recoil 아톰으로
isLoading·isHospitalInfoLoaded·isHospitalDetailsLoaded셋을 다 갖고 있고, 스피너 판정은 다시 세 개의 AND입니다. 끝을 만들어 준 뒤에는 플래그가 둘이면 됐을 텐데 줄이지 않았어요. - 초기값이 문제를 하나 더 만듭니다.
hospitalFirstData의 기본값이[{ googleMapId: '', bookmarkDate: '' }]라 길이가 1이고,length > 0조건이 서버 응답 전에도 참입니다.googleMapId가 빈 문자열이라 안쪽if에서 걸러지긴 하지만, 목록을 받기 전에 지도 스크립트를 한 번 더 부르는 경로예요. 이 글을 쓰며 봤고 그때는 몰랐습니다. - 3월 31일 회고에 이 화면은 "로딩 중 / 데이터 없음 상태에 따른 동적 화면 처리"라고 결과 한 줄로 적혀 있습니다. 이 글이 그 한 줄의 사흘입니다.
관련 글: 직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법 · 무한 스크롤에서는 왜 이슈가 생긴 걸까 — 상태의 주인이 둘일 때 · 배포 첫날 커밋 열 개