
📅 2025-12-04
➡️ Next.js Streaming, Suspense, Error Handling, Asset 최적화에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
// package.json
"scripts": {
"server": "json-server --watch db.json --port 4000 --delay 5000"
}
// json-server는 0.17.4 버전 사용 필요 → 사용자에게 빈 화면만 보이게 되고, UX가 크게 떨어짐 이런 상황을 처리하는 대표적인 방법 3가지가 있음'use client';
import { useEffect, useState } from 'react';
export default function AsyncComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Data Loaded</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
fetch의 캐싱 옵션으로 빌드 or 요청 속도를 향상import BookList from "@/components/BookList";
import { Book } from '@/types/book';
const BooksPage = async () => {
const response = await fetch('http://localhost:4000/books',
{
cache: "force-cache"
}
);
const books: Book[] = await response.json();
return (
<div>
<h1>Books</h1>
<BookList books={books} />
</div>
);
};
export default BooksPage;
비동기 컴포넌트가 준비될 때까지 대체 UI(fallback)를 보여주는 기능
→ 사용자에게 점진적으로 화면이 완성되는 느낌 제공
데이터 패칭 / 코드 분할(lazy-loading) 등 비동기 UI 처리에 유용
여러 비동기 컴포넌트를 각각 독립적으로 로딩 가능
- Suspense 단위로 비동기 UI를 쪼갤 수 있음
- 준비된 컴포넌트부터 순차적으로 화면 렌더링
- 전체가 느려도 부분적으로 먼저 표시되어 체감 성능 향상
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<BookList />
</Suspense>
);
}
라우트 세그먼트 내 비동기 작업이 완료될 때까지 자동으로 Loading UI를 표시
// /book/loading.tsx
export default function Loading() {
return <div>책 목록을 불러오는 중입니다...</div>;
}
Loading UI가 중요한 이유
사용자 경험 개선
사용자의 불필요한 행동 문제 해결
향상된 SEO 및 접근성
error.tsx 파일을 생성하여 예기치 못한 런타임 오류를 처리할 수 있음
'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>
)
}"use client"에서 동작해야 함왜 클라이언트 컴포넌트여야 할까?

사용자 상호작용 기능 필요
reset() 함수 사용 → 사용자 클릭을 통한 페이지 재시도에러 복구 로직은 클라이언트 상태 기반
Image 컴포넌트를 통해 이미지 최적화를 자동으로 수행함특징
크기 최적화
포맷 변환
시각적 안정성
Lazy Loading 기본 적용
자산 유연성
import Image from "next/image";
import profilePic from "/public/profile.png";
export default function Page() {
return (
<Image
src={profilePic}
alt="Picture of the author"
// width={500} 자동 제공
// height={500} 자동 제공
// blurDataURL="data:..." 자동 제공
// placeholder="blur" // 로딩 중 블러업 옵션
/>
)
}
너비, 높이 등등의 이런 세부적인 기능들은 Next.js에서 자동으로 제공
원격, 네트워크 이미지
import Image from 'next/image'
export default function Page() {
return (
<Image
src="<https://picsum.photos/seed/refactoring/400/600>"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.amazonaws.com',
port: '',
pathname: '/my-bucket/**',
},
],
},
}module.exports = {
images: {
// ...
formats: ['image/avif', 'image/webp'],
},
}폰트를 자동으로 최적화하고 외부 네트워크 요청을 제거하여 개인정보 보호와 성능을 향상시킬 수 있음
모든 폰트 파일에 대해 자동으로 셀프 호스팅을 제공
레이아웃 시프팅(Layout Shifting) 없이 최적의 방식으로 웹 폰트를 로드할 수 있도록 도와줌
Google Fonts
google fonts는 내장되어 있어서 따로 설치 필요 없이 바로 가져다 쓸 수 있음
//ver 15
import { Geist } from 'next/font/google'
const geist = Geist({
subsets: ['latin'],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
)
}
특정 font weight를 사용하고 싶으면 font weight를 지정해야 합니다.
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: '400',
subsets: ['latin'],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={roboto.className}>
<body>{children}</body>
</html>
)
}
여러 폰트를 사용해야 할 때는 폰트를 내보내고 필요한 곳에서 가져와 사용
// app/fonts.ts
import { Geist, Geist_Mono } from 'next/font/google'
export const geist = Geist({
subsets: ['latin'],
})
export const geistMono = Geist_Mono({
subsets: ['latin'],
})
// app/layout.tsx
import { geist } from './fonts'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
)
}
// app/fonts.ts
import localFont from 'next/font/local'
export const myFont = localFont({
src: './my-font.woff2',
})
// app/layout.tsx
import { myFont } from './fonts'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}import Script from 'next/script'
export default function Dashboard() {
return (
<>
<Script src="https://example.com/script.js" />
</>
)
}
beforeInteractive: Next.js 코드 및 페이지 하이드레이션 전에 스크립트 로드afterInteractive: (기본값) 페이지 하이드레이션 후 스크립트 로드lazyOnload: 브라우저 유휴 시간에 스크립트 로드worker: (실험적) 웹 워커에서 스크립트 로드