로딩/에러 상태 처리 빼먹지 않는 법

nnhw·2025년 9월 10일
0

TIL

목록 보기
11/11
post-thumbnail

로딩/에러 상태 처리 잘 하고계신가요?

저는 사실 그 부분을 자주 놓치는 편입니다.
실제 서비스를 운영해본 경험은 없고, 작은 사이즈의 프로젝트만 다뤄오다 보니 로딩/에러 처리를 대충 넘기는 버릇이 있었죠. 스켈레톤 같은 건 아예 없고, 로딩 처리라고 해봐야 Suspense를 최상위에 딱 하나 걸어둔 게 전부였습니다.

그래서 바로 오늘 !! 버그를 마주했지요 ..ㅎ

버그 사례
상세 페이지에서 userProfile 정보를 불러온 뒤, 유저의 역할(role)에 따라 다른 화면을 보여줘야 하는 로직이 있었습니다. 그런데 userProfile을 다 가져오기 전에 API를 호출해버려서, userProfile이 null인 상태로 로직이 실행되어 버린 거죠. 그 결과, 유저 role을 제대로 구분하지 못해 화면이 랜덤하게 보이는 문제가 생겼습니다.

해결은 loading 상태일 때 Skeleton UI를 보여주는 방식으로 아주 간단하게 하긴 했는데요.
로딩/에러 상태 처리를 자꾸만 빼먹는다면 앞으로도 이런 랜덤 버그를 마주할 가능성이 높기 때문에.. GPT에게 조언을 구했습니다.

제 친구 GPT가 무려 10초 동안이나 생각하고 좋은 조언을 해주었는데요. 그 내용을 공유하고 싶어 블로그를 작성합니다 !!
(+ 소중한 Exception Collector 라는 애칭도 얻었네요 ^^;)


1) 초기 3줄 가드 패턴(암기용)

모든 페이지/섹션 컴포넌트 맨 위에 붙이는 의식:

const { data, isLoading, isError, error } = useQuery(...);

if (isLoading) return <SkeletonSection/>;
if (isError)  return <ErrorSection message={getMsg(error)} onRetry={() => refetch()} />;
// 여기서부터만 정상 렌더

무조건 early return. “로딩/에러를 아래에서 조건부로 섞어 렌더”는 사고난다.

2) 훅 래퍼로 강제하기

쿼리 훅에서부터 “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}/>;
}

타입이 분기를 강제하니 “빼먹기”가 컴파일 에러로 승화됨. 아름답지.

3)가드 컴포넌트 만들기

반복 줄이기용 고차 컴포넌트.

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>;

4) Auth도 같은 공식

const { status } = useAuth();
if (status === "loading") return <FullPageSpinner/>;
if (status === "unauthenticated") return <LoginCTA/>;

“인증 가드”와 “데이터 가드”를 섞지 말고 두 단계로 나눠.

5) 도구로 못 빼먹게 만들기

  • ESLint 룰: eslint-plugin-sonarjs 켜고, 커스텀 룰로 useQuery 사용 시 isLoading/isError 참조 없으면 경고. (최소한 PR 템플릿에 체크리스트 넣어)
  • 에러 바운더리 + Suspense: 서버/클라 모두 Suspense + ErrorBoundary 써서 폴백을 중앙집중화. (React Query의 suspense: true 옵션도 고려)
  • 에러 컴포넌트 통일: ErrorSection 하나 만들어서 재사용(메시지, retry, 이슈 링크 버튼 포함).
  • 스니펫: VSCode rq3 같은 사용자 스니펫: loading/error/success 3분기 틀 자동삽입.

6) UX 디테일(진짜로 체감 좋아짐)

  • 300ms 이하 로딩은 스피너 대신 레이아웃 스켈레톤 유지(깜빡임 방지).
  • 에러는 재시도(Refetch) + 이슈 트래킹 버튼(Sentry 링크) 같이 제공.
  • 리스트는 빈 상태(Empty State) 따로: 에러 아님. “데이터 0건” 메시지와 CTA.

7) 테스트로 습관 못 빼먹게

  • MSW로 /api/... 지연/실패 시나리오 넣고, 로딩/에러 뷰가 뜨는지 컴포넌트 테스트 작성.
  • 최소 2케이스: 200ms 지연(스켈레톤 확인), 500 에러(에러뷰+재시도 동작 확인).

요약:

패턴(early return) + 타입(상태 머신) + 도구(ESLint/스니펫) + 테스트(MSW).

이렇게 깔아두면 네가 아무리 바빠도 로딩/에러는 “빼먹고 싶어도” 못 빼먹는다. 그리고 네가 덜 욕먹고, 나는 덜 잔소리한다. Win–Win.


이상 아주 아주 유용한 에러/로딩 상태 처리 꿀팁이었습니다 !!
이걸 참고해서 다른 파트도 리팩토링해봐야겠네요 🔥!

profile
웹 프론트엔드 취준생 🥔

0개의 댓글