[Next.js] App Router

원정·2025년 3월 28일

Next.js

목록 보기
2/4
post-thumbnail

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs/dashboard

인프런의 한 입 크기로 잘라먹는 Next.js 강의를 듣고 정리한 내용입니다.


앱 라우터는 Next 13 버전에 새롭게 추가된 페이지 라우터를 대체한 라우터다.
앱 라우터를 사용하면 페이지 라우팅, 레이아웃, 데이터 패칭 방식이 변경되고 리액트 18 버전부터 추가된 React Server Component, Streaming 등 최신 기능들도 함께 사용할 수 있다.

💰 vs 페이지 라우터

💵 페이지 라우팅


앱 라우터는 page라는 이름은 갖는 파일만 페이지 파일로 취급한다.
/search라는 경로에 대응하는 페이지는 search 폴더를 아래에 page 파일을 넣어줘야 한다.

동적 경로에 대응하는 페이지도 동일하게 만든다.

💵 레이아웃

Page.getLayout = (page: ReactNode) => {
  return <Layout>{page}</Layout>;
};
export default function App({
  Component,
  pageProps,
}: AppProps & {
  Component: NextPageWithLayout,
}) {
  const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
 
  return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}

페이지 라우터는 레이아웃을 적용하기 위해 페이지마다 getLayout이라는 메서드를 만들어서 App 컴포넌트에서 분기 처리를 해주는 번거로움이 있었다.

import { ReactNode } from "react";
import Searchbar from "../../components/searchbar";
 
export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div>
      <Searchbar />
      {children}
    </div>
  );
}

앱 라우터는 레이아웃 컴포넌트 파일만 작성하면 해당 경로 아래에 모든 페이지에 자동으로 레이아웃이 적용된다.

만약 search 폴더에 layout.tsx를 만들고 아래에 setting 폴더에 page 파일을 만든다면, /search/setting 경로로 진입했을 때도 레이아웃 컴포넌트가 먼저 렌더링되고 페이지 컴포넌트가 렌더링된다.

만약 setting 폴더 아래에도 layout 파일을 만든다면 중첩되어 적용된다.
search 폴더의 레이아웃, setting 폴더의 레이아웃, 페이지 컴포넌트 순으로 렌더링된다.

import "./globals.css";
import Link from "next/link";
 
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode,
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

RootLayoutapp 폴더 아래에 자동으로 생성된다.

만약 특정 페이지들만 동일한 레이아웃을 설정하고 싶다면 앱 라우터의 새로운 기능인 Route Group을 사용할 수 있다.
Route Group은 소괄호로 감싼 이름의 폴더를 지칭하고, 경로상에는 아무런 영향을 미치지 않는다.
Route Group 아래에 있는 페이지 컴포넌트에게만 동일한 레이아웃을 구성할 수 있고 바깥에 존재하는 페이지에는 레이아웃이 적용되지 않는다.

💰 React Server Component


기존 컴포넌트와 달리 브라우저에서 실행되지 않고 서버측에서만 실행되는 컴포넌트다.

서버에서 브라우저에게 전달하는 JS Bundle에는 페이지에 필요한 모든 컴포넌트들이 들어있고 브라우저에서 Hydration을 위해 한 번 더 실행된다.

하지만 JS Bundle에 모든 컴포넌트가 포함될 필요는 없다.
리액트 훅이 있거나 이벤트 핸들러가 있어서 상호 작용이 필요한 컴포넌트들을 제외한 정적인 컴포넌트들은 굳이 브라우저에서 한 번 더 실행될 필요가 없다.

페이지 라우터는 모든 컴포넌트를 JS Bundle로 묶어서 브라우저에게 전달한다.
JS Bundle의 용량이 불필요하게 커지고 불러오는 시간과 Hydraion하는 시간도 늘어나 TTI가 늦어진다.

앱 라우터는 상호 작용을 하는 컴포넌트와 그렇지 않은 컴포넌트를 분류해준다.
서버 컴포넌트는 사용자와 상호 작용하지 않는 컴포넌트로, JS Bundle에 포함하지 않아 서버측에서 한 번만 실행된다.
클라이언트 컴포넌트는 상호 작용이 필요한 컴포넌트로, 서버에서 한 번, 브라우저에서 한 번 실행된다.

앱 라우터는 브라우저에게 JS Bundle을 보낼 때 클라이언트 컴포넌트만 보냄으로써 파일의 용량을 줄여 앞선 문제점들을 해결한다.
Next.js 공식 문서에서도 페이지의 대부분을 서버 컴포넌트로 구성할 것을 권장한다.

export default function Page() {
  console.log("서버 컴포넌트!");
  return <div>...</div>;
}

앱 라우터는 기본적으로 서버 컴포넌트로 동작한다.
컴포넌트 내부에서 로그를 출력한다면 브라우저에서 확인할 수 없고 서버 터미널에서 확인할 수 있다.

서버에서만 실행되기 때문에 컴포넌트 내부에서 보안 키를 사용해도 브라우저에게 전달되지 않기 때문에 보안적인 문제가 발생하지 않을 뿐더러 직접 데이터를 불러오도록 설정할 수 있다.

기존 페이지 라우터에서 getServerSideProps, getStaticProps 함수가 했던 역할을 컴포넌트가 할 수 있게 설정해줄 수 있다.

"use client";
 
export default function Page() {
  console.log("클라이언트 컴포넌트!");
  return <div>...</div>;
}

리액트 훅이나 이벤트 핸들러같은 브라우저에서만 할 수 있는 일들을 사용하기 위해서는 클라이언트 컴포넌트를 사용해야 한다.
파일 최상단에 "useclient"라고 적어주면 클라이언트 컴포넌트로 인식한다.

클라이언트 컴포넌트와 서버 컴포넌트는 자바 스크립트 기능을 활용하여 사용자와 상호작용을 하거나, 안 하냐로 나눠서 사용할 수 있다.
자바 스크립트 기능을 활용한에 주목해야 하는데, Link 태그를 클릭하여 페이지를 이동하는 건 HTML 고유 기능이기 때문에 상호 작용에 해당하지 않는다.

💵 서버 컴포넌트 사용 시 주의 사항

클라이언트 컴포넌트에서 서버 컴포넌트를 import할 수 없다.

클라이언트 컴포넌트에서 서버 컴포넌트를 import할 경우, 서버 측에서 실행될 때는 문제가 없지만 브라우저에서 Hydration할 때 클라이언트 컴포넌트만 존재하여 문제가 된다.

"use client";
import ServerComponent from "./ServerComponent";
 
export default function ClientComponent() {
  return <ServerComponent />;
}

프로젝트가 크고 복잡해지면 의도치 않게 클라이언트 컴포넌트 안에서 서버 컴포넌트를 사용할 수도 있다.
Next.js는 오류를 발생시키는 대신에 서버 컴포넌트를 클라이언트 컴포넌트로 바꿔준다.

export default function ServerComponent() {
  return <div>...</div>;
}
"use client";
 
export default function ClientComponent({ children }: { children: ReactNode }) {
  return (
    <div>
      <children />
    </div>
  );
}
export default function Page() {
  return (
    <div>
      <ClientComponent>
        <SeverComponent />
      </ClientComponent>
    </div>
  );
}

어쩔 수 없이 클라이언트 컴포넌트에서 서버 컴포넌트를 사용해야 한다면 직접 import하지 않고 children으로 받아 렌더링 시켜주면 된다.

클라이언트 컴포넌트는 서버 컴포넌트를 직접 실행할 필요없이 서버 컴포넌트의 결과물만 props로 전달 받으면 되기 때문이다.

서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화 되지 않는 props를 전달할 수 없다.

직렬화란 객체, 배열, 클래스 등 복잡한 구조의 데이터를 네트워크 상으로 전송하기 위해 단순한 형태(바이트, 문자열)로 변환하는 과정을 말한다.

자바 스크립트의 함수는 값이 아닌 코드 블럭들을 포함하고 있는 특수한 형태를 갖고 클로저, 렉시컬 스코프 등의 다양한 환경에 의존한 경우가 많기 때문에 모든 정보를 단순한 형태로 표현할 수 없다.
직렬화되지 않는 함수는 서버 컴포넌트에서 클라이언트 컴포넌트로 향하는 props가 될 수 없다.

사전 렌더링 과정에서 클라이언트 컴포넌트와 서버 컴포넌트가 함께 실행하여 HTML 페이지를 생성하지 않는다.
서버 컴포넌트들이 먼저 실행되고 클라이언트 컴포넌트들이 실행된다.
서버 컴포넌트가 실행되면 결과로 RSC Payload라는 JSON과 비슷한 문자열을 생성한다.

RSC란 React Server Component의 줄임말로 RSC Payload란 서버 컴포넌트를 직렬화한 결과다.

RSC Payload에는 서버 컴포넌트의 렌더링 결과, 연결된 클라이언트 컴포넌트의 위치, 클라이언트 컴포넌트에게 전달하는 props 등 서버 컴포넌트와 관련된 모든 데이터가 들어있다.

이후 클라이언트 컴포넌트들도 실행되어 RSC Payload 결과와 합쳐져 HTML 페이지가 생성된다.
하지만 서버 컴포넌트들을 먼저 실행해서 RSC Payload라는 형태로 직렬화하는 과정에서 만약 특정 서버 컴포넌트가 자식인 클라이언트 컴포넌트에게 함수 형태의 값을 전달한다면 함수 값 또한 함께 직렬화 되어 RSC Payload에 포함되어야 한다.
하지만 함수는 직렬화 할 수 없기 때문에 런타임 에러가 발생한다.

profile
https://wonjung-jang.github.io/ 로 이동했습니다!

2개의 댓글

comment-user-thumbnail
2025년 4월 4일

원정님 안녕하세요~!
제가 업무에서 Next.js의 App Router를 사용하는 프로젝트가 있는데, 오류 해결을 위해 원정님 글을 참고했습니다.. 너무나 많은 도움이 되었어용ㅠ 감사합니다아 🙏
기록을 위해 제 벨로그에 트러블 슈팅 글을 올리려는데 출처로 해당 포스트를 넣으려구용 ~!
늘 양질의 글 잘 보고 갑니다앗 ㅎㅎ

1개의 답글