OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 23 / 50
master-project
CHAPTER 23 / 50
읽기 약 2
FUNCTION

에러/로딩/빈 상태 UI


핵심 개념

Skeleton·Spinner·ErrorBoundary·404·500·Empty State — 사용자가 마주치는 모든 비주얼 상태 표준화.

본문

Loading — Skeleton

TSX📋 코드 (11줄)
// 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

TSX📋 코드 (3줄)
import { Loader2 } from 'lucide-react'

<Loader2 className="h-4 w-4 animate-spin" />

Next.js loading.tsx

TSX📋 코드 (13줄)
// 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

TSX📋 코드 (22줄)
// 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)

TSX📋 코드 (17줄)
// 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)

TSX📋 코드 (20줄)
// 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 포함)

TSX📋 코드 (32줄)
// 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

TSX📋 코드 (20줄)
'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

TSX📋 코드 (3줄)
{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