Next.js 앱 라우터의 파일 기반 라우팅에 대한 심층 가이드로 그 강력한 기능을 활용해 보세요. 애플리케이션 구조화, 동적 라우트 생성, 레이아웃 처리 방법 등을 배워보세요.
Next.js 앱 라우터: 파일 기반 라우팅 종합 가이드
Next.js 13에서 도입되어 이후 버전에서 표준이 된 Next.js 앱 라우터는 우리가 애플리케이션을 구조화하고 탐색하는 방식을 혁신합니다. 이는 개발을 단순화하고 성능을 개선하며 전반적인 개발자 경험을 향상시키는 강력하고 직관적인 파일 기반 라우팅 시스템을 도입합니다. 이 종합 가이드에서는 앱 라우터의 파일 기반 라우팅을 심층적으로 다루어, 여러분이 견고하고 확장 가능한 Next.js 애플리케이션을 구축하는 데 필요한 지식과 기술을 제공할 것입니다.
파일 기반 라우팅이란 무엇인가요?
파일 기반 라우팅은 애플리케이션의 라우트 구조가 파일 및 디렉토리의 구성에 의해 직접 결정되는 라우팅 시스템입니다. Next.js 앱 라우터에서는 `app` 디렉토리 내에 파일을 생성하여 라우트를 정의합니다. 각 폴더는 라우트 세그먼트를 나타내며, 해당 폴더 내의 특수 파일들은 그 라우트 세그먼트가 어떻게 처리될지를 정의합니다. 이 접근 방식은 여러 가지 이점을 제공합니다:
- 직관적인 구조: 파일 시스템이 애플리케이션의 라우트 구조를 그대로 반영하여 이해하고 탐색하기 쉽습니다.
- 자동 라우팅: Next.js가 파일 구조를 기반으로 라우트를 자동으로 생성하므로 수동 구성이 필요 없습니다.
- 코드 연관성(Collocation): 라우트 핸들러와 UI 컴포넌트가 함께 위치하여 코드 구성과 유지보수성이 향상됩니다.
- 내장 기능: 앱 라우터는 레이아웃, 동적 라우트, 데이터 가져오기 등을 기본적으로 지원하여 복잡한 라우팅 시나리오를 단순화합니다.
앱 라우터 시작하기
앱 라우터를 사용하려면 새로운 Next.js 프로젝트를 생성하거나 기존 프로젝트를 마이그레이션해야 합니다. Next.js 버전 13 이상을 사용하고 있는지 확인하세요.
새 프로젝트 생성:
다음 명령어를 사용하여 앱 라우터가 포함된 새로운 Next.js 프로젝트를 생성할 수 있습니다:
npx create-next-app@latest my-app --example with-app
기존 프로젝트 마이그레이션:
기존 프로젝트를 마이그레이션하려면 `pages` 디렉토리의 페이지들을 `app` 디렉토리로 옮겨야 합니다. 그에 따라 라우팅 로직을 조정해야 할 수도 있습니다. Next.js는 이 과정을 돕기 위해 마이그레이션 가이드를 제공합니다.
파일 기반 라우팅의 핵심 개념
앱 라우터는 라우트가 처리되는 방식을 정의하는 몇 가지 특수 파일과 규칙을 도입합니다:
1. `app` 디렉토리
`app` 디렉토리는 애플리케이션 라우트의 루트입니다. 이 디렉토리 내의 모든 파일과 폴더는 라우트를 생성하는 데 사용됩니다. `app` 디렉토리 외부에 있는 것들(마이그레이션 중인 경우 `pages` 디렉토리 등)은 앱 라우터에 의해 무시됩니다.
2. `page.js` 파일
`page.js` (또는 `page.jsx`, `page.ts`, `page.tsx`) 파일은 앱 라우터의 가장 기본적인 부분입니다. 특정 라우트 세그먼트에 대해 렌더링될 UI 컴포넌트를 정의합니다. 직접 접근 가능한 라우트 세그먼트를 만들려면 **필수적인** 파일입니다.
예제:
다음과 같은 파일 구조가 있다고 가정해 봅시다:
app/
about/
page.js
사용자가 `/about`으로 이동하면 `app/about/page.js`에서 내보낸 컴포넌트가 렌더링됩니다.
// app/about/page.js
import React from 'react';
export default function AboutPage() {
return (
<div>
<h1>회사 소개</h1>
<p>저희 회사에 대해 더 알아보세요.</p>
</div>
);
}
3. `layout.js` 파일
`layout.js` (또는 `layout.jsx`, `layout.ts`, `layout.tsx`) 파일은 라우트 세그먼트 내의 여러 페이지에 걸쳐 공유되는 UI를 정의합니다. 레이아웃은 일관된 헤더, 푸터, 사이드바 및 여러 페이지에 존재해야 하는 기타 요소를 만드는 데 유용합니다.
예제:
`/about` 페이지와 가상의 `/about/team` 페이지 모두에 헤더를 추가하고 싶다고 가정해 봅시다. `app/about` 디렉토리에 `layout.js` 파일을 만들 수 있습니다:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>우리 회사에 대하여</h1>
</header>
<main>{children}</main>
</div>
);
}
`children` prop은 동일한 디렉토리나 중첩된 디렉토리의 `page.js` 파일에 의해 렌더링되는 UI로 대체됩니다.
4. `template.js` 파일
`template.js` 파일은 `layout.js`와 유사하지만, 각 자식 라우트에 대해 컴포넌트의 새 인스턴스를 생성합니다. 이는 자식 라우트 간 이동 시 컴포넌트 상태를 유지하거나 재렌더링을 방지하려는 시나리오에 유용합니다. 레이아웃과 달리 템플릿은 탐색 시 재렌더링됩니다. 템플릿을 사용하는 것은 탐색 시 요소를 애니메이션화하는 데 좋습니다.
예제:
// app/template.js
'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<main>
<p>템플릿: {count}</p>
<button onClick={() => setCount(count + 1)}>템플릿 업데이트</button>
{children}
</main>
)
}
5. `loading.js` 파일
`loading.js` (또는 `loading.jsx`, `loading.ts`, `loading.tsx`) 파일을 사용하면 라우트 세그먼트가 로딩되는 동안 표시될 로딩 UI를 만들 수 있습니다. 이는 데이터를 가져오거나 다른 비동기 작업을 수행할 때 더 나은 사용자 경험을 제공하는 데 유용합니다.
예제:
// app/about/loading.js
import React from 'react';
export default function Loading() {
return <p>회사 정보를 로딩 중입니다...</p>;
}
사용자가 `/about`으로 이동하면 `page.js` 컴포넌트가 완전히 렌더링될 때까지 `Loading` 컴포넌트가 표시됩니다.
6. `error.js` 파일
`error.js` (또는 `error.jsx`, `error.ts`, `error.tsx`) 파일을 사용하면 라우트 세그먼트 내에서 오류가 발생했을 때 표시될 사용자 정의 오류 UI를 만들 수 있습니다. 이는 더 사용자 친화적인 오류 메시지를 제공하고 전체 애플리케이션이 충돌하는 것을 방지하는 데 유용합니다.
예제:
// app/about/error.js
'use client'
import React from 'react';
export default function Error({ error, reset }) {
return (
<div>
<h2>오류가 발생했습니다!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>다시 시도</button>
</div>
);
}
`/about` 페이지를 렌더링하는 동안 오류가 발생하면 `Error` 컴포넌트가 표시됩니다. `error` prop에는 오류에 대한 정보가 포함되어 있으며, `reset` 함수를 통해 사용자는 페이지를 다시 로드하려고 시도할 수 있습니다.
7. 라우트 그룹
라우트 그룹 `(groupName)`을 사용하면 URL 구조에 영향을 주지 않고 라우트를 구성할 수 있습니다. 폴더 이름을 괄호로 묶어 생성합니다. 이는 레이아웃과 공유 컴포넌트를 구성하는 데 특히 유용합니다.
예제:
app/
(marketing)/
about/
page.js
contact/
page.js
(shop)/
products/
page.js
이 예제에서 `about`과 `contact` 페이지는 `marketing` 그룹 아래에 그룹화되고 `products` 페이지는 `shop` 그룹 아래에 있습니다. URL은 각각 `/about`, `/contact`, `/products`로 유지됩니다.
8. 동적 라우트
동적 라우트를 사용하면 가변 세그먼트가 있는 라우트를 생성할 수 있습니다. 이는 데이터베이스나 API에서 가져온 데이터를 기반으로 콘텐츠를 표시하는 데 유용합니다. 동적 라우트 세그먼트는 세그먼트 이름을 대괄호로 묶어 정의합니다(예: `[id]`).
예제:
ID를 기반으로 개별 블로그 게시물을 표시하는 라우트를 만들고 싶다고 가정해 봅시다. 다음과 같은 파일 구조를 만들 수 있습니다:
app/
blog/
[id]/
page.js
`[id]` 세그먼트는 동적 세그먼트입니다. 사용자가 `/blog/123`이나 `/blog/456`과 같은 URL로 이동하면 `app/blog/[id]/page.js`에서 내보낸 컴포넌트가 렌더링됩니다. `id` 매개변수의 값은 컴포넌트의 `params` prop에서 사용할 수 있습니다.
// app/blog/[id]/page.js
import React from 'react';
export default async function BlogPost({ params }) {
const { id } = params;
// 주어진 ID로 블로그 게시물 데이터 가져오기
const post = await fetchBlogPost(id);
if (!post) {
return <p>블로그 게시물을 찾을 수 없습니다.</p>;
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
async function fetchBlogPost(id) {
// 데이터베이스나 API에서 데이터 가져오기 시뮬레이션
return new Promise((resolve) => {
setTimeout(() => {
const posts = {
'123': { title: '나의 첫 블로그 게시물', content: '이것은 나의 첫 블로그 게시물의 내용입니다.' },
'456': { title: '또 다른 블로그 게시물', content: '이것은 좀 더 흥미로운 내용입니다.' },
};
resolve(posts[id] || null);
}, 500);
});
}
라우트에서 여러 동적 세그먼트를 사용할 수도 있습니다. 예를 들어, `/blog/[category]/[id]`와 같은 라우트를 가질 수 있습니다.
9. 캐치올(Catch-all) 세그먼트
캐치올 세그먼트를 사용하면 임의의 수의 세그먼트와 일치하는 라우트를 만들 수 있습니다. 이는 URL 구조가 사용자에 의해 결정되는 CMS와 같은 시나리오에 유용합니다. 캐치올 세그먼트는 세그먼트 이름 앞에 점 세 개를 추가하여 정의합니다(예: `[...slug]`).
예제:
app/
docs/
[...slug]/
page.js
`[...slug]` 세그먼트는 `/docs` 뒤의 모든 세그먼트와 일치합니다. 예를 들어, `/docs/getting-started`, `/docs/api/users`, `/docs/advanced/configuration`과 일치합니다. `slug` 매개변수의 값은 일치하는 세그먼트를 포함하는 배열이 됩니다.
// app/docs/[...slug]/page.js
import React from 'react';
export default function DocsPage({ params }) {
const { slug } = params;
return (
<div>
<h1>문서</h1>
<p>슬러그: {slug ? slug.join('/') : '슬러그 없음'}</p>
</div>
);
}
선택적 캐치올 세그먼트는 세그먼트 이름을 이중 대괄호 `[[...slug]]`로 묶어 만들 수 있습니다. 이렇게 하면 라우트 세그먼트가 선택 사항이 됩니다. 예제:
app/
blog/
[[...slug]]/
page.js
이 설정은 `/blog`와 `/blog/any/number/of/segments` 모두에서 page.js 컴포넌트를 렌더링합니다.
10. 병렬 라우트
병렬 라우트를 사용하면 동일한 레이아웃에 하나 이상의 페이지를 동시에 렌더링할 수 있습니다. 이는 대시보드와 같이 페이지의 다른 섹션을 독립적으로 로드할 수 있는 복잡한 레이아웃에 특히 유용합니다. 병렬 라우트는 `@` 기호 뒤에 슬롯 이름을 붙여 정의합니다(예: `@sidebar`, `@main`).
예제:
app/
@sidebar/
page.js // 사이드바 콘텐츠
@main/
page.js // 메인 섹션 콘텐츠
default.js // 필수: 병렬 라우트의 기본 레이아웃 정의
`default.js` 파일은 병렬 라우트를 사용할 때 필요합니다. 다른 슬롯들이 어떻게 결합되어 최종 레이아웃을 만드는지 정의합니다.
// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
{sidebar}
</aside>
<main style={{ flex: 1, padding: '20px' }}>
{main}
</main>
</div>
);
}
11. 인터셉팅 라우트
인터셉팅 라우트를 사용하면 현재 레이아웃 내에서 애플리케이션의 다른 부분에서 라우트를 로드할 수 있습니다. 이는 모달, 이미지 갤러리 및 기존 페이지 콘텐츠 위에 나타나야 하는 기타 UI 요소를 만드는 데 유용합니다. 인터셉팅 라우트는 `(..)` 구문을 사용하여 정의되며, 이는 인터셉트된 라우트를 찾기 위해 디렉토리 트리에서 얼마나 위로 올라갈지를 나타냅니다.
예제:
app/
(.)photos/
[id]/
page.js // 인터셉트된 라우트
feed/
page.js // 사진 모달이 표시되는 페이지
이 예제에서 사용자가 `/feed` 페이지에서 사진을 클릭하면 `app/(.)photos/[id]/page.js` 라우트가 인터셉트되어 `/feed` 페이지 위에 모달로 표시됩니다. `(.)` 구문은 Next.js에게 한 단계 위(`app` 디렉토리)로 올라가 `photos/[id]` 라우트를 찾도록 지시합니다.
앱 라우터로 데이터 가져오기
앱 라우터는 서버 컴포넌트와 클라이언트 컴포넌트를 사용하여 데이터 가져오기를 기본적으로 지원합니다. 서버 컴포넌트는 서버에서 렌더링되고 클라이언트 컴포넌트는 클라이언트에서 렌더링됩니다. 이를 통해 각 컴포넌트의 요구 사항에 따라 최상의 접근 방식을 선택할 수 있습니다.
서버 컴포넌트
서버 컴포넌트는 앱 라우터의 기본값입니다. 별도의 API 라우트 없이 컴포넌트에서 직접 데이터를 가져올 수 있습니다. 이를 통해 성능을 개선하고 코드를 단순화할 수 있습니다.
예제:
// app/products/page.js
import React from 'react';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div>
<h1>제품</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
async function fetchProducts() {
// 데이터베이스나 API에서 데이터 가져오기 시뮬레이션
return new Promise((resolve) => {
setTimeout(() => {
const products = [
{ id: 1, name: '제품 A' },
{ id: 2, name: '제품 B' },
{ id: 3, name: '제품 C' },
];
resolve(products);
}, 500);
});
}
이 예제에서는 `fetchProducts` 함수가 `ProductsPage` 컴포넌트 내에서 직접 호출됩니다. 컴포넌트는 서버에서 렌더링되고, HTML이 클라이언트로 전송되기 전에 데이터가 가져와집니다.
클라이언트 컴포넌트
클라이언트 컴포넌트는 클라이언트에서 렌더링되며 이벤트 리스너, 상태 및 브라우저 API와 같은 클라이언트 측 기능을 사용할 수 있습니다. 클라이언트 컴포넌트를 사용하려면 파일 상단에 `'use client'` 지시문을 추가해야 합니다.
예제:
// app/counter/page.js
'use client'
import React, { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>카운터</h1>
<p>횟수: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
이 예제에서 `CounterPage` 컴포넌트는 `useState` 훅을 사용하기 때문에 클라이언트 컴포넌트입니다. `'use client'` 지시문은 Next.js에 이 컴포넌트를 클라이언트에서 렌더링하도록 지시합니다.
고급 라우팅 기술
앱 라우터는 복잡하고 정교한 애플리케이션을 만드는 데 사용할 수 있는 몇 가지 고급 라우팅 기술을 제공합니다.
1. 라우트 핸들러
라우트 핸들러를 사용하면 `app` 디렉토리 내에 API 엔드포인트를 생성할 수 있습니다. 이렇게 하면 별도의 `pages/api` 디렉토리가 필요하지 않습니다. 라우트 핸들러는 `route.js` (또는 `route.ts`)라는 이름의 파일에 정의되며, 다른 HTTP 메서드(예: `GET`, `POST`, `PUT`, `DELETE`)를 처리하는 함수를 내보냅니다.
예제:
// app/api/users/route.js
import { NextResponse } from 'next/server'
export async function GET(request) {
// 데이터베이스에서 사용자 가져오기 시뮬레이션
const users = [
{ id: 1, name: '홍길동' },
{ id: 2, name: '김영희' },
];
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json()
console.log('수신된 데이터:', body)
return NextResponse.json({ message: '사용자가 생성되었습니다' }, { status: 201 })
}
이 예제는 `/api/users`에서 `GET` 및 `POST` 요청을 모두 처리하는 라우트 핸들러를 정의합니다. `GET` 함수는 사용자 목록을 반환하고, `POST` 함수는 새 사용자를 생성합니다.
2. 다중 레이아웃을 가진 라우트 그룹
라우트 그룹을 레이아웃과 결합하여 애플리케이션의 다른 섹션에 대해 다른 레이아웃을 만들 수 있습니다. 이는 사이트의 다른 부분에 대해 다른 헤더나 사이드바를 갖고 싶을 때 유용합니다.
예제:
app/
(marketing)/
layout.js // 마케팅 레이아웃
about/
page.js
contact/
page.js
(admin)/
layout.js // 관리자 레이아웃
dashboard/
page.js
이 예제에서 `about`과 `contact` 페이지는 `marketing` 레이아웃을 사용하고, `dashboard` 페이지는 `admin` 레이아웃을 사용합니다.
3. 미들웨어
미들웨어를 사용하면 요청이 애플리케이션에 의해 처리되기 전에 코드를 실행할 수 있습니다. 이는 인증, 권한 부여, 로깅 및 사용자의 위치나 장치에 따라 사용자를 리디렉션하는 등의 작업에 유용합니다.
미들웨어는 프로젝트의 루트에 있는 `middleware.js` (또는 `middleware.ts`) 파일에 정의됩니다.
예제:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// 사용자가 인증되었는지 확인
const isAuthenticated = false; // 실제 인증 로직으로 교체하세요
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// 자세한 내용은 아래 "경로 매칭"을 참조하세요
export const config = {
matcher: '/admin/:path*',
}
이 예제는 사용자가 `/admin` 아래의 모든 라우트에 접근하기 전에 인증되었는지 확인하는 미들웨어를 정의합니다. 사용자가 인증되지 않은 경우 `/login` 페이지로 리디렉션됩니다.
파일 기반 라우팅을 위한 모범 사례
앱 라우터의 파일 기반 라우팅 시스템을 최대한 활용하려면 다음 모범 사례를 고려하세요:
- 파일 구조를 체계적으로 유지하세요: 의미 있는 폴더 이름을 사용하고 관련 파일을 함께 그룹화하세요.
- 공유 UI에 레이아웃을 사용하세요: 여러 페이지에서 공유되는 헤더, 푸터, 사이드바 및 기타 요소에 대해 레이아웃을 만드세요.
- 로딩 UI를 사용하세요: 데이터를 가져오거나 다른 비동기 작업을 수행하는 라우트에 대해 로딩 UI를 제공하세요.
- 오류를 정상적으로 처리하세요: 오류 발생 시 더 나은 사용자 경험을 제공하기 위해 사용자 정의 오류 UI를 만드세요.
- 구성을 위해 라우트 그룹을 사용하세요: URL 구조에 영향을 주지 않고 라우트를 구성하기 위해 라우트 그룹을 사용하세요.
- 성능을 위해 서버 컴포넌트를 활용하세요: 서버에서 데이터를 가져오고 UI를 렌더링하여 성능과 SEO를 향상시키기 위해 서버 컴포넌트를 사용하세요.
- 필요할 때 클라이언트 컴포넌트를 사용하세요: 이벤트 리스너, 상태 및 브라우저 API와 같은 클라이언트 측 기능이 필요할 때 클라이언트 컴포넌트를 사용하세요.
Next.js 앱 라우터를 이용한 국제화 예제
Next.js 앱 라우터는 파일 기반 라우팅을 통해 국제화(i18n)를 단순화합니다. 다음은 i18n을 효과적으로 구현하는 방법입니다:
1. 하위 경로 라우팅
하위 경로를 사용하여 로케일(locale)에 따라 라우트를 구성합니다. 예를 들어:
app/
[locale]/
page.tsx // 해당 로케일의 홈 페이지
about/
page.tsx // 해당 로케일의 About 페이지
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';
export default async function HomePage({ params: { locale } }) {
const t = await getTranslations(locale);
return (<h1>{t.home.title}</h1>);
}
// dictionaries.js
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
es: () => import('./dictionaries/es.json').then((module) => module.default),
};
export const getTranslations = async (locale) => {
try {
return dictionaries[locale]() ?? dictionaries.en();
} catch (error) {
console.error(`로케일 ${locale}의 번역을 로드하는 데 실패했습니다`, error);
return dictionaries.en();
}
};
이 설정에서 `[locale]` 동적 라우트 세그먼트는 다른 로케일(예: `/en`, `/es`)을 처리합니다. 번역은 로케일에 따라 동적으로 로드됩니다.
2. 도메인 라우팅
더 고급 접근 방식으로, 각 로케일에 대해 다른 도메인이나 하위 도메인을 사용할 수 있습니다. 이는 종종 호스팅 제공업체와의 추가 구성이 필요합니다.
3. 로케일 감지를 위한 미들웨어
미들웨어를 사용하여 사용자의 선호 로케일을 자동으로 감지하고 그에 따라 리디렉션합니다.
// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
let locales = ['en', 'es', 'fr'];
function getLocale(request) {
const negotiatorHeaders = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(languages, locales, 'en'); // "en"을 기본 로케일로 사용
} catch (error) {
console.error("로케일 매칭 오류:", error);
return 'en'; // 매칭 실패 시 영어로 대체
}
}
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
);
}
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
이 미들웨어는 요청된 경로에 로케일 접두사가 있는지 확인합니다. 없는 경우 `Accept-Language` 헤더를 사용하여 사용자의 선호 로케일을 감지하고 적절한 로케일별 경로로 리디렉션합니다. `@formatjs/intl-localematcher` 및 `negotiator`와 같은 라이브러리는 로케일 협상을 처리하는 데 사용됩니다.
Next.js 앱 라우터와 글로벌 접근성
전 세계적으로 접근 가능한 웹 애플리케이션을 만들려면 접근성(a11y) 원칙을 신중하게 고려해야 합니다. Next.js 앱 라우터는 접근성 있는 경험을 구축하기 위한 견고한 기반을 제공하지만, 능력에 관계없이 모든 사람이 애플리케이션을 사용할 수 있도록 모범 사례를 구현하는 것이 중요합니다.
주요 접근성 고려 사항
- 시맨틱 HTML: 콘텐츠를 구조화하기 위해 시맨틱 HTML 요소(예: `<article>`, `<nav>`, `<aside>`, `<main>`)를 사용하세요. 이는 보조 기술에 의미를 제공하고 사용자가 사이트를 더 쉽게 탐색할 수 있도록 돕습니다.
- ARIA 속성: ARIA(Accessible Rich Internet Applications) 속성을 사용하여 사용자 정의 컴포넌트 및 위젯의 접근성을 향상시키세요. ARIA 속성은 요소의 역할, 상태 및 속성에 대한 추가 정보를 보조 기술에 제공합니다.
- 키보드 탐색: 모든 상호작용 요소가 키보드를 통해 접근 가능한지 확인하세요. 사용자는 `Tab` 키를 사용하여 애플리케이션을 탐색하고 `Enter` 또는 `Space` 키를 사용하여 요소와 상호작용할 수 있어야 합니다.
- 색상 대비: 시각 장애가 있는 사용자의 가독성을 보장하기 위해 텍스트와 배경 간에 충분한 색상 대비를 사용하세요. 웹 콘텐츠 접근성 가이드라인(WCAG)은 일반 텍스트의 경우 최소 4.5:1, 큰 텍스트의 경우 3:1의 대비율을 권장합니다.
- 이미지 대체 텍스트: 모든 이미지에 대해 설명적인 대체 텍스트(alt text)를 제공하세요. 대체 텍스트는 스크린 리더가 읽을 수 있는 이미지의 텍스트 대안을 제공합니다.
- 폼 레이블: `<label>` 요소를 사용하여 폼 레이블을 해당 입력 필드와 연결하세요. 이렇게 하면 사용자가 각 필드에 어떤 정보가 필요한지 명확하게 알 수 있습니다.
- 스크린 리더 테스트: 스크린 리더로 애플리케이션을 테스트하여 시각 장애가 있는 사용자에게 접근 가능한지 확인하세요. 인기 있는 스크린 리더로는 NVDA, JAWS, VoiceOver가 있습니다.
Next.js 앱 라우터에서 접근성 구현하기
- Next.js Link 컴포넌트 사용: 탐색을 위해 `<Link>` 컴포넌트를 사용하세요. 프리페칭 및 포커스 관리와 같은 내장된 접근성 기능을 제공합니다.
- 포커스 관리: 페이지 간 이동이나 모달을 열 때 포커스가 적절하게 관리되는지 확인하세요. 포커스는 새 페이지나 모달의 가장 논리적인 요소에 설정되어야 합니다.
- 접근성 있는 사용자 정의 컴포넌트: 사용자 정의 컴포넌트를 만들 때 위에서 설명한 원칙을 따라 접근성을 보장하세요. 시맨틱 HTML, ARIA 속성 및 키보드 탐색을 사용하여 모든 사람이 컴포넌트를 사용할 수 있도록 만드세요.
- 린팅 및 테스트: ESLint와 같은 린팅 도구와 접근성 플러그인을 사용하여 코드에서 잠재적인 접근성 문제를 식별하세요. 또한 자동화된 테스트 도구를 사용하여 애플리케이션의 접근성 위반을 테스트하세요.
결론
Next.js 앱 라우터의 파일 기반 라우팅 시스템은 애플리케이션을 구조화하고 탐색하는 강력하고 직관적인 방법을 제공합니다. 이 가이드에서 설명한 핵심 개념과 모범 사례를 이해함으로써 견고하고 확장 가능하며 유지보수 가능한 Next.js 애플리케이션을 구축할 수 있습니다. 앱 라우터의 다양한 기능을 실험해보고 개발 워크플로를 단순화하고 사용자 경험을 개선하는 방법을 알아보세요.