Next.js는 서버에서 HTML을 미리 생성해 초기 로딩과 SEO 문제를 해결한다. 그런데 실제 프로젝트에서는 어떻게 사용할까?
Next.js App Router는 렌더링, 데이터 패칭, API 작성, SEO 설정까지 하나의 구조 안에서 처리할 수 있도록 설계되어 있다.
이번 글에서는 App Router를 기준으로 자주 사용하는 기능을 정리해보려고 한다.
App Router의 특징
App Router에서는 서버 컴포넌트(Server Component)가 기본값이다.
React에서는 보통 브라우저에서 데이터를 가져오고 화면을 만들었다. 반면, App Router는 필요한 HTML을 서버에서 먼저 생성한다.
그리고 버튼 클릭, 입력 처리처럼 상호작용이 필요한 부분만 클라이언트 컴포넌트(Client Component)로 내려보낸다.
즉, 데이터 조회는 서버에서 처리하고, 필요한 HTML을 미리 생성하여 인터랙션만 브라우저에서 담당하는 구조다. 이 구조 덕분에 초기 로딩 성능과 SEO를 모두 챙길 수 있다.
렌더링 방식 선택하기
App Router에서는 fetch의 캐시 전략에 따라 렌더링 동작이 달라진다.
예전 Pages Router에서는 getServerSideProps, getStaticProps 같은 함수를 사용했다.
하지만 App Router에서는 서버 컴포넌트 안에서 fetch를 사용하는 것만으로도 렌더링 전략을 선택할 수 있다.
1. SSG : 정적으로 생성
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return (
<div>
{posts.map(post => (
<p key={post.id}>{post.title}</p>
))}
</div>
)
}
특별한 동적 설정이 없다면 Next.js는 결과를 정적으로 캐시 한다. 빌드 시점에 HTML을 생성해 두고 이후 요청에는 캐시 된 결과를 제공한다.
다음과 같은 페이지에 적합하다.
- 기술 블로그
- 회사 소개 페이지
- 마케팅 랜딩 페이지
2. SSR : 요청마다 생성
const posts = await fetch('https://api.example.com/posts', {
cache: 'no-store',
})
cache: 'no-store' 옵션을 사용하면 요청마다 새로운 HTML을 생성한다. 항상 최신 데이터를 보여줘야 하는 경우 사용한다.
다음과 같은 페이지에 적합하다.
- 실시간 재고
- 관리자 대시보드
- 사용자별 개인화 페이지
3. ISR : 일정 주기로 갱신
const posts = await fetch('https://api.example.com/posts', {
next: {
revalidate: 60,
},
})
ISR은 SSG와 SSR의 중간 형태다. 정적 페이지처럼 빠르게 응답하면서도 일정 시간이 지나면 새로운 HTML을 다시 생성한다.
뉴스 목록이나 상품 목록처럼 데이터가 자주 변경되지만 실시간까지는 필요 없는 경우에 적합하다.
언제 무엇을 사용할까
| 상황 | 방식 |
| 거의 변하지 않는 페이지 | SSG |
| 일정 주기로 갱신되는 페이지 | ISR |
| 요청마다 데이터가 달라지는 페이지 | SSR |
| 사용자 인터랙션 중심 기능 | Client Component |
실제 프로젝트에서는 하나만 사용하는 경우보다 페이지별로 적절히 조합하는 경우가 많다.
데이터 패칭
컴포넌트 자체를 async 함수로 만들 수 있다. 그래서 별도의 데이터 패칭 함수 없이 서버 컴포넌트 안에서 바로 데이터를 가져올 수 있다.
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const product = await fetch(
`https://api.example.com/products/${params.id}`
).then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}원</p>
</div>
)
}
Pages Router의 getServerSideProps보다 훨씬 단순한 구조다. 또한 서버에서 실행되기 때문에 API Key 같은 민감한 정보도 안전하게 사용할 수 있다.
클라이언트에서 데이터를 가져와야 하는 경우
모든 데이터를 서버에서 가져올 수 있는 것은 아니다. 좋아요 버튼, 실시간 입력, 사용자 인터랙션처럼 브라우저 상태가 필요한 경우에는 클라이언트 컴포넌트를 사용해야 한다.
'use client'
import { useState, useEffect } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
useEffect(() => {
fetch('/api/like')
.then(r => r.json())
.then(data => setLiked(data.liked))
}, [])
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
)
}
useState, useEffect, 이벤트 핸들러를 사용하려면 반드시 use client가 필요하다.
Route Handler로 API 만들기
App Router에서는 별도의 Express 서버 없이 API를 만들 수 있다.
app/api 폴더 안에 route.ts 파일을 만들면 자동으로 API 엔드포인트가 생성된다.
app
└─ api
└─ posts
├─ route.ts
└─ [id]
└─ route.ts
Route Handler를 사용하면 DB 연결, 인증, 파일 업로드 같은 백엔드 작업도 Next.js 안에서 처리할 수 있다.
metadata로 SEO 설정하기
정적 Metadata
export const metadata = {
title: '내 서비스',
description: '서비스 설명',
}
Next.js가 자동으로 head 태그를 생성해준다.
페이지별 title 설정
export const metadata = {
title: {
default: '내 서비스',
template: '%s | 내 서비스',
},
}
export const metadata = {
title: '블로그',
}
결과
블로그 | 내 서비스
동적 Metadata
export async function generateMetadata({
params,
}: {
params: { id: string }
}) {
const post = await getPost(params.id)
return {
title: post.title,
description: post.summary,
}
}
상세 페이지마다 다른 title, description, Open Graph 정보를 생성할 수 있다.
마무리
App Router의 핵심은 서버 컴포넌트를 기본값으로 사용한다는 점이다. 필요한 HTML은 서버에서 만들고, 인터랙션이 필요한 부분만 클라이언트로 내려보낸다.
그리고 이 과정에서 아래와 같은 과정을 하나의 프레임워크 안에서 처리할 수 있다.
- fetch로 렌더링 전략 선택
- async 컴포넌트로 데이터 패칭
- Route Handler로 API 작성
- metadata로 SEO 관리
React가 브라우저 중심 구조였다면 Next.js App Router는 서버와 클라이언트의 역할을 다시 분리해 성능과 개발 경험을 모두 개선하려는 방향에 가깝다.
'💻 STUDY > Frontend' 카테고리의 다른 글
| MVC부터 React의 단방향 데이터 흐름까지 (0) | 2026.06.29 |
|---|---|
| z-index 무한루프에서 벗어나기 — React Portal 제대로 이해하기 (0) | 2026.06.22 |
| Next.js는 React의 어떤 문제를 해결할까? (0) | 2026.06.08 |
| React 클라이언트 전역 상태 관리 (0) | 2026.06.07 |
| Storybook이란? — props가 많아질수록 개발이 느려진다고 느꼈다면 (0) | 2026.06.01 |