출시·고도화 중
Next.js 안내서 · 3/6
Next.js App Router를 이해하는 데 필요한 중심 개념은 몇 가지로 정리됩니다. 중첩되는 레이아웃과 페이지, 서버에서만 실행되는 Server Component와 브라우저에서 상호작용하는 Client Component, 서버 코드를 직접 호출하는 Server Action, 그리고 정적 · 동적 · 스트리밍 렌더링과 메타데이터입니다.
레이아웃은 하위 라우트를 children으로 받아 감쌉니다. 폴더마다 레이아웃을 둘 수 있고, 바깥 레이아웃이 안쪽 레이아웃을 감싸는 식으로 중첩됩니다. 같은 레이아웃 아래에서 페이지를 이동하면 레이아웃은 다시 그려지지 않고 상태가 유지되므로, 내비게이션이나 사이드바처럼 계속 남아 있어야 하는 부분을 레이아웃에 둡니다.
// app/dashboard/layout.tsx
import Link from "next/link";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<nav>
<Link href="/dashboard">개요</Link>
<Link href="/dashboard/settings">설정</Link>
</nav>
<div>{children}</div>
</section>
);
}App Router에서 레이아웃과 페이지는 기본적으로 Server Component입니다. Server Component는 서버에서 실행되어 결과만 브라우저로 보내므로, 그 코드와 사용한 라이브러리는 브라우저 자바스크립트 묶음에 들어가지 않습니다. 데이터베이스를 직접 조회하거나 API 키를 안전하게 쓸 수 있는 것도 이 때문입니다.
반면 useState, useEffect 같은 훅, onClick 같은 이벤트 처리, window나 localStorage 같은 브라우저 API가 필요하면 Client Component를 씁니다. 파일 맨 위에 "use client" 지시어를 적으면 그 파일과 그 파일이 가져오는 모듈이 브라우저 코드에 포함됩니다.
// app/ui/like-button.tsx
"use client";
import { useState } from "react";
export function LikeButton({ initial }: { initial: number }) {
const [likes, setLikes] = useState(initial);
return <button onClick={() => setLikes(likes + 1)}>좋아요 {likes}</button>;
}Server Component는 Client Component를 가져와 쓸 수 있습니다. 이때 서버에서 클라이언트로 넘기는 속성은 직렬화할 수 있는 값(문자열, 숫자, 일반 객체, 배열 등)이어야 하며, 함수는 넘길 수 없습니다(Server Action은 예외입니다).
// app/posts/[id]/page.tsx (Server Component)
import { LikeButton } from "@/app/ui/like-button";
import { getPost } from "@/lib/posts";
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton initial={post.likes} />
</article>
);
}권장하는 방법은 "use client" 경계를 가능한 한 잎 쪽에 두는 것입니다. 페이지 전체를 Client Component로 만들기보다 버튼이나 입력 폼처럼 상호작용이 필요한 작은 부분만 클라이언트로 두면 브라우저로 보내는 코드가 줄어듭니다. Client Component 안에 서버에서 그린 내용을 넣고 싶다면 children 속성으로 전달합니다.
Server Action(더 넓게는 Server Function)은 "use server" 지시어가 붙은 비동기 함수로, 브라우저에서 호출하지만 서버에서 실행됩니다. 별도의 API 경로를 만들지 않고도 폼 제출이나 데이터 변경을 처리할 수 있습니다. 폼의 action 속성에 넘기면 자바스크립트가 아직 불러와지지 않은 상태에서도 폼이 동작합니다.
// app/guestbook/page.tsx
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export default function GuestbookPage() {
async function addEntry(formData: FormData) {
"use server";
const message = String(formData.get("message") ?? "");
await db.entry.create({ data: { message } });
revalidatePath("/guestbook");
}
return (
<form action={addEntry}>
<input name="message" required />
<button type="submit">남기기</button>
</form>
);
}Server Action은 사실상 누구나 호출할 수 있는 공개 엔드포인트입니다. 함수 안에서 로그인 여부와 권한을 반드시 확인하고 입력 값을 검증해야 합니다.
Next.js는 라우트마다 언제 HTML을 만들지 정합니다.
| 방식 | 언제 만드나 | 어울리는 화면 |
|---|---|---|
| 정적 렌더링 | 빌드할 때(또는 재검증할 때) 미리 | 소개 페이지, 블로그 글, 문서 |
| 동적 렌더링 | 요청이 올 때마다 | 사용자별 대시보드, 검색 결과 |
| 스트리밍 | 준비된 부분부터 나눠서 | 느린 데이터가 섞인 화면 |
쿠키(cookies()), 요청 헤더(headers()), 검색 매개변수처럼 요청 시점에만 알 수 있는 값을 쓰거나 캐시하지 않는 데이터를 읽으면 그 부분은 요청 시점에 렌더링됩니다. 느린 부분은 <Suspense>로 감싸거나 같은 폴더에 loading.tsx를 두면, 나머지 화면을 먼저 보내고 준비된 내용을 이어서 스트리밍합니다.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { RecentOrders } from "./recent-orders";
export default function DashboardPage() {
return (
<main>
<h1>대시보드</h1>
<Suspense fallback={<p>주문을 불러오는 중…</p>}>
<RecentOrders />
</Suspense>
</main>
);
}최근 버전에는 한 페이지 안에서 정적인 틀과 동적인 부분을 섞는 모델(Cache Components, cacheComponents 설정)도 있습니다. 동작 방식이 버전에 따라 달라지고 있으므로, 쓰고 있는 버전의 공식 문서를 함께 확인합니다.
제목, 설명, Open Graph 같은 <head> 정보는 레이아웃이나 페이지에서 metadata 객체를 내보내 정합니다. 데이터에 따라 달라진다면 generateMetadata 함수를 씁니다. 두 가지 모두 Server Component에서만 쓸 수 있습니다.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPost } from "@/lib/posts";
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title, description: post.summary };
}"use client"로 분리합니다.Suspense와 loading.tsx로 스트리밍합니다.metadata와 generateMetadata로 페이지마다 검색 정보를 정합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.