들어가며
새로 시작하는 프로젝트에서는 기존에 사용하던 Next.js의 Pages Router 대신 13 버전부터 도입된 App Router를 채택하여 사용하기로 하였습니다.
App Router라는 이름만 들어보고 아직 사용해 본 적이 없기에 이를 적용하기 전에 먼저 살펴보는 글을 써보려 합니다.
App Router
Next.js에서 라우트를 처리하고 뷰를 렌더링 하기 위한 새로운 전략입니다. (일부에서는 App Directory라고 불리는 것 같습니다.)
공식 문서에 따르면 App Router는 기존 파일 시스템 기반의 Pages Router에서 발전된 형태로 Next.js의 라우팅 및 레이아웃 경험을 개선한 것으로 소개하고 있습니다. 현재 Next js에서는 Pages Router와 App Router를 함께 사용할 수 있어서 점진적으로 App Router로 전환할 수 있습니다. (App Router가 Pages Router보다 우선순위가 더 높습니다.)
/app
폴더는 아래와 같은 새로운 기능을 지원합니다.
- Layouts
- Server components
- Streaming
- Support for Data Fetching
위 기능은 아래에서 더 자세히 알아보기로 하고 /app
폴더의 새로운 아키텍처를 알아보겠습니다.
파일 시스템 기반의 라우터 형식이기 때문에 /app
안의 각 폴더는 경로 세그먼트라 불립니다. 하위 경로는 이전과 마찬가지로 폴더 안의 새 폴더를 중첩하여 경로를 생성할 수 있습니다.
여기서 기존과 다른 점은 test.co.kr/dashboard
페이지를 만든다면
- Pages Router는
/pages
폴더안에dashboard.tsx
파일을 생성하여 경로를 결정합니다. - App Router는
/app
폴더에서/dashboard
폴더를 생성하고 그 안에page.tsx
파일을 통해서 라우팅하게 됩니다.
/pages
폴더로 구분이 되어있지 않기 때문에 각 폴더 안에 page.tsx
또는 route.tsx
파일이 있어야만 라우터로서 공개적으로 주소 지정이 가능합니다.
기존처럼 /pages
폴더에 묶이지 않고 /app
폴더에 위치하므로, 많은 페이지가 있는 경우에 페이지 디렉터리로 쉽게 식별할 수 있을지 의문이 들기도 합니다. 🤔
App Router에서는 /dashboard
폴더 안에 아래와 같이 새로운 파일 규칙이 적용됩니다.
layout | 세그먼트 및 해당 하위 항목에 대한 공유되는 Layout UI |
page | 경로의 고유한 UI 및 경로에 공개적으로 액세스를 가능하게 하는 페이지 |
loading | 세그먼트 및 해당 하위 항목에 대한 Loading UI |
not-found | 세그먼트 및 해당 하위 항목에 대한 페이지가 없을 경우 사용하는 UI |
error | 세그먼트 및 해당 하위 항목에 대한 오류 UI |
global-error | 전역 오류 UI |
route | 서버 측 API 엔드포인트 |
template | 특수화된 다시 렌더링될 수 있는 레이아웃 UI |
default | 병렬 경로에 대한 대체 UI |
Layouts
Layouts은 비용이 많이 드는 리렌더링을 피하면서 상태를 보존하며 라우트 간에 UI를 쉽게 공유할 수 있습니다.
새로운 파일 규칙에서 흥미로운 건 layout이였습니다. 이전에는 페이지 간에 공통으로 사용되는 레이아웃을 관리하기 위해 'Layout.tsx'와 같은 컴포넌트를 생성하여 관리했었는데, App Router에서는 기본적으로 제공된다는 사실입니다.
가장 상위에 있는 layout 파일이 루트 레이아웃 파일이 되며 루트 레이아웃 파일은 반드시 존재해야하고 이 안에 html, body 태그가 포함되어야 합니다.
layout은 페이지끼리 공유가 가능하고 상태를 유지하며 다시 렌더링 되지 않습니다. template 파일과의 차이점은 template은 페이지가 보일 때마다 렌더링이 된다는 점입니다. useState와 같이 라이프사이클 훅을 필요로 하는 UI라면 template을 활용하는 게 더 좋은 선택일 수 있습니다.
Server components
가장 동적인 애플리케이션에 대해서 서버 우선을 기본으로 설정
지금까지 Pages Router에서 우리가 사용하던 컴포넌트는 클라이언트 컴포넌트로 클라이언트 사이드에서 렌더링이 되었다면 서버 컴포넌트는 서버에서 렌더링 되는 컴포넌트입니다.
서버 컴포넌트를 사용하면 초기 로딩이 빨라지고 자바스크립트의 번들 사이즈가 축소되기 때문에 성능을 향상시킬 수 있다는 장점을 가지고 있습니다.
그렇다면 언제 서버 컴포넌트를 사용하는 것일까요? 공식 홈페이지에 자세히 나와있는데요, 다음과 같은 상황을 제외하고는 서버 컴포넌트 사용을 권장합니다.
onClick
과 같은 이벤트 리스너 사용useEffect
와 같은 라이프사이클 훅 사용- 브라우저 API 사용
- 라이프사이클 훅과 이벤트 리스너 사용하는 커스텀 훅 사용
- class component 사용
그동안 데이터와 관련해서 구분 짓지 않고 클라이언트 컴포넌트로만 작성해왔는데, 기능에 따라 컴포넌트를 구분해서 작성하려면 구조적으로 머리가 아플 것 같습니다 🤯
기본적으로 App Router에서는 서버 컴포넌트로 지정이 되고 'use client'
지시문을 통해 클라이언트 컴포넌트로 전환할 수 있습니다.
'use client';
export default function ClientComponent() {
return (
<div>
<p>Hello Client Component</p>
</div>
);
}
사용 시 주의점은 클라이언트 컴포넌트에서 서버 컴포넌트를 import
해서 사용할 수 없고 children
이나 prop
으로 전달해서 사용해야 한다는 점입니다.
잘못된 방법 🙅♀️
'use client';
import ServerComponent from './ServerComponent';
export default function ClientComponent() {
return (
<>
<ServerComponent />
</>
);
}
올바른 방법 🙆♀️
'use client';
// children: ServerComponent
export default function ClientComponent({children}) {
return (
<>
{children}
</>
);
}
이번 글에서는 이 정도로 간단히 소개하고, 서버 컴포넌트에 대해 더 이해한 뒤 다음 글에서 자세히 다뤄보도록 하겠습니다.
Streaming
UI가 렌더링 되는 동안 로딩 상태를 표시하고 UI의 단위를 스트리밍 합니다.
Streaming은 새로운 파일 규칙인 loading 을 이용해 스켈레톤, 로딩 스피너 UI 로딩 화면을 보여주고 데이터 로딩이 완료되면 완료된 부분부터 점진적으로 표시하게 됩니다.
UI가 렌더링 되기 전에 모든 데이터가 로드될 때까지 기다리지 않고 페이지의 일부를 더 빨리 표시할 수 있습니다.
// loading.tsx
export default function Loading() {
return <LoadingSkeleton />
}
같은 폴더에서 loading은 layout 안에 중첩됩니다. <Suspense>
경계에서 page
파일과 하위 항목을 자동으로 래핑 합니다.
Support for Data Fetching
비동기 서버 컴포넌트 및 확장된 fetch API를 사용하여 구성 요소 수준에서 데이터를 가져오는 것이 가능합니다.
기본 fetch() Web API를 확장시키면서 기존의 SSG, SSR, ISR(Incremental Static Regeneration) 등의 기법을 fetch() Web API 하나로 대체 가능합니다.
// 데이터 캐시에서 일치하는 요청을 찾습니다.
// `getStaticProps`와 비슷한 형태
fetch(`https://...`, { cache: 'force-cache'})
// 캐시를 조회하지 않고 요청이 있을 때마다 원격 서버에서 리소스를 가져오며, 다운로드한 리소스로 캐시를 업데이트하지 않습니다.
// `getServerSideProps` 와 비슷한 형태
fetch(`https://...`, { cache: 'no-store' })
// 리소스의 캐시 수명(초)을 설정합니다.
// `revalidate` 옵션을 사용한 `getStaticProps` 와 비슷한 형태
fetch(`https://...`, { next: { revalidate: false | 0 | number } })
// 리소스의 캐시 태그를 설정합니다. 필요에 따라 데이터를 재검증할 수 있습니다.
fetch(`https://...`, { next: { tags: ['collection'] } })
마치며
Next.js에서 이제 App Router을 사용한다 정도만 알고 있고 깊게 들여다보지 않았는데요, 글을 작성해 보면서 사용 이유와 방법에 대해 정리하는 시간을 가져보았습니다.
아직 실제로 코드를 작성하지 않았지만, 앞으로 이를 적용하면서 어떤 느낌을 받았는지에 대한 후속 글도 써보려 합니다.
읽어주셔서 감사합니다.