Next.js의 App Router는 기존 Page Router의 방식과 같이 Client Side Rendering(CSR)을 통해 페이지 이동을 처리한다. 하지만 서버 컴포넌트의 도입으로 인해 기존 방식에 몇 가지 중요한 차이가 발생하기 때문에 이번 글에서는 App Router에서의 페이지 이동(Navigating)프리페칭이 어떻게 동작하는지, 실습 예제와 함께 알아보도록 하자.


📕 App Router의 페이지 이동 동작 원리

기존 Page Router 방식

  1. 초기 접속 요청: 서버에서 사전 렌더링된 HTML을 브라우저로 전달.

  2. 페이지 이동 전 프리페칭: Next.js는 이동 가능성이 있는 경로들의 JS 번들 및 데이터를 미리 가져옴.

  3. 페이지 이동 요청이 발생하면,

  • 브라우저는 이동할 페이지에 필요한 JS 번들 파일(컴포넌트들)을 서버로부터 가져옴.

    • 만약 해당 페이지가 프리페칭되어 있다면, 프리페칭된 데이터를 활용해 JS 번들 로딩 속도를 최적화한다.
  • 브라우저에서는 불러온 JS 번들을 실행하여 새로운 페이지를 렌더링.

App Router의 변경점

App Router에서도 페이지 이동은 CSR 방식으로 처리된다. 다만, 서버 컴포넌트가 추가되면서 페이지 이동 시 JS 번들 외에 RSC Payload도 함께 전달된다.

왜냐?

  • JS 번들에는 클라이언트 컴포넌트만 포함되며, 서버 컴포넌트는 포함되지 않는다.

  • 하지만 대부분의 페이지는 서버 컴포넌트와 클라이언트 컴포넌트가 혼합되어 있으므로, 서버 컴포넌트로 구성한 부분의 데이터들은 브라우저에서는 아예 누락이 되어버리는 것이기 때문에 정상적으로 페이지를 이동시킬 수 없게 된다. 그래서 서버 컴포넌트를 실행한 결과물인 RSC Payload가 JS 번들과 함께 브라우저로 전달된다.


📕 페이지 이동 실습

App Router에서도 next/link의 Link 컴포넌트를 활용해 네비게이션 바를 구성할 수 있다.

import "./globals.css";
import Link from "next/link";
import style from "./layout.module.css";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <div className={style.container}>
          <header>
            <Link href={"/"}>index</Link>
            &nbsp;
            <Link href={"/search"}>search</Link>
            &nbsp;
            <Link href={"/book/1"}>book/1</Link>
          </header>
          <main>{children}</main>
        </div>
      </body>
    </html>
  );
}

위 코드를 작성한 후 브라우저에서 각 페이지로 이동하면 네트워크 탭에서 요청을 확인할 수 있다.

네트워크 탭에서 Fetch/XHR이라는 요청만 필터링 되게 선택하고 search페이지로 이동해 보면,

search?_rsc라는 요청을 통해 RSC Payload를 불러오고 있는 것을 확인할 수 있다.

RSC Payload 확인

그리고 네트워크 요청의 Preview 탭에서 RSC Payloa가 직렬화된 텍스트인 JSON 형태로 전달된 것을 확인할 수 있다.
이 과정에서 RSC Payload는 서버 컴포넌트의 실행 결과물로, 서버 컴포넌트가 렌더링한 데이터를 포함한다.

그런데 이때 이 RSC Payload가 정상적으로 나오지 않는 경우도 있다. 그건 오류가 아니라 캐싱 됐기 때문이다. 그래서 혹시나 RSC Payload의 응답 값이 정상적으로 나오지 않는다면 다시 index페이지로 이동한 다음에 새로고침 마크에 오른쪽 클릭을 눌러서 "캐시 비우기 및 강력 새로고침"을 눌러본 다음에 다시 search페이지로 이동해 보면 모든 캐시가 비워져서 정상적으로 RSC Payload가 캐시되지 않고 불러와지게 된다.


📕 클라이언트 컴포넌트와 JS 번들

❗ 클라이언트 컴포넌트가 없는 경우

만약 해당 페이지가 서버 컴포넌트로만 구성되어 있다면, 이런 경우에는 페이지 이동시에 JS번들로서 전달될 클라이언트 컴포넌트가 없어서 그냥 RSC Payload만 전달이 되게 된다. 그럼 브라우저는 JS 번들 없이 RSC Payload만 전달받게 된다.

클라이언트 컴포넌트 추가

서버 컴포넌트와 클라이언트 컴포넌트를 혼합한 페이지를 구성해보자.
search 페이지에 클라이언트 컴포넌트를 추가한 코드 예제 👇

// src/pages/search/page.tsx

import ClientComponent from "../components/ClientComponent";

export default function SearchPage() {
  return <ClientComponent>{}</ClientComponent>;
}

이제 브라우저에서 "캐시 비우기 및 강력새로고침"을 실행한 다음에 네트워크탭에서 All을 선택하고 다시 확인하면, RSC Payload와 함께 클라이언트 컴포넌트가 포함된 JS 번들 파일도 요청된 걸 확인할 수 있다.

이렇듯이 App Router버전의 페이지 이동은 기존의 Page Router의 페이지 이동과는 기본적으로 거의 동일하게 이루어지지만 서버 컴포넌트의 추가로 인해서 서버 컴포넌트는 RSC Payload로 클라이언트 컴포넌트는 JS번들로서 전달이 되는 차이점이 생겼다.


📕 Programmatic 페이지 이동

Programmatic 네비게이션이란?

Programmatic 네비게이션은 사용자의 이벤트(ex: 버튼 클릭, 키보드 입력 등)에 의해 페이지를 이동시키는 방식을 말한다.
예를 들어, 검색 창에 검색어를 입력한 후 "검색" 버튼을 클릭하면 해당 검색어를 포함한 페이지로 이동하는 동작이 이에 해당한다.

✨ 검색 창을 통한 Programmatic 네비게이션 구현

// src/components/searchbar.tsx

"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import style from "./serachbar.module.css";

export default function Searchbar() {
  const router = useRouter(); // useRouter 훅으로 라우터 객체 가져오기
  const searchParams = useSearchParams(); // 현재 URL의 쿼리스트링 정보 가져오기
  const [search, setSearch] = useState(""); // 검색어 상태 저장

  const q = searchParams.get("q");

  useEffect(() => {
    setSearch(q || ""); // "q" 파라미터 값이 변경되면 search 상태를 업데이트
  }, [q]);

  const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
  };

  const onSubmit = () => {
    // 검색어가 없거나, 현재 검색어와 같다면 실행하지 않음
    if (!search || q === search) return;
    
    // 검색어를 포함한 페이지로 이동
    router.push(`/search?q=${search}`);
  };

  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      onSubmit();
    }
  };

  return (
    <div className={style.container}>
      <input
        value={search}
        onChange={onChangeSearch} // 검색어 상태 업데이트
        onKeyDown={onKeyDown} // Enter 키로 검색 실행
      />
      <button onClick={onSubmit}>검색</button>
    </div>
  );
}

이 컴포넌트는 "검색"이라는 버튼이 클릭되면, input태그에 사용자가 입력한 값(search state에 보관된 값)을 토대로 페이지를 search페이지로 이동시켜주면 되는 것이다.

  1. useRouter 활용
  • next/navigation으로 부터 useRouter훅을 불러와 페이지를 프로그래매틱하게 이동할 때 사용한다.
  1. 페이지 이동 동작 구현 (onSubmit 함수)
  • 검색어가 없거나, 현재 검색어와 같다면 아무 작업도 수행하지 않는다.

  • 검색어가 유효하면 router객체의 push메서드를 호출하여 /search?q=검색어 경로로 이동한다.

next/routerPage Router 버전에서 사용되던 라우터 훅이다.
App Router에서는 이를 대체하여 next/navigationuseRouter를 사용해야 페이지 이동이 제대로 처리된다.
따라서 App Router 기반 프로젝트에서는 반드시 next/navigation에서 useRouter를 가져와 사용해야 한다.


📕 App Router의 프리페칭

프리페칭이란?

프리페칭은 현재 페이지에서 연결된 다른 페이지들의 데이터를 미리 불러오는 작업이다. App Router는 프리페칭 시 각 페이지의 데이터 유형(static 또는 dynamic)에 따라 다르게 동작한다.

✨ Static과 Dynamic 페이지의 차이

Static 페이지

  • Page Router 기준으로 SSG(Static Site Generation) 방식으로 생성된 페이지와 유사하다.

  • 빌드 타임에 미리 생성된 정적인 콘텐츠로 구성된 페이지.

  • 데이터 업데이트가 필요하지 않으므로 RSC Payload와 JS 번들 파일을 모두 프리페칭한다.

Dynamic 페이지

  • Page Router 기준으로 SSR(Server Side Rendering) 방식으로 생성된 페이지와 유사하다.

  • 요청 시 즉각적으로 생성되는 동적 콘텐츠로 구성된 페이지.

  • 데이터 업데이트 가능성이 있으므로, RSC Payload만 프리페칭하며 JS 번들은 실제 페이지 이동 시 요청된다.

search페이지에서 "강력새로고침"을 하면 네트워크 탭에 index 페이지에 해당하는 RSC Payload

또는 book/1페이지에 해당하는 RSC Payload도 불러온 걸 볼 수 있다.

그리고 마지막으로는, next/static/chunks/app(with-searchbar)폴더 안에 page에 해당하는 JS번들 파일 그러니까 인덱스 페이지에 해당하는 JS번들 파일까지 이렇게 미리 프리페칭 된 걸 볼 수 있다.

프리페칭 동작 설명

  1. /(index) 페이지
  • static페이지로, 정적인 콘텐츠로 구성.

  • RSC Payload와 JS번들 파일 모두 프리페칭.

  1. /book/1 페이지
  • Dynamic 페이지로, URL Parameter(/book/[id])를 사용.

  • RSC Payload만 프리페칭, JS 번들은 실제 이동 시 요청.

  1. /search 페이지
  • Dynamic 페이지로, Query String을 사용.

  • RSC Payload만 프리페칭, JS 번들은 실제 이동 시 요청.

인덱스 페이지는 JS번들과 RSC Payload까지 받아오고 있지만, book/1페이지의 경우에는, RSC Payload만 받아오고 있다. 이 페이지는 JS번들은 받아오고 있지 않는데 왜냐, 인덱스 페이지의 경우에는 static한 페이지 즉, 정적인 페이지이기 때문에 JS번들까지 미리 다 불러오게 되지만, book/1페이지의 경우에는 dynamic한 페이지 즉, 동적인 페이지로서 자동 설정이 되어있기 때문에 JS번들은 생략하고 RSC Payload만 불러오도록 설정이 된 것이다.

✨ Static 페이지와 Dynamic 페이지의 판별 기준

App Router에서 페이지는 기본적으로 Static 페이지로 설정된다.
그러나 아래 조건에 해당하면 자동으로 Dynamic 페이지로 전환된다.

✔️ 페이지 내부에서 queryString을 사용하는 경우

  • ex) /search페이지에서 ?q=apple와 같은 queryString을 이용해서 동작하는 경우.

✔️ URL Parameter를 사용하는 경우

  • ex) /book/[id]와 같이 URL 경로에 따라 데이터를 다르게 렌더링해야 하는 경우.

빌드 결과를 보면, /search 페이지와 /book/[id]페이지는 querystring과 url parameter를 사용하므로 Dynamic 페이지로 설정되며, RSC Payload만 프리페칭한다. 반면 / (index 페이지)는 정적 콘텐츠로 구성되어 Static 페이지로 설정되며, RSC Payload와 JS 번들 파일을 모두 프리페칭한다.

그렇기 때문에 URL Parameter를 꺼내오도록 설정한 /book/[id]페이지의 경우에는 요청이 들어왔을 때 URL Parameter를 꺼내다가 써야되기 때문에 기본적으로 동적인 dynamic페이지로 설정된 걸 알 수 있고, 마찬가지로 /search페이지의 경우에도 querystring에 따라 다르게 동작하기 때문에 dynamic페이지로 설정된 걸 볼 수 있고, 그 외에 index 페이지나, not-found페이지의 경우에는 아무것도 설정을 해준 게 없기 때문에 기본적으로 static페이지로 되어 있는 걸 볼 수 있다.

✨ App Router의 프리페칭 동작 원리

Static 페이지의 프리페칭

  • 데이터 업데이트가 필요하지 않으므로 브라우저가 RSC Payload와 JS 번들 파일을 미리 요청한다.

  • 페이지 이동 시 별도의 추가 데이터 요청 없이 즉시 렌더링 가능하다.

Dynamic 페이지의 프리페칭

  • 데이터 업데이트 가능성을 고려해 RSC Payload만 프리페칭한다.

  • 이동 시 JS 번들은 서버에서 동적으로 요청된다.


결론

App Router는 기존 Page Router의 CSR 방식과 동일한 네비게이션 방식을 사용하지만, 서버 컴포넌트의 추가로 인해 다음과 같은 변화가 생겼다.

  1. 페이지 이동 시 데이터 처리
  • 클라이언트 컴포넌트는 JS 번들로 전달.

  • 서버 컴포넌트는 RSC Payload로 전달.

  1. 페이지 유형별 프리페칭 차이
  • Static 페이지는 JS 번들과 RSC Payload를 모두 프리페칭.

  • Dynamic 페이지는 RSC Payload만 프리페칭.


개인적으로 혼란한 부분 최종 정리...

클라이언트 컴포넌트와 서버 컴포넌트가 혼합된 경우에는 JS번들과 RSC Payload가 모두 프리페칭 되는 건가?

클라이언트 컴포넌트와 서버 컴포넌트가 혼합된 페이지의 경우, JS 번들과 RSC Payload 모두 프리페칭되는지 여부는 해당 페이지가 Static인지 Dynamic인지에 따라 다르다.

💡 동작 원리

  1. Static 페이지
  • 클라이언트 컴포넌트와 서버 컴포넌트가 혼합되어 있더라도, 페이지 자체가 Static으로 설정되어 있다면, JS 번들과 RSC Payload 모두 프리페칭된다.
  1. Dynamic 페이지
  • 클라이언트 컴포넌트와 서버 컴포넌트가 혼합되어 있는 경우라도, 페이지가 Dynamic으로 설정되어 있다면, RSC Payload만 프리페칭되고, JS 번들은 실제 페이지 이동 시에만 요청된다.

🔍 왜 이런 동작이 발생할까?

Next.js의 App Router는 페이지의 Static 또는 Dynamic 여부를 기준으로 프리페칭 동작을 결정한다.

  • Static 페이지는 데이터가 변경되지 않기 때문에 JS 번들과 RSC Payload를 미리 요청해도 안전하다.

  • Dynamic 페이지는 데이터 업데이트 가능성이 있기 때문에 RSC Payload만 미리 요청하고, JS 번들은 이동 시에만 요청하도록 최적화되어 있다.

🧪 예제

/search 페이지 (Dynamic 페이지)

  • 클라이언트 컴포넌트: 검색창(Searchbar)

  • 서버 컴포넌트: Query String 처리

이 페이지는 Dynamic 페이지로 설정된다. 따라서,

  • RSC Payload만 프리페칭된다.

  • 클라이언트 컴포넌트가 있더라도, JS 번들은 실제 페이지 이동 시에만 요청된다.

/index 페이지 - Static 페이지)

  • 클라이언트 컴포넌트: 일부 정적 UI

  • 서버 컴포넌트: Static 데이터 렌더링

이 페이지는 Static 페이지로 설정된다. 따라서,

  • JS 번들과 RSC Payload 모두 프리페칭된다.

따라서 Static인지 Dynamic인지의 페이지 유형에 따라 프리페칭 방식과 데이터 전달 방식이 달라지며, 이를 이해하면 효율적인 페이지 이동 전략을 설계할 수 있다. 🚀

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN