어드민 전 페이지에 로딩·에러 상태 화면 추가
목차
어드민 UI 전반에 로딩/에러/빈 상태를 한 번에 정비했다.
파일 목록만 봐도 범위가 꽤 넓다. ad-units, audit-log, categories, dashboard — 어드민 주요 페이지 전체에 loading.tsx를 심었고, audit-log와 protected 레이아웃 루트에는 error.tsx까지 추가했다. 한 커밋에 여러 페이지를 동시에 건드린 건, 반대로 말하면 그 전까지 이 상태들이 아무것도 없었다는 뜻이다.
왜 지금 이걸 했나
기능 개발 초반에는 보통 데이터가 잘 오는 경우만 먼저 만들게 된다. "일단 돌아가게 하자"는 기조로 달리다 보면 loading / error 처리는 항상 후순위로 밀린다. 이번에도 그랬다. 어드민 기능 자체는 어느 정도 완성됐는데, 페이지 전환 시 화면이 그냥 빈 채로 있거나, API 오류가 나면 흰 화면만 덩그러니 남는 상황이 계속 돼서 팀 내에서도 "이건 좀 손봐야겠다"는 얘기가 나왔다.
어드민이라고 해서 UX를 막 해도 된다는 법은 없다. 오히려 운영자들이 매일 쓰는 툴이기 때문에 피드백 부재가 더 치명적이다. 로딩 중인지 에러인지 구분이 안 되면 "페이지가 죽었나?", "새로 고침 해야 하나?" 하는 혼선이 생기고, 결국 운영 문의로 돌아온다.
Next.js App Router의 파일 기반 상태 처리
이번 작업이 비교적 깔끔하게 정리된 건 Next.js App Router 덕분이기도 하다. loading.tsx / error.tsx 파일을 각 라우트 폴더에 두는 것만으로 React의 Suspense와 ErrorBoundary가 자동으로 붙는다. 별도 래퍼 컴포넌트를 만들 필요가 없다.
| 파일 | 역할 | 적용 범위 |
|---|---|---|
loading.tsx |
Suspense fallback — 페이지 데이터 페칭 중 표시 | 해당 라우트 |
error.tsx |
ErrorBoundary — 런타임 에러 캐치 및 UI 표시 | 해당 라우트 |
(protected)/error.tsx |
루트 레벨 에러 캐치 | protected 하위 전체 |
특히 (protected)/error.tsx를 레이아웃 루트에 둔 게 포인트다. 개별 페이지 error.tsx가 없는 경우에도 최소한 흰 화면은 막을 수 있다. 페이지별로 세분화하기 전에 안전망을 먼저 치는 순서가 맞다.
// error.tsx 기본 패턴
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 에러 로깅 연동 포인트
console.error(error);
}, [error]);
return (
<div>
<p>문제가 발생했습니다.</p>
<button onClick={reset}>다시 시도</button>
</div>
);
}
reset 함수를 제공하는 게 중요한데, 단순 에러 메시지만 보여주면 사용자는 새로 고침 외에 선택지가 없다. reset()을 호출하면 해당 세그먼트만 다시 렌더링 시도를 하기 때문에 훨씬 낫다.
로딩 스켈레톤 설계 시 챙긴 것들
loading.tsx에는 단순 스피너 대신 스켈레톤을 넣었다. 이유는 몇 가지다.
- 레이아웃 시프트 방지: 스피너는 데이터가 로드되면서 레이아웃이 확 바뀐다. 스켈레톤은 실제 콘텐츠와 비슷한 구조를 미리 잡아줘서 시각적 안정감이 있다.
- 체감 속도: 스켈레톤이 있으면 실제 로딩이 조금 더 걸려도 "뭔가 오고 있다"는 신호가 돼서 기다릴 수 있다.
- 페이지별 맞춤:
dashboard와audit-log는 테이블/카드 구조가 다르기 때문에 스켈레톤도 각 페이지 레이아웃에 맞게 따로 잡았다. 공용 스켈레톤 컴포넌트를 쓰되, 컬럼 수나 행 수를 props로 조절하는 방식이 유지보수하기 편하다.
// loading.tsx 예시 패턴
export default function Loading() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-12 animate-pulse rounded bg-muted" />
))}
</div>
);
}
회고
솔직히 이 작업은 "티가 안 나는" 작업이다. 기능 추가도 아니고, 버그 수정도 아니고. 근데 어드민을 쓰는 팀원들 입장에서는 실제로 체감되는 변화다. 이전까지는 페이지가 느리면 고장인지 로딩인지 알 수 없었는데, 이제는 스켈레톤이 뜨면 "기다리면 된다"는 게 명확해졌다.
팀 리드 입장에서 이런 작업의 우선순위를 언제 올리느냐가 항상 고민이다. 너무 일찍 하면 UI가 바뀔 때마다 스켈레톤도 같이 고쳐야 하는 오버헤드가 생기고, 너무 늦으면 그 사이에 운영 신뢰도가 깎인다. 이번엔 주요 페이지 구조가 어느 정도 안정된 시점에 한 번에 정리한 게 적절했다고 본다.
다음 스텝은 audit-log 같이 에러 경계가 생긴 페이지에서 실제 에러 발생 시 로깅 연동을 붙이는 것 정도가 남아 있다.
끝.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.