본문 바로가기
Dev log

Next.JS - page directory VS app directory

by Pig_CoLa 2024. 4. 1.
SMALL

서론

기존까지 Next.JS를 사용했을 때 (13 Ver 이하) page directory 구조로만 작업했었다.
ServerSideProps가 편리했고 폴더 구조 또한 path기반으로 단순했기 때문.

이번에 토이 프로젝트를 만들 때 app directory구조로 만들어 볼까 해서
두 방식에 어떤 차이가 있는지 큰 틀만 간단하게 정리해봅니다.

page directory와 app directory의 차이

폴더 구조의 차이

역시 가장 크게 두드러지는 차이는 라우팅 되는 경로를 지정하는 방법이지 않을까?

page directory에서는 pages의 하위 폴더 구조 및 파일 이름(index일 때에는 해당 폴더)이 경로 그 자체인데 반해,
app directory에서는 app 하위 폴더에서 여러 규칙들(라우팅 그룹, 슬롯 등)을 거쳐 예약어(page, route 등)로 만들어진 파일이 경로가 된다.

  • pages directory
pages
├── _app.tsx
├── _document.tsx
├── api
│   └── hi.ts
├── layout.module.scss
├── hi
│   └── index.tsx
└── index.tsx
  • app directory
app
├── (default)
│   ├── hi
│   │   └── page.tsx
│   ├── layout.module.scss
│   ├── layout.tsx
│   └── page.tsx
├── api
│   └── hi
│       └── route.ts
└── layout.tsx

직관적으로 보기에는 pages direcotry가 보기 편한 것 같다.

app direcotry의 경우 같은 레이아웃을 공유(라우팅 그룹이 같음)하지만 다른 path일 경우
한눈에 추적하기 어려워 질 수 있을 것 같다.

  • 아래와 같으면 폴더 및 파일 구조를 보고 한번에 파악하기 어려울 것이다…

/A - 레이아웃 그룹 1
/A/B - 레이아웃 그룹 1
/A/B/C - 레이아웃 그룹 2
/B - 레이아웃 그룹 1
/B/C - 레이아웃 그룹 2

API 라우팅

pages directory에서는 api/* 경로 아래에 handler를 구성해야 한다는 제약이 있다.
그 외 경로에서는 page가 되어버린다.

app directory에서는 경로와 상관없이 route.ts (js) 파일을 구성하는 것으로 보다 자유롭다.

또한 두 방식의 API라우팅 핸들러는 구성이 다르다.

  • pages directory API handler
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    return res.status(200).send('hi')
  }

  if (req.method === 'POST') {
    return res.status(200).json({ a: 1 })
  }

  return res.status(404).send('')
}
  • app directory API handler
import type { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  return Response.json({ a: 1 })
}

export const GET = function (req: NextRequest) {
  return new Response('hi')
}

레이아웃

pages directory에서는 layout지정이 따로 없다.

그렇다고 page를 만들 때 마다 특정 컴포넌트를 삽입하는 비효율적인 작업을 하진 않고
보통 _app.tsx 와 같은 루트 컴포넌트에서 처리하도록 한다.

  • pages directory
  • 모든 페이지가 하나의 레이아웃만 갖는다면 _app.tsx에 직접 요소들을 넣어줘도 큰 문제 없겠지만, 여러 레이아웃을 공통으로 갖는 경우도 잦으므로 LayoutProvider를 만들어 핸들링 하도록 했었다.
// types/index.d.ts

import type { ReactElement, ReactNode } from 'react'

type ChildrenExpect = ReactNode | HTMLElement

type Children<T> = T extends true
  ? Readonly<{ children: ReactNode }>
  : T extends false
  ? {}
  : T extends ReactNode
  ? { children: T }
  : T extends HTMLElement
  ? { children: ReactElement<T> }
  : never

declare global {
  type FCF<T extends object | ChildrenExpect = {}, K extends ChildrenExpect = false> = ((
    props: T extends ChildrenExpect ? Children<T> : Children<K> & T,
  ) => ReactNode) & { layout?: string }
}
// pages/_app.tsx

import type { AppProps } from 'next/app'

import Footer from '@/components/footer'
import Header from '@/components/header'

export default function App(app: AppProps) {
  return <LayoutProvider app={app} />
}

function LayoutProvider({ app }: { app: AppProps }) {
  let _app = app as unknown as { Component: { layout?: string } }
  if (!('layout' in _app.Component)) {
    _app.Component.layout = 'default'
  }

  if (_app.Component.layout === 'default') {
    return (
      <div key={app.router.pathname}>
        <Header></Header>
        <div>
          <app.Component {...app.pageProps} />
        </div>
        <Footer></Footer>
      </div>
    )
  } else if (_app.Component.layout === 'none') {
    return <app.Component {...app.pageProps} />
  }
}
// pages/index.tsx

const Home: FCF = function () {
  return (
    <div>
      <title>hi</title>메인페이지 입니다.
    </div>
  )
}

Home.layout = 'default'

export default Home

두 방식의 차이

일단 위 구성대로면 둘다 헤더와 푸터를 지닌 페이지를 렌더링 할 것이다.
또한 다른 페이지로 이동하더라도 같은 레이아웃을 사용한다면 동일한 폼일 것이다.

하지만 동일하게 보일 뿐 실제로는 다른 행동을 하게된다.

app directory에서는 동일한 레이아웃일 경우 교체되는 부분을 제외하고는 리렌더링이 일어나지 않는다.

하지만 pages directory에서는 리렌더링이 일어난다. 심지어 unmount되었다가 다시 mount된다.

  • useEffect의 clean up 함수를 아무거나 찍고 페이지 전환을 해보자.
    app directory간 전환은 같은 레이아웃이면 clean up 함수가 실행되지 않는다.
    pages directory간 전환은 같은 레이아웃이라도 clean up 함수가 실행된다.

같이 사용하기

놀랍게도 pages directory와 app directory는 같이 사용할 수 있다.

물론 더욱 폴더 구조가 복잡해 보이겠지만, 점진적으로 다른 쪽으로 마이그레이션 할 수 있다는 것이다.

src
├── app
├── components
├── hooks
├── pages
└── styles

다만 pages directory와 app directory가 너무 무분별하게 섞여 있다면 좋진 않을 것이다.
두 종류의 페이지간 전환은 SPA가 아니다..!!

  • 만약 전역변수가 있다면 pages → pages 와 app → app 의 전환에서 변수가 유지되지만
    pages ↔ app 간의 전환에서는 유지되지 않는다

결론

아직 pages directory가 deprecated된 것은 아니기 때문에
꼭 app directory만을 사용해야 하는 것은 아니다.

app directory만의 새로운 기능들을 활용하고 싶다면 일부를 마이그레이션 해보고 결정 하자

LIST

댓글