로딩/에러 상태 처리 잘 하고계신가요?
저는 사실 그 부분을 자주 놓치는 편입니다.
실제 서비스를 운영해본 경험은 없고, 작은 사이즈의 프로젝트만 다뤄오다 보니 로딩/에러 처리를 대충 넘기는 버릇이 있었죠. 스켈레톤 같은 건 아예 없고, 로딩 처리라고 해봐야 Suspense를 최상위에 딱 하나 걸어둔 게 전부였습니다.
그래서 바로 오늘 !! 버그를 마주했지요 ..ㅎ
버그 사례
상세 페이지에서 userProfile 정보를 불러온 뒤, 유저의 역할(role)에 따라 다른 화면을 보여줘야 하는 로직이 있었습니다. 그런데 userProfile을 다 가져오기 전에 API를 호출해버려서, userProfile이 null인 상태로 로직이 실행되어 버린 거죠. 그 결과, 유저 role을 제대로 구분하지 못해 화면이 랜덤하게 보이는 문제가 생겼습니다.
해결은 loading 상태일 때 Skeleton UI를 보여주는 방식으로 아주 간단하게 하긴 했는데요.
로딩/에러 상태 처리를 자꾸만 빼먹는다면 앞으로도 이런 랜덤 버그를 마주할 가능성이 높기 때문에.. GPT에게 조언을 구했습니다.
제 친구 GPT가 무려 10초 동안이나 생각하고 좋은 조언을 해주었는데요. 그 내용을 공유하고 싶어 블로그를 작성합니다 !!
(+ 소중한 Exception Collector 라는 애칭도 얻었네요 ^^;)
모든 페이지/섹션 컴포넌트 맨 위에 붙이는 의식:
const { data, isLoading, isError, error } = useQuery(...);
if (isLoading) return <SkeletonSection/>;
if (isError) return <ErrorSection message={getMsg(error)} onRetry={() => refetch()} />;
// 여기서부터만 정상 렌더
무조건 early return. “로딩/에러를 아래에서 조건부로 섞어 렌더”는 사고난다.
쿼리 훅에서부터 “undefined 데이터”를 못 퍼뜨리게 해.
type QueryResult<T> =
| { state: "loading" }
| { state: "error"; error: unknown; retry: () => void }
| { state: "success"; data: T };
export function useSafeQuestion(id: string): QueryResult<Question> {
const q = useQuery({ queryKey: ["q", id], queryFn: fetchQ, placeholderData: null });
if (q.isLoading) return { state: "loading" };
if (q.isError) return { state: "error", error: q.error, retry: () => q.refetch() };
return { state: "success", data: q.data! };
}
소비측은 스위치 문으로 강제 분기:
const q = useSafeQuestion(id);
switch (q.state) {
case "loading": return <Skeleton/>;
case "error": return <ErrorView onRetry={q.retry}/>;
case "success": return <View data={q.data}/>;
}
타입이 분기를 강제하니 “빼먹기”가 컴파일 에러로 승화됨. 아름답지.
반복 줄이기용 고차 컴포넌트.
function QueryGuard<T>({ query, children }:{
query: { isLoading: boolean; isError: boolean; error?: any; refetch: () => any };
children: React.ReactNode;
}) {
if (query.isLoading) return <SkeletonSection/>;
if (query.isError) return <ErrorSection message={String(query.error)} onRetry={() => query.refetch()}/>;
return <>{children}</>;
}
// 사용
const q = useQuery(...);
return <QueryGuard query={q}><RealContent data={q.data}/></QueryGuard>;
const { status } = useAuth();
if (status === "loading") return <FullPageSpinner/>;
if (status === "unauthenticated") return <LoginCTA/>;
“인증 가드”와 “데이터 가드”를 섞지 말고 두 단계로 나눠.
패턴(early return) + 타입(상태 머신) + 도구(ESLint/스니펫) + 테스트(MSW).
이렇게 깔아두면 네가 아무리 바빠도 로딩/에러는 “빼먹고 싶어도” 못 빼먹는다. 그리고 네가 덜 욕먹고, 나는 덜 잔소리한다. Win–Win.
이상 아주 아주 유용한 에러/로딩 상태 처리 꿀팁이었습니다 !!
이걸 참고해서 다른 파트도 리팩토링해봐야겠네요 🔥!