Front-End

Next.js Project Structure

나른한 노치 2024. 2. 3. 15:45

Next.js 구조에 대해서 제대로 모르고 사용하다보니 쉽게 해결할 일도 어렵게 만드는 것을 가끔 겪는 것 같습니다.

오늘은 Next.js에서 제공해주는 프로젝트 구조에 대해서 간단하게 살펴볼 예정입니다. 공부하게 되면서 새롭게 알게 되는 것들을 적용할 수 있으니 한번 블로그를 적으면 알아보겠습니다.

Top-level folder

최상위 폴더에는 app, pages, public, src가 있습니다.

app

app

: App Router를 사용하기 위한 폴더입니다.

  • Next.js에서 공유 레이아웃, 중첩 라우팅, 로딩 상태, 오류 처리 등을 지원하는 React Server Components를 기반으로 구축된 새로운 매커니즘입니다.

page

: Page Router를 사용하기 위한 폴더입니다.

  • 페이지 개념을 기반으로 구축된 파일 시스템 기반 라우터이며, 파일이 디렉터리에 추가되면 pages 자동으로 경로로 사용할 수 있습니다.

public

: Static Assets 즉, 정적 에셋을 제공할 때, 사용하는 폴더입니다.

src

: application 소스 폴더이며, 필수는 아닙니다.

Top-level files

next.config.js

: Next.js를 구성하는 파일입니다.

  • 서버 및 빌드 단계에서 사용되며 브라우저 빌드에는 포함되지 않습니다.
  • ESM이 필요한 경우, next.config.mjs 를 통해 사용할 수도 있습니다.
  • 12.1.0부터는 async를 통해 비동기로도 사용할 수 있습니다.
  • Webpack, Babel 또는 TypeScript에서 파싱되지 않습니다.

package.json

: 프로젝트 종속성 및 스크립트를 관리하는 파일입니다.

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}
  • next dev: 개발 모드에서 Next.js를 시작하기
  • next build: 프로덕션용 애플리케이션을 빌드하기
  • next start: Next.js 프로덕션 서버를 시작하기
  • next lint: Next.js의 기본 제공 ESLint 구성을 설정하기

instrumentation.ts

: 코드를 사용하여 모니터링 및 로깅 도구를 애플리케이션에 통합하는 프로세스입니다.

import { registerOTel } from '@vercel/otel'

export function register() {
  registerOTel('next-app')
}
  • 아직 canary 단계이기에 next.config.js에서 아래와 같이 설정이 필요합니다.
  • module.exports = { experimental: { instrumentationHook: true, }, }
  • 사용하기 위해선 프로젝트의 root에 있어야 합니다. src 폴더 사용 시, 페이지 또는 앱과 함께 src 안에 파일을 배치해야 합니다.

middleware.ts

: Next.js에서 request 시 사용되는 미들웨어입니다.

  • 요청이 완료되기 전에 코드를 실행할 수 있고, 그 요청에 따라 재작성이 가능합니다.
  • matcher: 특정 경로에서만 실행되도록 미들웨어를 필터링 하는 기능입니다.
    • 단일 또는 배열을 이용해서 다중 경로 사용이 가능합니다.
    • 전체 정규식을 사용할 수 있습니다.
    • missing 을 사용하여 미들웨어를 거칠 필요가 없는 prefetch를 무시할 수 있습니다.
export const config = {
  matcher: [
        '/about/:path*', '/dashboard/:path*', // 다중 경로
        '/((?!api|_next/static|_next/image|favicon.ico).*)', // 정규식
        {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}
  • request.cookies를 통해 cookie를 사용할 수 있습니다.
  • response.headers.set을 통해 헤더를 설정할 수 있습니다.
  • Response.json 메서드를 통해 응답을 만들 수 있습니다.

etc.

  • .env : 환경 변수를 모아둔 파일입니다.
    • .env.local: local 에서 사용하는 환경 변수를 모아둔 파일입니다.
    • .env.production: Production 에서 사용하는 환경 변수를 모아둔 파일입니다.
    • .env.development: Development에서 사용하는 환경 변수를 모아둔 파일입니다.
  • .eslintrc.json: ESLint 구성을 설정하는 파일입니다.
  • .gitignore: Git에서 무시할 파일/폴더를 설정합니다.
  • next-env.d.ts: Next.js를 위한 타입스크립트 declaration 파일입니다.
  • tsconfig.json: 타입스크립트 설정 파일입니다.
  • jsconfig.json: 자바스크립트 설정 파일입니다.

app 라우팅 컨벤션(규칙)

Routing Files

layout.{js,jsx,tsx}

: Layout

routes(해당 경로 이하)에 공유되는 UI입니다.

  • root layout은 최상위 레이아웃으로또는태그와 기타 전역적으로 공유되는 UI를 정의하는데 사용됩니다.
    • root layout은 반드시 app 디렉토리에 포함되어야 합니다.
    • root layout은 반드시 html과 body 태그를 정의해야 합니다.
      • root layout에서 title, meta 태그를 수동으로 추가해서는 안됩니다.
      • Metadata API를 통해서 추가해야 합니다.
      • import { Metadata } from 'next'
  • layout은 children 속성을 사용해야 합니다.
  • layout에서는 searchParams를 받지 못합니다.
    • 공유된 layout은 다시 렌더링되지 않기 때문입니다.

page.{js,jsx,tsx}

: Page

경로에 유니크한 UI입니다.

  • params: 루트 세그먼트에서 해당 페이지로 내려가는 동적 경로 매개변수가 포함된 객체입니다.
  • searchParams: 현재 URL의 검색 매개변수가 포함된 객체입니다.
    • searchParams는 값을 미리 알 수 없는 동적 API입니다. 이를 사용하면 요청 시 페이지가 동적 렌더링으로 선택됩니다.
    • searchParamsURLSearchParams 인스턴스가 아닌 일반 JavaScript 객체를 반환합니다.

loading.{js,jsx,tsx}

: Loading UI

Suspense를 기반으로 즉각적인 로딩 상태를 생성할 수 있습니다.

  • default는 Sever Component 이지만 "use client"를 통해 Clinet Component로 사용할 수 있습니다.

not-found.{js.jsx.tsx}

: Not found UI

경로 세그먼트 내에서 notFound 함수가 던져질 때 UI를 렌더링하는 데 사용됩니다. 스트리밍되지 않은 응답의 경우 404를 반환합니다.

  • 앱에서 처리하지 않는 URL을 방문하는 사용자에게는 app/not-found.js 파일에서 내보낸 UI가 표시됩니다.
  • async를 통해 데이터를 가져올 수 있습니다.
import Link from 'next/link'
import { headers } from 'next/headers'

export default async function NotFound() {
  const headersList = headers()
  const domain = headersList.get('host')
  const data = await getSiteData(domain)
  return (
    <div>
      <h2>Not Found: {data.name}</h2>
      <p>Could not find requested resource</p>
      <p>
        View <Link href="/blog">all posts</Link>
      </p>
    </div>
  )
}

error.{js,jsx,tsx}

: Error UI

경로 세그먼트의 오류 UI 경계를 정의합니다. Server Component와 Client Component에서 발생하는 예기치 않은 오류를 포착하고 대체 UI를 표시하는 데 유용합니다.

  • error.jsboundary들은 클라이언트 컴포넌트여야 합니다.
  • production 에서는 민감한 정보가 유출되지 않도록 세부 정보가 제거 됩니다.
  • error.js의 boundary는 layout.js 컴포넌트에 중첩되어 있기 때문에 동일한 위치에 있으면 layout에서 발생한 에러는 캐치하지 못합니다.

Props

  • error: Client Component로 전달되는 에러 객체의 인스턴스입니다.
    • error.message
      • Client Component에서 전달된 에러의 경우, 에러 메시지가 됩니다.
      • Server Component에서 전달된 에러의 경우, 일반 에러 메시지가 됩니다. errors.digest를 사용해서 서버 측 로그에서 해당 오류를 일치시킬 수 있습니다.
    • error.digest: Server Component에서 발생한 에러의 자동 생성 해시입니다. 서버 측 로그에서 해당 오류와 일치하는 데 사용할 수 있습니다.
  • reset: Error Boundary를 재설정하는 함수입니다. 이 함수가 실행되면 Error Boundary 내용을 다시 렌더링하려고 시도하며, 성공하면 fallback Error Component가 다시 렌더링한 결과를 대체합니다.
    • 사용자에게 오류 복구를 시도하라는 메시지를 표시하는 데 사용할 수 있습니다.

global-error.{js,jsx,tsx}

: Global error UI

root의 layout.js의 오류를 처리하기 위해 필요한 파일입니다.

  • global-error.js는 활성화되면 루트 layout.js를 대체하므로 자체 <html><body> 태그를 정의해야 합니다.
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

route.{js,ts}

: API endpoint

라우트 핸들러를 사용하면 웹 Request 및 Response API를 사용하여 지정된 경로에 대한 사용자 지정 요청 핸들러를 만들 수 있습니다.

  • request 는 웹 요청 API의 확장인 NextRequest 객체입니다.
    • NextRequest를 사용하면 쿠키 및 확장된 파싱된 URL 객체 nextUrl에 쉽게 액세스하는 등 들어오는 요청을 추가로 제어할 수 있습니다.
  • 현재 context의 유일한 값은 현재 경로에 대한 동적 경로 매개변수가 포함된 객체인 params뿐입니다.
  • 라우트 핸들러는 NextResponse 객체를 반환하여 웹 응답 API를 확장할 수 있습니다.
    • 이를 통해 쿠키, 헤더, 리디렉션 및 재작성을 쉽게 설정할 수 있습니다.
// route.ts
export async function GET(request: Request) {}

export async function HEAD(request: Request) {}

export async function POST(request: Request) {}

export async function PUT(request: Request) {}

export async function DELETE(request: Request) {}

export async function PATCH(request: Request) {}

// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and  set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}

template.**{js,jsx,tsx}**

: Re-rendered layout

각 하위 레이아웃 또는 페이지를 감싸고 있다는 점에서 레이아웃과 유사합니다. 경로 전체에 걸쳐 상태를 유지하는 레이아웃과 달리 템플릿은 탐색 시 각 하위 레이아웃에 대해 새 인스턴스를 생성합니다.

default.js.{js,jsx,tsx}

: Parallel route fallback page(병렬 경로 폴백 페이지)

해당 파일은 전체 페이지 로드 후 Next.js가 슬롯의 활성 상태를 복구할 수 없는 경우 병렬 경로 내에서 폴백을 렌더링하는데 사용됩니다.

Dynamic Routes

[folder](Dynamic route segment)

: 동적 세그먼트는 폴더의 이름을 대괄호로 묶어 만들 수 있습니다.

Route Example URL params
app/blog/[slug]/page.js /blog/a { slug: 'a' }
app/blog/[slug]/page.js /blog/b { slug: 'b' }
app/blog/[slug]/page.js /blog/c { slug: 'c' }

[…folder](Catch-all route segment)

: 괄호 안의 [...폴더명] 안에 줄임표를 추가하여 동적 세그먼트를 모든 후속 세그먼트를 포함하는 세그먼트로 확장할 수 있습니다.

Route Example URL params
app/shop/[...slug]/page.js /shop/a { slug: ['a'] }
app/shop/[...slug]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[...slug]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

[[...folderName]](Optional catch-all route segment)

: 이중 대괄호 안에 매개변수를 포함하여 포괄 세그먼트를 선택사항으로[[...folderName]] 만들 수 있습니다 .

  • catch-allOptional catch-all 세그먼트의 차이점은 Optional을 사용하면 매개변수가 없는 경로도 match가 가능합니다.
Route Example URL params
app/shop/[[...slug]]/page.js /shop {}
app/shop/[[...slug]]/page.js /shop/a { slug: ['a'] }
app/shop/[[...slug]]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[[...slug]]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

Route Groups and Private Folders

  • (folder)(Route Group): 폴더 이름을 괄호로 묶어 URL 경로에 영향을 주지 않고 경로 그룹을 만들 수 있습니다
    • 레이아웃에 특정 세그먼트 선택: (folder) 디렉터리 내에 layout.js을 만들면 폴더 내에 있는 세그먼트에만 layout.js가 적용됩니다.
    • 여러 루트 레이아웃 만들기: 최상위 layout.js 파일을 제거하고 각 경로 그룹 안에 layout.js 파일을 추가하여 여러 루트 레이아웃을 만들 수 있습니다.
  • _folder(Private Folder): 폴더 및 모든 하위 세그먼트를 라우팅에서 제외됩니다.
    • 해당 폴더가 비공개 구현 세부 정보이며 라우팅 시스템에서 고려하지 않아야 함을 나타내므로 해당 폴더 및 모든 하위 폴더가 라우팅에서 제외됩니다.
    • 다음과 같은 경우에 유용할 수 있습니다.
      • UI 로직과 라우팅 로직을 분리하는 경우.
      • 프로젝트와 Next.js 에코시스템 전반에서 내부 파일을 일관성 있게 정리.
      • 코드 편집기에서 파일 정렬 및 그룹화.
      • 향후 Next.js 파일 규칙과 충돌할 수 있는 잠재적 명명 충돌 방지.

Parallel and Intercepted Routes (병렬 및 인터셉트 경로)

@folder(Named slot)

: 병렬 경로는 명명된 슬롯을 사용하여 만들어집니다. 슬롯은 @폴더 규칙으로 정의됩니다.

  • Slot은 부모 layout.jsprops으로 전달됩니다.
  • 슬롯은 경로 세그먼트가 아니며 URL 구조에 영향을 미치지 않습니다.
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

(.)folder(Intercept same level)

  • (..)folder(Intercept one level above)
  • (..)(..)folder(Intercept two levels above)
  • (…)folder(Intercept from root)

: 상대 경로 규칙 ../과 유사하지만 세그먼트의 경우 (..) 규칙을 사용하여 경로를 가로채는 것을 정의할 수 있습니다. (같은 param를 사용할 수 있음)

  • (..) 규칙은 파일 시스템이 아닌 경로 세그먼트를 기준으로 한다는 점에 유의하세요.

  • Intercepting Routes를 병렬 경로와 함께 사용하여 모달을 만들 수 있습니다.
    • URL을 통해 모달 콘텐츠를 공유할 수 있도록 만들기.
    • 페이지를 새로 고칠 때 모달을 닫지 않고 컨텍스트 유지.
    • 이전 경로로 이동하지 않고 뒤로 탐색할 때 모달을 닫습니다.
    • 앞으로 탐색할 때 모달을 다시 열기.

정리

이번에 정리하면서 Next.js에서 제공하는 폴더 구조를 보며 어떻게 하면 더 프로젝트를 잘 정리하고 효율적으로 사용할 수 있는지 고민하게 되는 시간이었던 것 같습니다. 특히 병렬로 라우팅을 한다는걸 보고 멍해졌지만 문득 회원의 등급에 따라 보여줄 수 있는 컨텐츠를 선별해서 보여줄 수 있겠다 싶어서 고민을 해봐야겠습니다.