본문 바로가기
웹개발/NextJS

[NextJS] NextJS에서 쓰는 라우팅용 특수 파일들

by 김무스비 2025. 7. 3.
728x90
반응형

NextJS와 같은 프레임워크는 개발의 생산성과 효율성을 극대화하기 위해 미리 정해진 규칙과 구조를 제공하는데요. 그 핵심 중 하나가 바로 라우팅 시스템을 위한 특수 파일들입니다. 이러한 파일들은 단순한 파일명처럼 보이지만, 각각 고유한 역할을 담당하며 Next.js 애플리케이션의 라우팅 동작, UI 구성, 그리고 데이터 처리에 깊이 관여합니다. 

오늘은 이런 특수 파일들에 대해 알아보겠습니다.(typescript 기반으로 알아보겠습니다)

반응형


우선, 파일명은 무조건 위에 첨부된 사진 .tsx로 해야한다는 점 잊지 마시구요~! 하나씩 알아봅시다.(typescript 환경을 기본으로 가정했습니다.)

자신이 위치한 폴더의 모든 하위 경로에 대해 기본적으로 적용되는 항목들에 대해서는 옆에 별을 넣었습니다.

 

1. Layout.tsx ★: 동일 폴더와 하위 폴더 페이지의 레이아웃을 정의

  • 역할: layout.tsx는 특정 라우트 세그먼트와 그 하위에 있는 모든 자식 라우트 세그먼트(페이지 또는 다른 레이아웃)에 공통적으로 적용될 UI(레이아웃)를 정의합니다. 하위 경로에 layout.tsx가 없다면, {children}에 page.tsx가 들어가게 되고, layout.tsx가 있다면 루트의 {children}에 그 layout.tsx가 들어가고, 그 layout.tsx의 {children}에는 하위 경로의 page.tsx가 들어가는 중첩 구조입니다.
  • 특징:
    • layout.tsx는 반드시 children prop을 받아야 하며, 이 children prop 위치에 하위 콘텐츠가 렌더링됩니다.
    • 상위 layout.tsx는 하위 layout.tsx를 감싸는 형태로 중첩(nesting)될 수 있습니다.
    • URL이 변경되어도 layout.tsx는 기본적으로 유지(persistent)됩니다. 즉, 같은 레이아웃 범위 내에서 페이지가 전환될 때 레이아웃은 다시 렌더링되지 않고, children 부분만 교체됩니다. 이는 상태 유지 및 성능 최적화에 유리합니다.
  • 예시: 웹사이트의 전역 헤더/푸터(app/layout.tsx), 특정 섹션(예: dashboard 섹션)의 사이드바 및 네비게이션 바(app/dashboard/layout.tsx).

폴더 구조와 코드로 layout.tsx를 완전히 뿌셔보실까요.

우선 폴더 구조입니다.

app/
├── layout.tsx         // (A) Root Layout: 앱 전체에 적용
├── dashboard/
│   ├── layout.tsx     // (B) Dashboard Layout: /dashboard 및 하위에 적용
│   ├── settings/
│   │   ├── layout.tsx // (C) Settings Layout: /dashboard/settings 및 하위에 적용
│   │   └── page.tsx   // (D) /dashboard/settings 페이지
│   └── page.tsx       // (E) /dashboard 페이지
└── page.tsx           // (F) Root Page: / 페이지

이제 각 경로에 접근했을 때 어떤 layout.tsx들이 어떻게 중첩되어 적용되는지 살펴보겠습니다.

 

- URL: / (Root Page)

  • app/page.tsx (F)가 렌더링됩니다.
  • 적용되는 레이아웃은 오직 (A) app/layout.tsx 입니다.
(A) app/layout.tsx
    ├── (F) app/page.tsx

 

- URL: /dashboard (Dashboard Page)

 

  • app/dashboard/page.tsx (E)가 렌더링됩니다.
  • 적용되는 레이아웃은 상위에서부터 (A) app/layout.tsx 가 감싸고, 그 안에 (B) app/dashboard/layout.tsx 가 또 감싸는 형태입니다.

 

(A) app/layout.tsx
    ├── (B) app/dashboard/layout.tsx
        ├── (E) app/dashboard/page.tsx

 

- URL: /dashboard/settings (Settings Page)

  • app/dashboard/settings/page.tsx (D)가 렌더링됩니다.
  • 적용되는 레이아웃은 최상위 (A) app/layout.tsx, 그다음 (B) app/dashboard/layout.tsx, 그리고 가장 안쪽에 (C) app/dashboard/settings/layout.tsx 가 감싸는 형태로 적용됩니다.
(A) app/layout.tsx
    ├── (B) app/dashboard/layout.tsx
        ├── (C) app/dashboard/settings/layout.tsx
            ├── (D) app/dashboard/settings/page.tsx

 

 

이제 코드도 보시죠.

// app/layout.tsx (A)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <header style={{ background: 'lightblue', padding: '10px' }}>전역 헤더</header>
        {children}
        <footer style={{ background: 'lightgray', padding: '10px' }}>전역 푸터</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx (B)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ border: '2px solid blue', padding: '20px', margin: '10px' }}>
      <nav>대시보드 사이드바</nav>
      {children} {/* 여기에 하위 레이아웃이나 페이지가 들어갑니다. */}
    </div>
  );
}

// app/dashboard/settings/layout.tsx (C)
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ border: '2px dashed green', padding: '15px', margin: '5px' }}>
      <h3>설정 섹션 헤더</h3>
      {children} {/* 여기에 최종 페이지가 들어갑니다. */}
    </div>
  );
}

// app/dashboard/settings/page.tsx (D)
export default function SettingsPage() {
  return (
    <div style={{ background: 'lightgreen', padding: '10px' }}>
      <h1>설정 페이지 콘텐츠</h1>
      <p>여기에서 설정을 변경할 수 있습니다.</p>
    </div>
  );
}

이렇게 layout을 구성하면, /dashboard/settings 페이지에 접속하면, 화면에는 전역 헤더와 전역 푸터를 포함하는 푸른색 테두리의 대시보드 레이아웃 안에, 다시 초록색 점선 테두리의 설정 섹션 레이아웃이 감싸고, 그 안에 설정 페이지 콘텐츠가 렌더링되는 것을 볼 수 있습니다.

 

최종적으로는 아래와 같이 렌더링이 되겠죠.

<!DOCTYPE html>
<html lang="ko">
  <head>
    </head>
  <body>
    <header style="background: lightblue; padding: 10px;">전역 헤더</header>

    <div style="border: 2px solid blue; padding: 20px; margin: 10px;">
      <nav>대시보드 사이드바</nav>

      <div style="border: 2px dashed green; padding: 15px; margin: 5px;">
        <h3>설정 섹션 헤더</h3>

        <div style="background: lightgreen; padding: 10px;">
          <h1>설정 페이지 콘텐츠</h1>
          <p>여기에서 설정을 변경할 수 있습니다.</p>
        </div>
        </div>
      </div>
    <footer style="background: lightgray; padding: 10px;">전역 푸터</footer>
    </body>
</html>

 

2. page.tsx: 특정 경로에 대한 페이지 UI 정의

  • 역할: page.tsx는 특정 URL 경로에 직접 매핑되어 사용자에게 보여질 최종 UI 콘텐츠를 정의합니다.
  • 특징:
    • page.tsx 파일이 있는 폴더는 하나의 라우트 세그먼트를 나타내며, 해당 URL로 접속했을 때 렌더링됩니다.
    • page.tsx는 children prop을 받지 않습니다. (자신이 라우트의 가장 말단이기 때문)
    • 서버 컴포넌트(async 키워드 사용 가능)로 데이터를 직접 페칭하고 렌더링할 수 있습니다.
  • 예시: app/page.tsx는 / 경로, app/products/page.tsx는 /products 경로를 처리합니다. 
// app/about/page.tsx
export default function AboutPage() {
  return (
    <main>
      <h1>About Us</h1>
      <p>저희 회사에 대해 알아보세요.</p>
    </main>
  );
}

 

 

3. loading.tsx : 페이지 로딩 중에 표시되는 UI

  • 역할: loading.tsx는 page.tsx나 layout.tsx 내에서 데이터를 가져오거나 비동기 작업이 완료되기를 기다리는 동안 사용자에게 보여줄 로딩 스켈레톤이나 스피너 등의 UI를 정의합니다. 즉, loading.tsx는 상위 layout.tsx의 children 영역에서, 해당 라우트 세그먼트의 page.tsx (또는 하위 layout.tsx와 그 콘텐츠)가 완전히 준비되기 전에 렌더링될 대체 UI 요소를 의미합니다.
  • 특징:
    • 해당 loading.tsx가 위치한 라우트 세그먼트와 그 하위 라우트에서 적용됩니다.
    • loading.tsx는 내부적으로 React의 Suspense를 활용하여 비동기 데이터 페칭 중인 콘텐츠를 감싸고, fallback UI를 제공하는 방식과 유사하게 동작합니다.
    • 사용자가 다음 페이지로 이동할 때 즉시 로딩 상태를 보여주어 사용자 경험을 향상시킵니다.
  • 예시: app/dashboard/loading.tsx는 /dashboard 페이지를 불러오는 동안 표시됩니다.
// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="loading-spinner">
      <h2>대시보드 데이터를 불러오는 중...</h2>
      <p>잠시만 기다려 주세요.</p>
    </div>
  );
}

 

4. not-found.tsx : 404 오류 페이지

 

  • 역할: 사용자가 존재하지 않는 URL로 접근했을 때 보여줄 404 "페이지를 찾을 수 없음" 오류 UI를 정의합니다.
  • 특징:
    • not-found.tsx 파일이 위치한 폴더의 경로 또는 그 하위 경로에서 notFound() 함수가 호출되거나, 유효한 page.tsx를 찾을 수 없을 때 렌더링됩니다.
    • 최상위 app/not-found.tsx는 전체 애플리케이션에 대한 기본 404 페이지로 작동합니다.
  • 예시: app/not-found.tsx를 통해 전역 404 페이지를 만들거나, app/products/category/not-found.tsx를 통해 특정 카테고리 내에서만 발생하는 404를 처리할 수 있습니다.
// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h1>404 - 페이지를 찾을 수 없습니다.</h1>
      <p>요청하신 페이지가 존재하지 않습니다.</p>
      <Link href="/">홈으로 돌아가기</Link>
    </div>
  );
}

 

 

5. error.tsx : 특정 라우트 세그먼트 내의 오류 처리

 

  • 역할: 특정 라우트 세그먼트(즉, 자신이 위치한 폴더와 그 하위 자식 컴포넌트)에서 발생하는 JavaScript 오류를 잡아내고, 사용자에게 오류 대체 UI를 표시하는 React Error Boundary 역할을 합니다. 애플리케이션의 JavaScript 코드 실행 중 발생하는 런타임 오류를 처리합니다.
  • 특징:
    • 클라이언트 컴포넌트('use client')여야 합니다.
    • error, reset prop을 받습니다. error 객체는 발생한 오류 정보를 포함하고, reset 함수는 오류 경계를 재설정하여 컴포넌트를 다시 렌더링하려고 시도합니다.
    • layout.tsx의 오류는 잡지 못하고, 그 하위 page.tsx나 다른 클라이언트 컴포넌트에서 발생하는 오류를 처리합니다.
    • 가장 가까운 error.tsx 우선합니다. 즉, 여러 레벨에 error.tsx가 있다면, 오류가 발생한 위치를 기준으로 가장 가까운 상위 error.tsx가 적용됩니다.
  • 예시: app/dashboard/error.tsx는 대시보드 내에서 발생하는 특정 오류를 사용자 친화적인 메시지와 함께 '다시 시도' 버튼을 보여줄 수 있습니다.
// app/dashboard/error.tsx
'use client'; // Error Boundaries는 클라이언트 컴포넌트여야 합니다.

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>대시보드에서 문제가 발생했습니다!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>다시 시도</button>
    </div>
  );
}

 

 

 

6. global-error.tsx : 루트 레이아웃에서 발생하는 오류 처리

  • 역할: 애플리케이션의 최상위 (app/layout.tsx)에서 발생하는 오류를 포함한 모든 예측 불가능한 오류를 처리하는 최종 오류 폴백(fallback)입니다.
  • 특징:
    • app/global-error.tsx 파일은 반드시 'use client'를 선언해야 합니다.
    • app/layout.tsx마저도 오류가 발생하여 렌더링되지 못하는 경우를 대비하기 때문에, 이 파일은 자체적으로 <html>과 <body> 태그를 포함하여 독립적인 오류 페이지를 렌더링해야 합니다.
    • 이 파일을 정의하면, error.tsx 파일이 잡지 못하는 Root Layout의 오류도 처리할 수 있습니다.
  • 예시: app/global-error.tsx를 사용하여 앱 전체에 걸친 심각한 오류 발생 시 "서비스에 문제가 발생했습니다"와 같은 일반적인 메시지를 보여줄 때 사용합니다.
// app/global-error.tsx
'use client'; // 전역 오류 핸들러는 클라이언트 컴포넌트여야 합니다.

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h1>전역 오류 발생!</h1>
        <p>죄송합니다. 서비스에 문제가 발생했습니다.</p>
        <button onClick={() => reset()}>새로고침</button>
      </body>
    </html>
  );
}

 

 

7. route.ts: API 엔드포인트 정의

  • 역할: 백엔드 API 엔드포인트를 정의합니다. 클라이언트 측에서 직접 백엔드 API를 호출하는 대신, Next.js 앱 내에서 서버 측 코드를 실행하여 데이터를 처리하고 응답을 반환할 수 있습니다. 쉽게 말하면, route.ts 파일에서 정의해 놓은 백엔드 엔드포인트에 클라이언트(브라우저)로부터 요청이 들어오면, route.ts 파일 내부에 정의된 GET, POST 등의 HTTP 메서드에 해당하는 함수 로직을 "서버"에서 실행하여 해당 요청을 처리하겠다는 의미입니다.
  • 특징:
    • HTTP 메서드(GET, POST, PUT, DELETE 등)에 해당하는 함수를 export 합니다.(함수명 고정입니다.)
    • 서버에서만 실행되며, 클라이언트 번들에 포함되지 않습니다.
    • 데이터베이스 접근, 외부 API 호출, 파일 시스템 접근 등 서버 측 작업을 수행할 수 있습니다.
  • 예시: app/api/users/route.ts는 /api/users 경로로 GET과 POST 요청이 들어오면 아래의 로직대로 "서버"에서 처리를 한다는 것을 읠미합니다.
// app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // 실제 DB에서 사용자 목록을 가져오는 로직
  const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const data = await request.json();
  // 데이터베이스에 새 사용자 추가 로직
  return NextResponse.json({ message: 'User created', data }, { status: 201 });
}

 

8. template.tsx : 페이지 전환 시 재렌더링되는 레이아웃

  • 역할: layout.tsx와 비슷하게 하위 자식들을 감싸는 UI를 제공하지만, template.tsx는 자식 라우트가 변경될 때마다 항상 새로운 인스턴스로 재렌더링됩니다. 즉, template.tsx 내의 상태(state)나 DOM 요소가 초기화됩니다. layout.ts와 비슷하지만, 변하지 않고 지속되어야 하는 UI는 layout.tsx에, 페이지가 바뀔 때마다 초기화되거나 애니메이션이 적용되어야 하는 UI는 template.tsx에 넣는다고 생각하시면 좋을 것 같습니다.
  • 특징:
    • layout.tsx와는 달리, 페이지 전환 시 내부 상태를 유지하지 않고 항상 마운트 해제 후 다시 마운트됩니다.
    • 특정 애니메이션 효과를 적용하거나, 자식 라우트가 변경될 때마다 초기화되어야 하는 로직이 있을 때 유용합니다.
  • 예시: 페이지 전환 시 특정 CSS 애니메이션이 항상 처음부터 다시 시작되도록 하고 싶을 때 사용합니다. 또한, useEffect 훅 을 재실행해 자식 페이지가 바뀔 때마다 template.tsx 내부의 useEffect 훅이 다시 실행되어 특정 로직을 재트리거해야 하는 경우 쓰면 좋겠죠?
// app/articles/template.tsx
import { motion } from 'framer-motion'; // 예시: 애니메이션 라이브러리

export default function ArticlesTemplate({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.5 }}
    >
      <h2>기사 목록</h2>
      {children} {/* 하위 페이지가 바뀔 때마다 이 div 전체가 다시 렌더링됩니다. */}
    </motion.div>
  );
}

 

9. default.tsx: 병렬 라우트의 대체 UI

  • 역할: 주로 병렬 라우트(Parallel Routes)와 함께 사용되어, 슬롯(slot)에 일치하는 라우트가 없을 때 렌더링될 대체(fallback) UI를 정의합니다. 병렬  라우트는 다음에 더 자세히 다뤄볼게요.
  • 특징:
    • 병렬 라우트는 @team이나 @analytics와 같이 @ 기호로 시작하는 폴더를 사용하여 여러 개의 독립적인 라우트를 동시에 렌더링할 수 있게 하는 고급 기능입니다.
    • 특정 조건(예: 인증되지 않은 사용자)에 따라 슬롯의 콘텐츠를 숨기거나 다르게 보여줄 때 default.tsx를 통해 유연하게 처리할 수 있습니다.
  • 예시: 대시보드에서 @team이라는 병렬 라우트 슬롯이 있을 때, 사용자가 팀에 소속되어 있지 않으면 app/@team/default.tsx에 정의된 "팀 정보를 볼 수 없습니다"와 같은 메시지를 표시할 수 있습니다.
// app/@team/default.tsx (병렬 라우트 슬롯)
export default function TeamDefault() {
  return (
    <div style={{ border: '1px solid gray', padding: '10px' }}>
      <h3>팀 정보를 찾을 수 없습니다.</h3>
      <p>팀에 가입하거나 생성해 보세요.</p>
    </div>
  );
}

이상입니다.

다음에 더 자세한 내용으로 찾아뵙겠습니다.

감사합니다.

 

728x90
반응형