
📅 2025-12-03
➡️ Next.js 렌더링 실습, Hydration, App 예약어 파일에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
npm i json-server{
"books": [
{
"id": 1,
"slug": "clean-code",
"title": "클린 코드",
"author": "로버트 C. 마틴",
"category": "programming",
"description": "좋은 코드를 작성하기 위한 원칙과 패턴을 다루는 고전적인 개발 서적입니다.",
"descriptionHtml": "<p>좋은 코드를 작성하기 위한 원칙과 패턴을 다루는 고전적인 개발 서적입니다.</p>",
"price": {
"amount": "32000",
"currencyCode": "KRW"
},
"coverImage": "https://picsum.photos/seed/cleancode/400/600",
"isNew": false,
"rating": 4.8,
"tags": ["프로그래밍", "베스트셀러", "클린코드"]
},
{
"id": 2,
"slug": "refactoring",
"title": "리팩터링 2판",
"author": "마틴 파울러",
"category": "programming",
"description": "기존 코드를 개선하면서도 동작을 유지하는 리팩터링 기법을 설명합니다.",
"descriptionHtml": "<p>기존 코드를 개선하면서도 동작을 유지하는 리팩터링 기법을 설명합니다.</p>",
"price": {
"amount": "45000",
"currencyCode": "KRW"
},
"coverImage": "https://picsum.photos/seed/refactoring/400/600",
"isNew": true,
"rating": 4.7,
"tags": ["프로그래밍", "리팩터링"]
},
{
"id": 3,
"slug": "javascript-deep-dive",
"title": "모던 자바스크립트 Deep Dive",
"author": "이웅모",
"category": "programming",
"description": "자바스크립트의 동작 원리를 깊이 있게 파헤치는 책입니다.",
"descriptionHtml": "<p>자바스크립트의 동작 원리를 깊이 있게 파헤치는 책입니다.</p>",
"price": {
"amount": "42000",
"currencyCode": "KRW"
},
"coverImage": "https://picsum.photos/seed/jsdeepdive/400/600",
"isNew": true,
"rating": 4.9,
"tags": ["자바스크립트", "프론트엔드"]
},
{
"id": 4,
"slug": "database-design",
"title": "실전 데이터베이스 설계",
"author": "홍길동",
"category": "database",
"description": "서비스 개발에 바로 적용할 수 있는 데이터베이스 모델링과 설계 전략을 다룹니다.",
"descriptionHtml": "<p>서비스 개발에 바로 적용할 수 있는 데이터베이스 모델링과 설계 전략을 다룹니다.</p>",
"price": {
"amount": "38000",
"currencyCode": "KRW"
},
"coverImage": "https://picsum.photos/seed/dbdesign/400/600",
"isNew": false,
"rating": 4.3,
"tags": ["데이터베이스", "백엔드"]
},
{
"id": 5,
"slug": "frontend-architecture",
"title": "프론트엔드 아키텍처 가이드",
"author": "김프론트",
"category": "frontend",
"description": "규모가 커지는 프론트엔드 프로젝트에서 구조를 설계하고 유지보수하는 방법을 소개합니다.",
"descriptionHtml": "<p>규모가 커지는 프론트엔드 프로젝트에서 구조를 설계하고 유지보수하는 방법을 소개합니다.</p>",
"price": {
"amount": "29000",
"currencyCode": "KRW"
},
"coverImage": "https://picsum.photos/seed/frontendarch/400/600",
"isNew": true,
"rating": 4.5,
"tags": ["프론트엔드", "아키텍처"]
}
],
"cart": []
}npm run json-server db.json --port 4000// src/types/book.ts
export interface BookPrice {
amount: string; // "32000"
currencyCode: string; // "KRW"
}
export interface Book {
id: number;
slug: string;
title: string;
author: string;
category: string;
description: string;
descriptionHtml: string;
price: BookPrice;
coverImage: string;
isNew: boolean;
rating: number;
tags: string[];
}import { Book } from '@/types/book';
import React from 'react';
const BookList = ({ books }: { books: Book[] }) => {
return (
<div>
{books.map((book) => (
<div key={book.id} className="mt-8">
<div className="border p-4 flex gap-6">
<img src={book.coverImage} alt={book.title} width={200} />
<div>
<h2 className="text-xl font-bold">{book.title}</h2>
<p className="text-sm text-gray-300">{book.author}</p>
<p>{book.description}</p>
</div>
</div>
</div>
))}
</div>
);
};
export default BookList;| 방식 | 특징 | fetch 옵션 | 언제 사용? |
|---|---|---|---|
| SSG | 빌드 시 HTML 생성 | cache: "force-cache" | 변경 없는 정적 페이지 (블로그, 문서) |
| SSR | 요청 시마다 HTML 생성 | cache: "no-store" | 항상 최신 데이터 필요 (검색 결과, 관리자 페이지) |
| ISR | SSG + 일정 주기 업데이트 | next: { revalidate: 10 } | 정적이지만 가끔 변경되는 페이지 (상품 목록) |
| CSR | 브라우저에서 fetch하여 렌더링 | 없음 (useEffect로 요청 수행) | 상호작용 많음, 초기 load 빠르게 SSG와 혼합 |
import { Book } from '@/types/book';
import BookList from '@/components/BookList';
const SSGBooks = async () => {
const res = await fetch('http://localhost:4000/books', {
cache: 'force-cache', // 캐싱
});
const books: Book[] = await res.json();
return (
<div>
<h1>Books</h1>
<p>개발 서적</p>
<BookList books={books} />
</div>
);
};
export default SSGBooks;
import { Book } from '@/types/book';
import BookList from '../BookList';
const SSRBooks = async () => {
const res = await fetch('http://localhost:4000/books', {
cache: 'no-store', // ✅ 매 요청마다 새로 가져오기 (SSR)
});
const books: Book[] = await res.json();
return (
<div>
<h1>Books SSR 예제(항상 최신 목록)</h1>
<p>개발 서적</p>
<BookList books={books} />
</div>
);
};
export default SSRBooks;
import { Book } from '@/types/book';
import BookList from '../BookList';
const ISRBooks = async () => {
const res = await fetch('http://localhost:4000/books', {
next: { revalidate: 10 }, // 10초마다 백그라운드에서 재생성 가능
});
const books: Book[] = await res.json();
return (
<div>
<h1>Books ISR 예제</h1>
<p>개발 서적</p>
<BookList books={books} />
</div>
);
};
export default ISRBooks;
'use client';
import { useEffect, useState } from 'react';
import { Book } from '@/types/book';
import BookList from '../BookList';
const CSRBooks = () => {
const [books, setBooks] = useState<Book[]>([]);
useEffect(() => {
const fetchBooks = async () => {
try {
const res = await fetch('http://localhost:4000/books');
const books: Book[] = await res.json();
setBooks(books);
} catch (err) {
console.log(err);
}
};
fetchBooks();
}, []);
return (
<div>
<h1>Books CSR 예제</h1>
<p>개발 서적</p>
<BookList books={books} />
</div>
);
};
export default CSRBooks;
CSR 방식에서는 전체 화면이 브라우저에서만 렌더링됨
index.html 하나만 전달처음부터 끝까지 JS가 직접 그리는 방식
공통적으로 2단계를 거침
app/layout.tsx가 자동 생성되며, 이는 전역(Global) 레이아웃으로 동작Root Layout의 규칙
<html>과 <body> 태그를 포함해야 함<head>에 직접 <title>이나 <meta>를 작성하는 것은 권장되지 않음 → 대신 Metadata API를 사용하여 관리왜 Metadata API를 쓰나?
children props
useEffect, animation 등의 사이드 이펙트도 다시 실행"use client"로 클라이언트 컴포넌트로 변경 가능언제 사용하나?
notFound() 함수가 호출된 경우 렌더링되는 UInotFound()가 호출되면 해당 세그먼트의 not-found.tsx가 렌더링됨HTTP 상태 코드 동작
200404루트 app/not-found.tsx
notFound()가 발생했을 때// app/not-found.tsx
export default function NotFound() {
return <div>존재하지 않는 페이지입니다.</div>;
}
특징
app/(segment)/error.tsx"use client" 필수)error: 서버/클라이언트에서 던져진 Error 객체 (프로덕션에서는 민감 정보는 제거됨)reset(): 에러 경계를 초기화해서 해당 세그먼트를 다시 렌더링 시도하는 함수// app/rendering/error.tsx
"use client";
import { useEffect } from "react";
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 에러 로깅 등
console.error(error);
}, [error]);
return (
<main className="flex min-h-[300px] flex-col items-center justify-center gap-4">
<h2 className="text-xl font-semibold">문제가 발생했습니다.</h2>
<p className="text-sm text-gray-500">
잠시 후 다시 시도해주세요. (에러 메시지: {error.message})
</p>
<button
onClick={() => reset()}
className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
>
다시 시도
</button>
</main>
);
}
특징
app/global-error.tsxapp/layout.tsx / app/template.tsx 에서 발생하는 모든 에러 처리 가능"use client" 필수)<html> / <body> 태그를 직접 작성해야 함// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="ko">
<body className="min-h-screen bg-gray-50">
<main className="flex min-h-screen flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">앱에 문제가 발생했습니다.</h1>
<p className="text-sm text-gray-500">
잠시 후 다시 시도해주세요. ({error.message})
</p>
<button
onClick={() => reset()}
className="rounded bg-red-600 px-4 py-2 text-sm text-white"
>
다시 시도
</button>
</main>
</body>
</html>
);
}
SEO란?
Next.js에서의 Metadata
<head> 태그를 직접 작성하지 않고 Metadata API를 사용Static Metadata (정적 메타데이터)
page.tsx 또는 layout.tsx에서 export const metadata로 정의예시: page.tsx
// src/app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Goorm Front End Next App",
description: "This is awesome Website",
};
export default function Home() {
return <div>안녕하세요 구름 프론트엔드 입니다.</div>;
}
예시: layout.tsx
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Sparta Next App",
description: "This is awesome Website",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body className={inter.className}>{children}</body>
</html>
);
}
Dynamic Metadata (동적 메타데이터)
/posts/[id])에서 URL 파라미터나 외부 데이터에 따라 메타데이터를 변경하고 싶을 때 사용generateMetadata 함수 이용예시
// 예: src/app/test/[id]/page.tsx
type Props = {
params: {
id: string;
};
};
export function generateMetadata({ params }: Props) {
return {
title: `Detail 페이지 : ${params.id}`,
description: `Detail 페이지 : ${params.id}`,
};
}
const TestDetailPage = ({ params }: Props) => {
return <div>Detail 페이지 : {params.id}</div>;
};
export default TestDetailPage;