이 페이지는 언제 만들어졌나
Article
3강에서 "서버가 컴포넌트를 실행해 HTML을 만든다"고 했다. 그런데 언제 실행하는지는 말하지 않았다.
두 가지 가능성이 있다.
빌드할 때 한 번
정적 렌더링
npm run build하면서 만든다- 요청이 오면 파일을 그냥 준다
- 누가 봐도 같은 화면
- 빠르고 싸다
요청이 올 때마다
동적 렌더링
- 사용자마다 컴포넌트를 실행
- 쿠키 · 검색어를 읽을 수 있다
- 사람마다 다른 화면
- 느리고 서버를 쓴다
우리는 이 선택을 한 적이 없다. Next.js가 코드를 보고 알아서 정한다. 무엇을 보고 정하는지 알아야 한다.
빌드 출력을 읽는다
npm run build를 돌리면 라우트 목록이 나온다.
- Route (app)
- ┌ ○ /
- ├ ○ /about
- ├ ƒ /dashboard
- └ ƒ /posts/[id]
- ○ (Static) prerendered as static content
- ƒ (Dynamic) server-rendered on demand
동그라미(○)는 빌드할 때 만들어진 것이다. 물결표(ƒ)는 요청마다 만드는 것이다.
/와 /about은 아무 데이터도 안 받으니 정적이다. /dashboard는 쿠키를 읽었고, /posts/[id]는 요청마다 글을 받아온다.
무엇이 페이지를 동적으로 만드나
요청이 있어야만 알 수 있는 값을 건드리는 순간이다.
| 이것을 쓰면 | 왜 미리 못 만드나 |
|---|---|
| await cookies() | 사용자마다 다르다 |
| await headers() | 요청마다 다르다 |
| await searchParams | 주소가 요청마다 다르다 |
| await connection() | "요청을 기다려라"는 뜻 |
| 캐시하지 않은 fetch | 값이 매번 달라질 수 있다 |
5강에서 이 목록의 절반을 이미 봤다. params와 searchParams, cookies(), headers()가 Promise가 된 이유가 이것이다.
await 하기 전까지는 미리 그릴 수 있다. await 하는 순간부터 요청을 기다리는 부분이 된다. 5강에서 던져둔 그 문장이 여기서 회수된다.
- 빌드 시점미리 그릴 수 있는 부분
- await cookies()여기서 멈춘다
- 요청 시점이 아래는 요청이 와야 안다
Next 16 에서 fetch 는 캐시되지 않는다
여기가 인터넷 예제와 가장 크게 어긋나는 지점이다.
export default async function Posts() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <ul>…</ul>;
}
이 페이지는 동적이다. ƒ로 표시된다.
Next.js 13~14에서는 이게 정적이었다. fetch 결과를 기본으로 캐싱했기 때문이다. "Next가 알아서 캐싱해준다"는 설명은 그 시절 것이다.
15부터 기본값이 뒤집혔다. 캐시하지 않는다. 캐싱은 이제 우리가 켠다.
| fetch 옵션 | 결과 |
|---|---|
| 아무것도 안 씀 | 캐시 안 함 → 페이지가 동적 |
| { cache: "force-cache" } | 캐시함 → 페이지가 정적 |
| { next: { revalidate: 3600 } } | 1시간마다 갱신 → 정적 + ISR |
| { next: { tags: ["posts"] } } | 태그를 붙인다 → 14강 |
다시 만드는 주기를 정한다 — ISR
블로그 글 목록을 생각해보자. 요청마다 DB를 조회할 만큼 자주 바뀌지 않는다. 그렇다고 빌드할 때 굳혀두면 새 글이 영영 안 보인다.
한 시간에 한 번만 다시 만든다.
export const revalidate = 3600;
export default async function Posts() {
const posts = await getPosts();
return <ul>…</ul>;
}
파일 맨 위에 revalidate를 내보내면 그 페이지 전체에 적용된다. 이걸 ISR이라 부른다. Incremental Static Regeneration, 조금씩 다시 만드는 정적 페이지다.
핵심은 기다리게 하지 않는다는 것이다.
- 01
저장된 HTML 을 그냥 준다
오래된 것이지만 즉시 받는다. 이 사람은 기다리지 않는다.
- 02
뒤에서 새로 만든다
응답을 보낸 뒤, 서버가 조용히 컴포넌트를 다시 실행한다.
- 03
다음 사람은 새 것을 받는다
만들어진 HTML 이 저장된 것을 덮어쓴다.
오래된 것을 주면서 뒤에서 갱신하는 이 방식을 stale-while-revalidate라 한다. 14강에서 이 이름이 다시 나온다.
강제로 정한다
Next.js의 판단을 덮어쓸 수 있다.
export const dynamic = "force-dynamic"; // 항상 요청마다
export const dynamic = "force-static"; // 항상 빌드할 때
export const dynamic = "error"; // 동적이 되면 빌드 실패
force-dynamic을 습관처럼 붙이는 사람이 있다. 먼저 왜 동적이 됐는지 찾는다. 대개 어딘가에서 cookies()를 부르고 있다.
"error"가 오히려 유용하다. "이 페이지는 반드시 정적이어야 한다"고 못박아두면, 누군가 실수로 cookies()를 넣었을 때 빌드가 실패해서 알려준다.
그런데 한 줄 때문에 전체가 동적이 된다
여기가 이 강의의 진짜 문제다.
블로그 글 페이지가 있다. 본문은 아무에게나 똑같다. 그런데 오른쪽 위에 로그인한 사람 이름을 띄우고 싶다.
export default async function Post({ params }) {
const { id } = await params;
const post = await getPost(id); // 모두에게 같다
const user = await getUser(); // 쿠키를 읽는다
return (
<>
<header>{user?.name ?? "로그인"}</header>
<article>{post.body}</article>
</>
);
}
getUser() 한 줄 때문에 페이지 전체가 동적이 된다. 본문까지 요청마다 다시 그린다.
우리가 원하는 것
섞고 싶다
- 본문은 빌드할 때 굳힌다
- 이름만 요청 때 채운다
- 대부분은 빠르게
지금 얻는 것
라우트 통째로
- 한 줄이 전체를 물들인다
- 본문도 매번 다시 그린다
- 정적의 이점이 사라진다
우회하는 방법은 있다. 이름 부분만 클라이언트 컴포넌트로 빼서 브라우저에서 받아오는 것이다. 그러면 화면이 한 번 깜빡이고, 8강에서 피하려던 그 방식으로 돌아간다.
페이지 하나 안에서 정적인 부분과 동적인 부분을 나눌 수는 없을까.
Next.js 16의 답이 있다. 이 문제를 풀려고 캐싱 모델 전체를 다시 설계했다. 다음 강의에서 "use cache"를 만난다.
정리하면
- 페이지는 빌드할 때 만들어지거나 요청이 올 때마다 만들어진다. 우리가 고르지 않고 Next.js가 코드를 보고 정한다.
npm run build출력에서○는 정적,ƒ는 동적이다.cookies(),headers(),searchParams, 캐시하지 않은fetch가 페이지를 동적으로 만든다.- 개발 서버에서는 항상 동적이다. 빌드해야 차이가 보인다.
- Next 15부터
fetch는 기본적으로 캐시되지 않는다. "알아서 캐싱된다"는 설명은 옛 글이다. revalidate로 ISR을 켠다. 오래된 것을 즉시 주고 뒤에서 갱신한다.- 페이지와
fetch의revalidate가 다르면 짧은 쪽이 이긴다. dynamic = "error"로 정적임을 못박을 수 있다.force-dynamic을 습관처럼 쓰지 않는다.- 정적/동적은 라우트 단위다. 한 줄 때문에 페이지 전체가 동적이 된다.
마지막 항목이 다음 강의의 출발점이다.