[NextJS] App Router

Yang Sooho·2025년 4월 12일
post-thumbnail

Next.js는 13버전부터 App Router를 도입하여 라우팅 방식을 대폭 개선.
기존의 페이지 단위 라우팅에서 더 세밀한 제어가 가능해졌고,
서버 컴포넌트, 스트리밍 및 Suspense 등 최신 기술을 자연스럽게 사용할 수 있도록 설계.

이 포스트에서는 Next.js의 App Router를 처음 접하는 개발자를 위해,
라우팅 구조와 각 파일(page.tsx, route.ts 등)의 명확한 역할 및 사용법을 알기 쉽게 정리.

1. App Router란?

  • 기존의 Pages Router와의 차이점
  • 장점: 서버 컴포넌트 지원, 중첩 라우팅, 병렬 라우팅 등

2. 폴더 기본 구조

app/
├── layout.tsx
├── page.tsx
├── loading.tsx
├── error.tsx
├── template.tsx
└── api/
    └── route.ts
  • 폴더와 파일의 구조적 의미 소개

3. 파일별 라우팅 특성 상세 정리

1. page.tsx (기본 페이지 컴포넌트)

  • 페이지 단위의 UI를 렌더링하는 기본 컴포넌트
  • 서버 및 클라이언트 컴포넌트로 구성 가능
    • 'use client'키워드로 클라이언트 구분 가능
  • 페이지 진입점(entry-point) 역할
  • 정적(Static) 및 동적(Dynamic) 라우팅 모두 지원
const Page = () => {
  return (
  	<div> Page </div>  
  );
}

export default TestPage;

2. layout.tsx (레이아웃 컴포넌트)

  • 공통 레이아웃(헤더, 푸터 등)을 정의하고 페이지 간 상태를 유지
  • 페이지 이동 시에도 레이아웃 유지
  • 중첩된(Nested) 레이아웃 구조 가능
const Layout= ({ children }) => {
  return (
    <>
      <header>헤더</header>
      {children}
      <footer>푸터</footer>
    </>
  );
}

export default Layout;

3. route.ts (API 라우팅)

  • 서버 기반 API 엔드포인트를 정의
  • HTTP 메소드(GET, POST, PUT, DELETE) 처리 방법
  • REST API 구현 시 필수
// app/api/users/route.ts

import { NextResponse } from 'next/server';

let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

// GET - 전체 유저 조회
export const GET = async () => {
  return NextResponse.json(users);
}

// POST - 새로운 유저 추가
export const POST = async (request: Request) => {
  const body = await request.json();
  const newUser = {
    id: users.length + 1,
    name: body.name
  };
  users.push(newUser);

  return NextResponse.json(newUser, { status: 201 });
}

// PUT - 유저 이름 수정 (간단히 id=1만 수정한다고 가정)
export const PUT (request: Request) => {
  const body = await request.json();
  const user = users.find(u => u.id === body.id);

  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }

  user.name = body.name;
  return NextResponse.json(user);
}

// DELETE - 유저 삭제 (id 기반 삭제)
export const DELETE = async (request: Request) => {
  const { searchParams } = new URL(request.url);
  const id = parseInt(searchParams.get('id') || '0');

  const index = users.findIndex(u => u.id === id);
  if (index === -1) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }

  const deleted = users.splice(index, 1)[0];
  return NextResponse.json(deleted);
}

4. loading.tsx (로딩 컴포넌트)

  • 페이지 데이터를 로딩 중일 때 사용자에게 표시할 UI를 구성
  • Suspense와 스트리밍 데이터 처리 시 자동으로 렌더링
  • 별도의 로딩 처리 로직이 필요하지 않음
const Loading = () => {
  return <div>Loading...</div> 
}

export default Loading;
  • VS layout.tsx vs template.tsx
항목layout.tsxtemplate.tsx
위치app/경로/layout.tsxapp/경로/template.tsx
재사용 여부유지됨 (경로 변경해도 유지됨)유지 안 됨 (경로 변경 시 새로 마운트됨)
용도공통 UI 구성, 상태 유지페이지 전환 효과, 초기화 목적
적용 대상하위 모든 page.tsx, layout.tsx하위 page.tsx 및 그 하위만 감쌈
상태 유지유지됨안 됨 (컴포넌트 리셋됨)

5. error.tsx (에러 컴포넌트)

  • 페이지에서 발생한 에러를 처리하고 사용자에게 적절한 메시지를 표시
  • 자동으로 에러를 캡처하여 UI로 렌더링 (Error Boundary)
  • 사용자 친화적인 에러 처리 가능
const Error = ({ error, reset }) => {
  return (
    <div>
      <p>에러가 발생했습니다: {error.message}</p>
      <button onClick={reset}>다시 시도</button>
    </div>
  );
}

export default Error;

6. template.tsx (템플릿 컴포넌트)

  • 페이지 전환 시 레이아웃 상태를 유지하지 않고 매번 초기화하여 렌더링
  • 페이지 전환 애니메이션이나 새로 렌더링되는 UI에 적합
  • layout과 달리 상태 유지하지 않음
const Template = ({ children }) => {
  return <div className="transition-opacity animate-fadeIn">{children}</div>;
}

export default Template;

4. 특수 라우팅

  • 동적 라우팅 ([id])
    • URL 경로 일부를 변수처럼 사용하는 방식
    • 파일명에 대괄호를 사용하여 동적 파라미터를 받음.

- URL 예시

  • /posts/1
  • /posts/hello-world

📘 코드 예시

// app/posts/[id]/page.tsx
import { useParams } from 'next/navigation';

const PostPage = () => {
  const params = useParams();
  return <div>Post ID: {params.id}</div>;
}

export default PostPage;
  • 병렬 라우팅 (@folder)
    • 여러 뷰(슬롯)를 동시에 렌더링하고 싶을 때 사용
    • @폴더명으로 병렬적으로 UI를 구성할 수 있음
    • layout.tsx 안에서 <Slot />으로 활용
app/
└── layout.tsx
└── @modal/
    └── page.tsx
└── @main/
    └── page.tsx
// app/layout.tsx

const RootLayout = ({ modal, main}: {modal: React.ReactNode; main: React.ReactNode }) => {
  return (
    <>
      <div>{main}</div>
      <aside>{modal}</aside>
    </>
  );
}

export default RootLayout;
  • 인터셉트 라우팅 ((.) / (..) / (...))
    • 현재 페이지를 유지하면서 다른 경로로 이동한 것처럼 보이게 하는 라우팅
    • 주로 모달, 사이드 패널 등을 라우팅으로 처리할 때 유용
  • 인터셉트 종류와 개념
표현의미가로채는 대상 경로 예시설명
(.)현재 경로 기준/dashboard/(.)modaldashboard 하위에서만 모달 라우트를 가로챔
(..)상위 1단계 경로까지/dashboard/(..)/modaldashboard 외부 경로에서도 modal로 접근 가능
(...)루트 기준, 전체 앱 어디에서든 가로챔/dashboard/(...)/modal어느 경로에서든 modal 라우트를 인터셉트함

예시 구조:

app/
├─ dashboard/
│  ├─ page.tsx
│  └─ (...)/modal/
│     └─ page.tsx
├─ modal/         <-- 일반 경로 (가로채지 않음)
│  └─ page.tsx
├─ layout.tsx
  • (.)modal/

    • /dashboard/modal로 접근했을 때만 가로채짐
    • 그 외 경로에서는 접근 불가 (에러)
  • (..)/modal/

    • /dashboard, /settings, 등 상위 1단계 경로에서도 modal을 가로챔
    • URL은 /dashboard/modal, /settings/modal 등으로 구성 가능
  • (...)/modal/

    • 앱 전체 어디서든 modal 라우트를 가로챔
    • /any/route/modal 로 접근해도 가로채기 적용됨
    • 가장 범용적인 방식
  • 인터셉트 라우팅(intercepted routing)으로 설정한 디렉토리(ex - modal)는 Slot으로 동작.

//app/dashboard/(...)/modal/page.tsx
'use client'

import { useParams, useRouter } from 'next/navigation';

const TestModal = () => {
  const params = useParams();
  const router = useRouter();

  const closeModal = () => {
    router.back(); // 뒤로 가기 (이전 경로로 복귀)
  };

  return (
    <div
      style={{
        position: 'fixed',
        inset: 0,
        backgroundColor: 'rgba(0,0,0,0.6)',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        zIndex: 100,
      }}
    >
      <div
        style={{
          backgroundColor: '#fff',
          padding: '2rem',
          borderRadius: '8px',
          width: '400px',
          textAlign: 'center',
        }}
      >
        <h2>모달 테스트</h2>
        <p>Modal ID: {params.id}</p>
        <button className={'bg-amber-400 rounded-2xl p-2 cursor-pointer'} onClick={closeModal}>닫기</button>
      </div>
    </div>
  );
}

export default TestModal;
//app/layout.tsx - modal 추가
const RootLayout = async ({children, modal} :{children: React.ReactNode, modal: React.ReactNode}) => {

  const systemMode = await getSystemMode()
  const direction = 'ltr'
  const session = await getServerSession(authOptions);

  return (
    <html id='__next' lang='en' dir={direction} suppressHydrationWarning>
      <body className='flex is-full min-bs-full flex-auto flex-col'>
        <InitColorSchemeScript attribute='data' defaultMode={systemMode}/>
        <SessionProvider session={session}>
          {children}
          {modal}
        </SessionProvider>
      </body>
    </html>
  )
}

export default RootLayout

5. 정리

파일명라우팅 역할특징
page.tsx페이지 컴포넌트 라우팅페이지 UI 렌더링, entry-point
layout.tsx공통 레이아웃 구성상태 유지, Nested Layout 가능
route.tsAPI 라우트 엔드포인트HTTP 요청 처리
loading.tsx로딩 상태 UI 표시Suspense 활용한 비동기 처리 지원
error.tsx에러 UI 표시 및 처리에러 Boundary 역할
template.tsx페이지 전환 시 상태 초기화페이지 전환 효과(애니메이션 등)
profile
개발 한웅큼 메모 한 스푼

0개의 댓글