Next.js가 2022년 10월 version 13을 발표하면서 몇 가지 기능을 새로이 소개하였다. 오늘은 그 중에서도 가장 큰 변화라고 할 수 있었던 App Router에 대해 (최대한)간단히 정리해 보고자 했다.
이 새로운 라우팅 시스템이 Next.js 블로그에서 처음 소개될 때에는 app
Directory(beta) 라는 명칭으로 소개가 되었다. Next.js 팀은 기존 라우팅의 한계점과 더 나은 경험을 제공하기 위해, 또한 프레임워크를 React의 발전 방향과 나란히 하고자(align with) 하는 목적으로 새로운 라우팅 시스템을 구축했다고 한다.
이후 version 13.4가 되어서야 "App Router"라는 이름으로 stable 버전이 발표 되기도 했다.
참고로, App Router가 등장하였다고 해서 기존의 Page Router가 더 이상 지원되지 않는 것은 아니다.
한 프로젝트에 둘이 함께 공존하더라도 작동은 정상적으로 이루어지며 Page Router → App Router 로의 점진적인 마이그레이션 또한 가능하기 때문에, App Router의 특정 기능이 필요한 부분만 먼저 우선적으로 마이그레이션 해볼 수도 있을 것 같다. (다만 App Router가 Page Router보다 높은 우선 순위를 갖는다)
그럼 지금부터는 이 새로운 라우팅 시스템에 대한 소개와 사용 방법에 대해 알아 보도록 하자.
App Router는 React의 최신 기능을 사용해 애플리케이션을 구축할 수 있는 새로운 모델을 도입한다.
App Router가 등장하기 이전, Next.js는 그간 여러 커뮤니티 채널을 통해 현 라우팅 시스템(Page Router)의 피드백을 수집하였고 이 과정에서 다음과 같은 한계점이 존재했다고 한다.
기존의 라우팅 시스템도 Next.js 초창기부터 잘 작동해 왔지만, 미래의 React와 일치하는 방향으로 개선해 나가고자 하는 목적으로 이 새로운 라우팅 시스템을 구축하려 했다고 한다.
그리하여 이러한 요구 사항을 충족시키기 위해 App Router가 도입되었다. 이에 따라 사용자 입장에서나 개발자 입장에서나 더 나은 라우팅 경험을 제공하고 우수한 성능과 풍부한 기능을 가진 웹 애플리케이션을 구축하는 데 도움을 얻을 수 있을 것으로 기대한다.
App Router는 여러 폴더들을 기반으로 라우터가 구성되는 파일 시스템 기반 라우터이다. 각 폴더가 URL 세그먼트에 매핑된다.
폴더
: 라우트를 정의하는 데 사용된다. root 폴더부터 시작해 페이지 파일이 포함된 leaf 폴더까지 계층 구조를 따라 경로가 정의된다.파일
: 라우트 세그먼트(URL Path에 있는 각 부분)에 표시되는 UI를 만든다.폴더를 서로 중첩시켜 Nested Routes를 구성할 수 있다.
예를 들어 app
폴더 안에 dashboard
폴더가 있고, 그 안에 invoices
폴더가 또 있다면 "/dashboard/invoices" 라는 경로가 만들어지게 된다.
/
: Root segmentdashboard
: Segmentinvoices
: Leaf Segment이렇게 중첩된 라우트를 만드려면 경로로 설정할 각 Segment로 폴더를 구성하고, Leaf Segment에 page.js
파일을 추가하면 된다. 그러면 브라우저에서는 "/dashboard/invoices/" 경로를 통해 접근이 가능해지는 형태이다.
중첩 라우팅 구조에서 특정 동작에 따라 UI를 생성할 수 있는 특수 파일 세트가 존재한다. 위에서 잠깐 page.js
파일에 대해 언급했는데, 이 파일은 라우트에 액세스하려면 꼭 필요한 파일이다. 이외에도 각기 다른 역할을 가진 파일들이 존재한다.
간단히 말하자면, 각 라우트에 해당하는 각각의 폴더 안에 아래에서 지원되는 파일명을 따르는 파일을 만들어, 라우트 별로 고유한 페이지
, 여러 페이지 간 공유되는 UI
, 404 페이지
등을 정의할 수 있는 기능 정도로 생각할 수 있을 것 같다.
대표적인 목록은 다음과 같다. (파일명은 .jsx
, .tsx
로도 대체 가능하다)
notFound
함수가 thrown 되었을 때의 UIRequest
및 Response
API를 사용해 각 라우트에 대한 custom request handler를 정의하기 위한 파일이 파일명 규칙에 따라 라우트를 구성하면 개발 프로세스를 단순화 하면서도 상황에 따른 유연한 애플리케이션 구조를 설계하는 데 도움이 될 것 같다.
다음은 App Router와 함께 동작하는 기능들이다.
레이아웃은 UI를 공유하는 것 외에도 레이아웃이 제공하는 이점은 네비게이팅이 일어나는 동안 여러 페이지에 걸쳐 상태를 보존하고, 비용이 많이 드는 Re-rendering을 방지하며, 그 외 복잡한 인터페이스도 쉽게 배치하도록 도와준다는 것이다.
또한 중첩 라우팅 구조 내에서 레이아웃 역시 서로 중첩이 된다.(참고: Nesting Layouts)
기본적으로 layout.js
파일에서 리액트 컴포넌트를 export 하는 것으로 레이아웃을 정의가 가능하다. 레이아웃은 껍데기 와도 같기 때문에, 그 속을 채울 수 있는 children
props가 제공되어야 한다.
// app/dashboard/layout.js
export default function DashboardLayout({
children // page 혹은 nested layout
}) {
return (
<section>
<nav>{/* 라우트들이 공유 할 UI를 작성.. */}</nav>
{children}
</section>
)
}
이들은 각각이 가진 장점과 특성에 맞게 서버와 클라이언트를 사용하므로 일명 하이브리드 웹 앱을 만들 수 있도록 해주며, 빠르고 상호 작용성이 뛰어난 앱을 구축하도록 도움을 주는 도구이다.
그 중에서도 서버 컴포넌트는 클라이언트로 전송되는 JS의 양을 줄여, 초기 페이지 로드 속도를 높이는 데 특화되어 있다.
app
디렉터리 내부의 파일이 렌더링 될 때, 기본 동작으로 서버에서 React Server Component로 렌더링 된다는 점이다. (출처 - Next.js Blog: layouts-rfc)
따라서 새로운 라우팅 시스템의 내부에서는 자연스레 React 18의 Streaming, Suspense 및 Transitions과 같은 기능이 활용 가능해지게 된다.
참고로, 이전 버전과의 호환성을 위해 서버 컴포넌트는 app
디렉터리나 사용자 고유 디렉터리에서는 동작하지만, page
디렉터리에서는 동작하지 않는다.
페이지의 HTML을 더 작은 청크로 분할하고, 준비가 완료되면 클라이언트로 해당 청크를 보낸다. 그 결과 사용자는 모든 데이터가 로드되기까지 기다리지 않고 페이지의 일부를 더 빠르게 보고 상호 작용 할 수 있게 된다.
초기 페이지 로딩 성능 뿐 아니라, 전체 라우트의 렌더링을 차단(blocking)하는 느린 데이터 페치 속도를 가지고 있는 UI 개선에 도움을 주며 더 나은 사용자 경험을 제공할 수 있다.
loading.js
를 활용하여 의미 있는 로딩 UI 구축 가능예를 들어 라우트 세그먼트의 컨텐츠가 로드되는 동안, 서버에서 스켈레톤/스피너 등을 이용해 즉시 로드 중인 상태임을 알리는 대체 UI를 표시하고, 렌더링이 완료되면 로드된 컨텐츠로 자동으로 교체된다.
// app/dashboard/page.js
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
이렇게 스트리밍을 활용하면 우선순위가 높거나(Ex. 제품 정보) 데이터에 의존하지 않는(Ex. 레이아웃) 컴포넌트를 먼저 내보낼 수 있다. 우선순위가 낮은 컴포넌트(Ex. 리뷰, 연관 제품)는 데이터를 페치한 뒤 동일한 서버 요청에서 보내진다. (코드 예제 - app-router.vercel.app/streaming 에서 확인)
이제 app
디렉토리의 레이아웃, 페이지 및 컴포넌트 내에서 확장된 fetch API를 통해 데이터 페치를 수행할 수 있다. 페이지 단에서의 데이터 페치만 가능했던 Page Router와는 다르게, App Router에서는 컴포넌트 단에서도 데이터를 페치할 수 있도록 변경되었다.
Page Router에서 사용되던 getServerSideProps
등과 같은 Data fetching 메서드는 App Router에서 이제 새로운 API로 대체되는데, 데이터를 가져오기 위해 아래 4가지 방식 중 하나를 따를 수 있다.
특히 확장된 Native fetch API에 따라, fetch 요청에는 자동으로 중복이 제거되고, 요청이 캐시되고, 유효성을 재검증하는 방법 또한 제공된다.
가장 먼저 App Router를 채택하는 Next.js 예제 프로젝트를 생성해 보자.
npx create-next-app \
nextjs-dashboard \
--example "https://github.com/vercel/next-learn/tree/main/dashboard/final-example"
이후 구성된 프로젝트 구조를 살펴 보면, page/
디렉터리가 있었던 이전 Next.js 12 버전에서와는 다르게 여기서는 app/
디렉터리가 눈에 띄는 것을 볼 수 있다.
이렇듯 App Router 를 사용한다면 해당 디렉터리에서 주로 작업하게 된다.
nextjs-dashboard/
├── app/
│ ├── dashboard/ # 중첩 라우팅
│ ├── layout.tsx
│ ├── lib/
│ ├── login/ # 중첩 라우팅
│ ├── page.tsx
│ └── ui/
├── next.config.js
├── public/
│ └── # 정적 애셋들..
└── # 그 외 기타..
app/
: 애플리케이션의 모든 라우트, 컴포넌트, 로직이 포함되는 곳. 파일 컨벤션에 따라 작성된 layout.tsx와 page.tsx 도 확인할 수 있다.public/
: 이미지와 같은 애플리케이션의 정적 애셋이 위치하는 곳next.config.js
: Next.js 구성 파일그 외 Next.js 프로젝트 구조에서 지원되는 항목에 대해서는 Next.js Project Structure에서 상세히 확인할 수 있다.
앞서 Next.js에는 파일 시스템 기반 라우팅을 적용되어, 폴더와 파일을 적절히 배치해 라우트를 구성할 수 있다고 언급하였다.
우리가 원하는 대로 라우트를 구성하기 위해서는, 1) 라우트를 생성하고 2) 해당 라우트에 접근하면 보여 줄 UI를 생성하는 것이라는 일련의 과정이 필요하다.
이를 위해 특수 파일인 page.js 파일(과 layout.js 파일)을 각 폴더마다 배치함으로써, 라우트 마다 별도의 UI를 제공할 수 있다.
예를 들어 이 프로젝트에서 app
디렉터리만 떼어놓고 살펴보면 아래와 같은 구조로 되어 있는데,
app/
├── dashboard/
│ ├── (overview)/
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── customers/
│ │ └── page.tsx
│ ├── invoices/
│ │ ├── [id]/edit/
│ │ │ ├── not-found.tsx
│ │ │ └── page.tsx
│ │ ├── create/
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── error.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── layout.tsx
├── lib/
├── login/
│ └── page.tsx
├── page.tsx
└── ui/
그런 경우 홈페이지에서 루트 경로(/)에 접근했을 때 app/page.tsx
와 app/layout.tsx
에 해당하는 UI가 보여지게 된다.
다음으로, /dashboard/customers 에 접근하면 app/dashboard/customers/pages.tsx
에 맞는 UI가 표시되고 이런 방식으로 중첩된 라우트 또한 구현이 가능하다.
또, app/dashboard/
아래에 있는 특수 파일 layout.tsx는 그 하위 수준에 있는 모든 페이지가 공유하는 UI로서 사용된다.
하지만 /lib 나 /ui 에 접근한다면 404에러가 보여지게 될 것이다. 이는 해당 디렉터리 안에서는 page.js 파일을 찾을 수 없이 때문에 공개적으로 액세스가 불가하다.
위의 폴더들을 주의 깊게 살펴보면 (overview)
라고, 폴더명을 소괄호로 감싸고 있는 부분이 있다.
이는 폴더를 라우트의 URL 경로에 포함하지 않도록 하는 Route Groups 기능이다. 괄호 안의 폴더는 URL에서 제외되기 때문에, 해당 기능을 통해 URL 경로 구조에 영향을 주지 않으면서 라우터 세그먼트와 프로젝트 파일을 논리적인 그룹으로 묶을 수 있다.
원래 loading.tsx를 app/dashboard/
바로 아래에 뒀다면 /dashboard/customers 또는 /dashboard/invoices 경로에 모두 로딩이 적용되었을 것이다. 하지만 여기서는 /dashboard/ 경로에 접근하는 경우에만 로딩을 표시하고자 하여 이러한 라우트 그룹 기능이 사용되었다.
이렇듯 라우트 그룹을 활용하면 디렉터리를 URL 경로에 포함시키지 않으면서 관련이 있는 파일을 논리적으로 통합할 수 있다.
위의 프로젝트 구조를 보면 app/dashboard/invoices
디렉터리 아래에 대괄호로 감싸진 폴더명이 있다.
Dynamic Routes는 동적 데이터에서 프로그래밍 방식으로 라우트 세그먼트를 생성할 수 있는 기능으로, 정확한 세그먼트 이름을 아직 모르며 이후 동적으로 받아 오는 데이터로부터 경로를 생성하려는 경우에 유용하다.
여기서는 [id]
부분이 동적 세그먼트에 해당하는 부분으로, layout 컴포넌트나 page 컴포넌트 혹은 route handler 등에 params
라는 props 형태로 전달된다.
Route | Example URL | params |
---|---|---|
app/dashboard/invoices/[id]/edit | /dashboard/invoices/3d999292-ca96-4068-8375-11c44ceb3312/edit | { id: '3d999292-ca96-4068-8375-11c44ceb3312' } |
이렇게 블로그 게시물, 제품 페이지 등과 같은 곳에서 동적 라우트를 활용해 상세 페이지 등의 UI를 만들 수 있다.
미들웨어는 들어오는 요청이 완료되기 전에 특정 코드를 실행할 수 있도록 한다. 이를 통해 리다이렉팅하거나 요청/응답 헤더를 수정하거나, 필요에 따라 직접적으로 응답을 변경할 수 있기도 하다.
간단히 말해, 미들웨어는 요청과 응답 사이에서 동작하여 서버의 동작을 제어하고 조정할 수 있는 도구이다.
애플리케이션에 미들웨어를 통합하면 성능이나 보안 및 사용자 경험 측면을 향상시키는 데 도움이 된다.
미들웨어가 효과적으로 사용될 수 있는 상황은 다음과 같다:
인증 및 권한 부여
: 특정 페이지나 API 라우트에 사용자가 액세스하려 할 때, 사용자 식별 정보를 미리 확인하고 세션 쿠키를 검사해 인증을 수행하거나 권한을 부여할 수 있다.서버 측 리다이렉트
(Redirect): 특정 조건에 따라 서버 단에서 사용자를 리다이렉트한다. (Ex: 지역, 사용자 역할)경로 재작성
(Path Rewrite): 요청 프로퍼티를 기반으로 API 라우트나 페이지에 대한 경로를 동적으로 변경하여, A/B 테스트나 기능 롤아웃 등을 지원한다.봇 탐지
: 봇 트래픽을 감지하고 차단함으로써 리소스를 보호한다.로깅 및 분석
: 페이지나 API 처리 전에 요청 데이터를 캡처하고 분석해 인사이트를 얻는다.기능 플래깅
(Flagging): 기능을 원활하게 출시하거나 테스트하기 위해 동적으로 기능을 활성화/비활성화 한다.예제 프로젝트에는 라우트를 보호하는 로직을 추가하기 위해 프로젝트 루트에 미들웨어를 작성하여 로그인하지 않은 사용자를 대시보드 페이지에 액세스하지 못하도록 막는 데 사용하였다.
// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
providers: [
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
// while this file is also used in non-Node.js environments
],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
} satisfies NextAuthConfig;
미들웨어는 요청이 완료되기 전에 호출되며, auth
및 request
속성을 가지고 있는 객체를 받는다. auth
에는 사용자 세션이 포함되며 request
는 말 그대로 들어오는 요청에 대한 내용이 들어 있다.
// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
미리 작성한 authConfig 파일을 middleware에 가져오고 있다. authConfig 객체는 NextAuth.js를 초기화하고, auth
속성을 내보내고 있다. 또한 미들웨어의 matcher
옵션을 통해 특정 경로에서만 실행되도록 지정하고 있다.
이러한 작업에 Next.js에서 제공하는 미들웨어를 사용했을 때의 장점은, 미들웨어가 작업(인증)을 완료할 때까지 보호된 경로에서 렌더링이 시작되고 있지 않아, 애플리케이션의 보안과 성능이 모두 향상된다는 점이 있다.
App router 도입으로 인해 애플리케이션의 라우팅이나 레이아웃 관리가 용이해질 것과 성능 면에서도 향상될 것이 기대된다.
하지만 이렇게 성능 측면에서 이야기 하기 위해선 실제로 직접 테스트를 진행해 보며 서버 컴포넌트와 같은 기능들이 실제로 어떻게 작동하는지 직접 확인해 볼 필요가 있을 것 같다.
더불어, 소개한 기본적인 내용과 기능 외에도 지원되는 다양한 추가 기능들이 많이 있으니 공식 문서, 그 중에서도 Routing 섹션을 참고하면 Next.js를 이용해 앞으로 애플리케이션을 구축 하는데 하는데 있어서도 많은 도움이 될 것 같다.