Next13 App Directory Routing

작년 말 Nextjs의 13버전으로 올라가면서 새로운 파일구조를 가진 App Directory Routing이 추가되었습니다. 이전에는 pages 폴더에 파일을 생성하여 라우팅을 했지만, 이제는 pages 폴더에 폴더를 생성하여 라우팅을 할 수 있게 되었습니다.

Nextjs 13버전 App Directory 사용하기

Nexjs의 최신버전인 13버전을 사용하려면 기존과 동일하게 npx create-next-app@latest를 사용하여 설치를 하면 됩니다.

다만, 아직 App Directory의 경우 실험적인 버전이므로 아래와 같이 next.config.js 파일에 설정을 추가해주어야 합니다.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
}

module.exports = nextConfig

App Directory Routing

이번 Next 13버전에 변경됭 라우팅 방식은 기존의 방식과 아예 다릅니다. 예약 파일이 증가 했고, 각 파일별로 사용하는 방법들이 변화 했습니다.

app
├── products
│       ├── layout.tsx
│       └── page.tsx
├── layout.tsx
└── page.tsx

기본적으로 페이지를 구성하는 방식은 위와 같습니다. 예약어인 page는 각 경로별로 page를 생성합니다. 위에서는 / 경로와 /products 경로에 대한 페이지를 생성한 것입니다.

그리고 layout은 각 페이지에 대한 레이아웃을 생성합니다. root의 layout의 경우 전체 레이아웃을 정의하며 products의 layout은 products 페이지에 대한 레이아웃만을 정의합니다.

만약 /products경로를 접속하게 되면 root layout내부의 products layout이 함께 렌더링 됩니다.

이때 root의 layout의 경우 기존의 _app.tsxdocument.tsx의 역할을 대체합니다. 만약 recoil과 같은 전역 상태 관리를 사용한다면 root layout에서 recoil의 provider를 정의하면 됩니다.

layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html>
      <body>
        <RecoilRoot>
          {children}
        </RecoilRoot>
      </body>
    </html>
  );
}

Dynamic Routing And Static Routing

1. Dynamic Routing

Nextjs 라우팅에서 유용한 기능인 Dynamic Routing은 기존과 동일하게 동작합니다. []를 사용하여 동적으로 라우팅을 할 수 있습니다.

app
├── products
│      ├── [id]
│      └── page.tsx

위와 같이 폴더를 생성하고 []를 사용하여 동적 라우팅을 할 수 있습니다.

/products/[id]/page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return <div>My Post: {params.id}</div>
}

2. Static Routing

Nextjs의 경우 기존에 SSR과 SSG를 지원하기 위해서 getServerSideProps와 getStaticProps를 사용하였습니다. 기존의 getServerSideProps와 getStaticProps는 더이상 사용되지 않으며, 이부분의 경우에도 13버전에 들어오면서 많은 부분이 변경 되었습니다.

Dynamic routing을 활용해서 products에 각 상품을 /products/1, /products/2과 같이 만든다고 가정하면 아래오 같이 만들 수 있습니다.

app
├── products
       ├── [...id]
       └── page.tsx

그리고 page.tsx에서 generateStaticParams를 사용하여 아래와 같이 정의할 수 있습니다.

/products/[...id]/page.tsx
export async function generateStaticParams() {
  const products = await fetch('https://.../products').then((res) => res.json())

  return products.map((product) => ({
    id: product.id,
    data: product.data,
  }))
}

export default function Page({ params }: { params: { id: string, data: any } }) {
  return <div>
    <h1>상품명 : {product.data.title}</h1>
    <h1>상품 가격{product.data.prcing}</h1>
  </div>
}

generateStaticParams는 서버에서 products의 데이터를 가져와서 정적으로 된 페이지를 생성합니다.

Parallel Routes and Interception

Parallel Routes과 Interception의 경우 이번 13.3버전에 새롭게 추가된 라우팅입니다. 해당 기능의 경우 기존에 라우팅방식에서 벗어나 완전 새로운 방식으로 컴포넌트를 사용하게 될 수 있을꺼 같습니다.

하지만 기존에 사용하던 코드 컨벤션과는 맞지 않아서 아직까지는 어떤 방식으로 활용해야 할지에 대해서는 추가적인 테스트가 필요할 것 같습니다.

우선 해당 기능들에 대해 쉽게 정의를 하자면, Parallel Routes의 경우에는 여러개의 라우팅을 동시에 사용할 수 있게 해주는 기능이며, Interception의 경우에는 라우팅을 가로채서 다른 컴포넌트를 렌더링 할 수 있게 해주는 기능입니다.

1. Parallel Routes

Parallel Routes는 하나의 페이지 안에 다른 페이지를 병렬로 렌더링 할 수 있게 해줍니다. @파일명으로 정의하고 해당 파일을 layout에서 원하는 곳에 렌더링을 시킬 수 있습니다

app
├── @barCharts
│     └── page.tsx
├── @kpi
│     └── page.tsx
├── page.tsx
└── layout.tsx

예를 들면 대시보드를 만든다고 가정했을때 위와 같이 barCharts와 kpi를 하나의 페이지에 정의를 한다하면 아래와 같이 사용할 수 있습니다.

layout.tsx
export default function RootLayout({
  children,
  barChart,
  kpi,
}: Readonly<{
  children: React.ReactNode;
  barChart: React.ReactNode;
  kpi: React.ReactNode;
}>) {
  return (
    <html>
      <body>
          {barChart}
          {kpi}
          {children}
      </body>
    </html>
  );
}

이렇게 되면 화면에 barChartkpipage.tsx가 렌더링 되고, 그 아래에 root의 page.tsx가 렌더링 됩니다.

Parallel Routes
Parallel Routes

부가적으로 @로 정의한 Parallel폴더 안에 error.tsx, loading.tsx를 정의 하여 병렬 처리한 페이지들을 개별적으로 로딩화면과 에러 화면을 구성 할 수 있습니다.

2. Interception

Interception의 경우 라우팅을 가로채서 다른 컴포넌트를 렌더링 할 수 있게 해주는 기능입니다. .로 interception할 경로를 정의할 수 있습니다. .의 경우 동일한 수준의 세그먼트를, ..의 경우 상위 세그먼트를 가로챌 수 있습니다.

또한 Parallel Routes와 함께 사용하면 아래와 같은 코드를 구성할 수 있습니다.

app
├── @modal
│ └── (..)products
│ └── [id]
│ └── page.tsx
├── products
│ └── [id]
│ └── page.tsx
├── page.tsx
└── layout.tsx

상품 페이지에서 특정 상품을 클릭했을때 모달창을 띄우고 싶다면 위와 같이 사용할 수 있습니다.

  1. 우선 상품 리스트를 root의 page.tsx에 정의해줍니다.

  2. 그런다음 @modal과 products의 폴더를 생성하고 page.tsx를 정의해줍니다.

  3. 그리고 layout에서 @modal을 사용하여 모달창을 렌더링 해줍니다.


이때 경로의 경우 '/'에서 상품을 클릭하게 되면 /product/1과 같이 라우팅이 됩니다. 원래 대로라면 products의 페이지로 라우팅이 되지만, @modal이 가로채서 모달창을 띄우게 됩니다.

@modal - /product/1
@modal - /product/1

이때 Parallel Routes를 활용하여 root page와 함께 modal이 렌더링이 되는 것을 알 수 있습니다.


만약 root의 page.tsx에서 상품을 클릭해서 이동하지 않고 해당 페이지로 바로 접속을 하게 되면, @modal의 렌더링 되는 것이 아닌, 기존의 products의 페이지로 라우팅이 됩니다. (또는 해당 페이지에서 리프레시)

page - /product/1
page - /product/1

Client Component와 Server Component

Client Component와 Server Component의 경우에는 Nextjs에서 추가된 기능은 아니고 React18에서 추가된 기능입니다.

Nextjs에서는 기본적으로 모든 컴포넌트 들이 Server Component로 동작하게 되어 있습니다. 만약 Client Component로 동작하게 하고 싶다면 페이지 상단에 use client를 추가해주면 됩니다.

서버 컴포넌트와 클라이언트 컴포넌트의 구분은 매우 간단합니다. 서버에서 동작하는 데이터 가져오고, SSG와 같은 서버 렌더링 등의 경우 서버 컴포넌트로 구성하고, 브라우저에서 동작을 하는 hook, 이벤트 핸들러 등의 경우 클라이언트 컴포넌트로 구성하면 됩니다.

Client Component내부에는 Server Component를 사용할 수 없지만, Server Component 내부에는 Client Component를 사용할 수 있습니다.

또한 데이터를 받아 오는 부분의 경우 React-query와 같이 클라이언트에서 패칭하는 라이브러리를 사용하는 것이 아닌 Nextjs의 fetch를 사용한다면, 서버 컴포넌트로만 구성할 수 있습니다.

그렇기 때문에 기존에 Container-Presenter와 같이 데이터 처리는 Server Component로 처리하고 해당 값을 UI만 구성되어 있는 Client Component로 구성하게 되면 효율적으로 컴포넌트를 구성할 수 있을 것 같습니다.