개요
본 글은 Next.js에서의 에러 핸들링, 로딩 UI, 그리고 스트리밍을 활용한 성능 최적화와 사용자 경험 향상 방법을 다룬다.
애플리케이션을 개발하다 보면 사용자의 입력 실수나 네트워크 문제 등으로 예상 가능한 에러가 발생하거나, 코드 상의 문제로 인한 예상치 못한 에러가 발생할 수 있다.
Next.js는 이러한 에러들을 효과적으로 처리하기 위해 명시적 상태 관리 기법과 에러 경계를 제공하며, 이를 통해 사용자에게 원활한 서비스를 제공할 수 있게 한다.
또한, 데이터를 가져오는 과정에서 사용자에게 빈 화면을 보여주는 대신 의미 있는 로딩 UI를 제공하고, 페이지 로딩 속도를 개선하는 스트리밍 기법을 사용할 수 있다.
Next.js는 React의 Suspense를 기반으로 페이지를 점진적으로 렌더링하는 스트리밍 방식을 지원하며, 이를 통해 빠르고 사용자 친화적인 웹 페이지를 구현할 수 있다.
본 글에서는 에러 핸들링과 로딩 UI, 스트리밍에 대한 이론적 배경과 실제 코드 예제를 기술하였다.
Error Handling
에러는 예상된 에러와 예상치 못한 에러 두 분류로 나눌 수 있다.
예상된 에러에는 사용자의 로그인 실패로 인한 폼 검증 실패, 사용자의 입력 누락으로 인해 실패한 요청 등이 있다.
예상된 에러는 try/catch로 처리하는 것을 피해야한다.
Next.js에서는 useActionState 훅을 사용하여 예상된 에러를 관리하고 클라이언트에 반환한다.
위의 경우에 해당하지 않는 에러는 예상치 못한 에러이다.
Next.js는 error.tsx나 global-error.tsx 파일을 사용하여 에러 경계를 구현하고 예상치 못한 에러에 대한 대체 UI를 제공한다.
예상된 에러는 명시적으로 처리 후 클라이언트에 반환해야 한다.
Handling Expected Errors from Server Actions
useActionState 훅을 사용하여 서버 액션 상태를 관리하고 에러를 처리한다.
해당 방식에서는 try/catch 블록으로 에러를 throw 하는 대신 지정된 반환 값으로 코드를 작성해야 한다.
app/actions.ts
'use server'
import { redirect } from 'next/navigation'
export async function createUser(prevState: any, formData: FormData) {
const res = await fetch('https://...')
const json = await res.json()
if (!res.ok) {
return { message: 'Please enter a valid email' }
}
redirect('/dashboard')
}
app/ui/signup.tsx
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button>Sign up</button>
</form>
)
}
요청을 처리하는 함수인 createUser은 서버로부터 성공 여부(ok)를 받지 못하면 { message: 'Please enter a valid email' } 을 전송한다.
클라이언트 컴포넌트에서는 사용자가 폼 제출을 할 시 createUser 함수가 실행된다.
state는 폼 제출 결과에 대한 상태를 가진다.
만약 서버에서 { message: 'Please enter a valid email' }을 전송했다면 Please enter a valid email이라는 토스트 메세지가 출력될 것이다.
Handling Expected Errors from Server Components
서버 컴포넌트 내에서 데이터를 가져올 때는 응답에 따라 조건부로 에러 메세지를 렌더링하거나 redirect 할 수 있다.
app/page.tsx
export default async function Page() {
const res = await fetch(`https://...`)
const data = await res.json()
if (!res.ok) {
return 'There was an error.'
}
return '...'
}
Uncaught Exceptions
Next.js는 예상치 못한 예외를 처리하기 위해 에러 경계를 사용한다.
에러 경계는 자식 컴포넌트에서 발생한 에러를 감지하고, 충돌한 컴포넌트 트리 대신 대체 UI를 표시한다.
경로 세그먼트 내에 error.tsx파일을 추가하고 대체 UI를 export하여 에러 경계를 생성한다.
error.tsx
p'use client' // Error boundaries must be Client Components
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}
에러는 가장 가까운 부모 경계로 전파된다.
이를 통해 경로 계층 구조에서 다양한 error.tsx 파일을 배치하여 세분화된 에러 처리가 가능하다.
Handling Global Errors
드물게 루트 레이아웃에서 에러를 처리해야 할 때는 app/global-error.tsx를 사용한다.
이 파일은 반드시 루트 app 폴더에 위치해야 한다.
전역 에러 UI는 루트 레이아웃과 템플릿을 대체하기 때문에 자체 <html>과 <body> 태그를 정의해야 한다.
'use client' // Error boundaries must be Client Components
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// global-error must include html and body tags
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Loading UI and Streaming
특정 경로 세그먼트에 대한 fallback을 생성하고, 콘텐츠가 준비되는 대로 자동으로 스트리밍할 수 있는 React Suspence 기반의 로딩 UI를 사용한다.
loading.ts 파일을 추가하여 폴더 내에 로딩 상태를 생성한다.
Streaming with Suspense
Suspense 경계는 UI 컴포넌트를 수동으로 스트리밍할 수 있도록 한다.
Node.js 및 Edge 런타임에서도 사용 가능하다.
스트리밍이란?
스트리밍은 SSR의 단점을 해결하기 위해 페이지의 HTML을 더 작은 청크로 나누고 서버에서 클라이언트로 점진적으로 전송하는 방식이다.
이를 스트리밍 렌더링 이라고 한다.
page.tsx
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
단순 html이 아닌 컴포넌트도 fallback prop으로 넘길 수 있다.
SEO(Search Engine Optimization)
Next.js는 UI를 스트리밍하기 전에 generateMetadata 내부에서 데이터 fetching이 완료될 때까지 대기한다.
이는 스트리밍 응답의 첫번째에 <head> 태그가 포함되도록 보장한다.
Status Codes
스트리밍은 응답을 시작하면서 항상 200 상태 코드를 반환하여 요청이 성공했음을 알린다.
하지만 중간에 redirect()나 notFound()와 같은 함수를 만나게 된다면 이미 스트리밍은 200으로 응답을 시작했기 때문에 나중에 404나 302와 같은 오류로 바꿀 수 없다.
Next.js는 이를 고려햐여 redirect나 notFound는 HTML 내에서 처리한다.
Next.js가 내부적으로 처리하기 때문에 검색엔진(SEO)가 볼 때는 문제없게 렌더링 해준다.
즉 이는 SEO에 영향을 미치지 않는다.
'Next.js' 카테고리의 다른 글
[Next.js] 리디렉션을 처리하는 여러 방법 (1) | 2025.04.24 |
---|---|
[Next.js] 네비게이션 작동 원리 (0) | 2025.04.19 |
[Next.js] Next.js 라우팅과 렌더링 원리 (0) | 2025.04.15 |