학습 자료

이 페이지는 언제 만들어졌나


Article

3강에서 "서버가 컴포넌트를 실행해 HTML을 만든다"고 했다. 그런데 언제 실행하는지는 말하지 않았다.

두 가지 가능성이 있다.

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강에서 이 목록의 절반을 이미 봤다. paramssearchParams, cookies(), headers()Promise가 된 이유가 이것이다.

await 하기 전까지는 미리 그릴 수 있다. await 하는 순간부터 요청을 기다리는 부분이 된다. 5강에서 던져둔 그 문장이 여기서 회수된다.

await 이 경계를 만든다
  1. 빌드 시점미리 그릴 수 있는 부분
  2. await cookies()여기서 멈춘다
  3. 요청 시점이 아래는 요청이 와야 안다

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, 조금씩 다시 만드는 정적 페이지다.

핵심은 기다리게 하지 않는다는 것이다.

1시간이 지난 뒤 첫 방문자가 왔을 때
  1. 01

    저장된 HTML 을 그냥 준다

    오래된 것이지만 즉시 받는다. 이 사람은 기다리지 않는다.

  2. 02

    뒤에서 새로 만든다

    응답을 보낸 뒤, 서버가 조용히 컴포넌트를 다시 실행한다.

  3. 03

    다음 사람은 새 것을 받는다

    만들어진 HTML 이 저장된 것을 덮어쓴다.

오래된 것을 주면서 뒤에서 갱신하는 이 방식을 stale-while-revalidate라 한다. 14강에서 이 이름이 다시 나온다.

세 가지 렌더링의 응답 시간
정적
파일 전송
ISR
저장된 파일 전송뒤에서 갱신 (사용자는 모른다)
동적
DB 조회렌더링 → 전송
요청
ISR 은 정적의 속도로 응답하면서 내용은 갱신된다

강제로 정한다

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는 기본적으로 캐시되지 않는다. "알아서 캐싱된다"는 설명은 옛 글이다.
  • revalidateISR을 켠다. 오래된 것을 즉시 주고 뒤에서 갱신한다.
  • 페이지와 fetchrevalidate가 다르면 짧은 쪽이 이긴다.
  • dynamic = "error"로 정적임을 못박을 수 있다. force-dynamic을 습관처럼 쓰지 않는다.
  • 정적/동적은 라우트 단위다. 한 줄 때문에 페이지 전체가 동적이 된다.

마지막 항목이 다음 강의의 출발점이다.

이 페이지는 언제 만들어졌나 — 디코드랩(DCODELAB)