master-project
CHAPTER 23 / 50
읽기 약 2분
FUNCTION
에러/로딩/빈 상태 UI
핵심 개념
Skeleton·Spinner·ErrorBoundary·404·500·Empty State — 사용자가 마주치는 모든 비주얼 상태 표준화.
본문
Loading — Skeleton
// src/components/ui/skeleton.tsx
export function Skeleton({ className }: { className?: string }) {
return <div className={`bg-muted animate-pulse rounded ${className}`} />
}
// 사용
<div className="space-y-3">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>Loading — Spinner
import { Loader2 } from 'lucide-react'
<Loader2 className="h-4 w-4 animate-spin" />Next.js loading.tsx
// src/app/(app)/dashboard/loading.tsx
import { Skeleton } from '@/components/ui/skeleton'
export default function Loading() {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-32" />
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-24" />)}
</div>
</div>
)
}ErrorBoundary
// src/app/(app)/dashboard/error.tsx
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
useEffect(() => {
Sentry.captureException(error)
}, [error])
return (
<div className="text-center py-12">
<h2 className="text-xl font-bold mb-2">문제가 발생했어요</h2>
<p className="text-sm text-muted-foreground mb-4">
{process.env.NODE_ENV === 'development' ? error.message : '잠시 후 다시 시도해주세요'}
</p>
<button onClick={reset} className="bg-primary text-primary-foreground px-4 py-2 rounded">
다시 시도
</button>
</div>
)
}404 (not-found.tsx)
// src/app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-7xl font-bold text-muted-foreground">404</p>
<h1 className="text-2xl font-bold mt-4 mb-2">페이지를 찾을 수 없어요</h1>
<p className="text-sm text-muted-foreground mb-6">
주소를 확인하거나 홈으로 돌아가세요
</p>
<Link href="/" className="text-primary hover:underline">홈으로 →</Link>
</div>
</div>
)
}500 (global-error.tsx)
// src/app/global-error.tsx
'use client'
import * as Sentry from '@sentry/nextjs'
import { useEffect } from 'react'
export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
useEffect(() => { Sentry.captureException(error) }, [error])
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-2">서버 오류</h1>
<button onClick={reset} className="text-primary">새로고침</button>
</div>
</div>
</body>
</html>
)
}Empty State (CTA 포함)
// src/components/empty-state.tsx
import { Sparkles } from 'lucide-react'
import Link from 'next/link'
export function EmptyState({ icon: Icon = Sparkles, title, description, ctaLabel, ctaHref }: {
icon?: React.ComponentType<{ className?: string }>
title: string
description: string
ctaLabel: string
ctaHref: string
}) {
return (
<div className="text-center py-12 border-2 border-dashed rounded-lg">
<Icon className="h-12 w-12 mx-auto mb-3 text-muted-foreground" />
<h3 className="font-semibold mb-1">{title}</h3>
<p className="text-sm text-muted-foreground mb-4">{description}</p>
<Link href={ctaHref} className="bg-primary text-primary-foreground px-4 py-2 rounded inline-block">
{ctaLabel}
</Link>
</div>
)
}
// 사용
{generations.length === 0 && (
<EmptyState
title="아직 생성한 콘텐츠가 없어요"
description="첫 AI 카피를 만들어보세요"
ctaLabel="시작하기"
ctaHref="/generate"
/>
)}Network Error UI
'use client'
import { WifiOff } from 'lucide-react'
import { useEffect, useState } from 'react'
export function OfflineBanner() {
const [online, setOnline] = useState(true)
useEffect(() => {
setOnline(navigator.onLine)
window.addEventListener('online', () => setOnline(true))
window.addEventListener('offline', () => setOnline(false))
}, [])
if (online) return null
return (
<div className="fixed top-0 inset-x-0 bg-red-500 text-white py-2 text-sm text-center flex items-center justify-center gap-2 z-50">
<WifiOff className="h-4 w-4" /> 오프라인 — 일부 기능 제한
</div>
)
}Form Validation Error
{state?.errors?.email && (
<p className="text-xs text-red-500 mt-1">{state.errors.email}</p>
)}다음 챕터
CH.24 "접근성 + SEO 메타데이터".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 에러·로딩·빈 상태 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
에러·로딩·빈 상태 관련 모범 사례·안티패턴 5개를 비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 에러·로딩·빈 상태 최적화 가능 위치와 리스크를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 1인 개발자 시장의 에러·로딩·빈 상태 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
에러/로딩/빈 상태 UI는 이 3가지만 확실히 잡으세요
1.loading.tsx + error.tsx + not-found.tsx = Next.js 표준 상태
2.Skeleton + EmptyState + ErrorBoundary 3종 필수
3.다음 챕터에서 a11y + SEO
공유하기
진행도 23 / 50