Next.js에서 AppRoute는 파일 시스템 기반의 라우터라고 설명하는데, app폴더 하위에 위치한 세그먼츠와 특수한 파일들을 통해 라우트를 정의하기 때문이다.
app
├─dashboard
│ ├─ settings
│ │ │ page.tsx // ex.com/dashboard/settings
│ // ex.com/dashboard 404 Not Found
│ page.tsx // ex.com
가장 상위에 있는 app폴더는 루트 path를 가리키고 하위 세그먼트는 그 뒤로 URL path에 매핑된다. page.tsx
폴더는 라우트폴더를 퍼블릭하게 접근할 수 있게 만들어주며 해당 URL로 이동을 했을 때 page.tsx의 UI가 보여진다는 의미이다.
Layout파일은 여러 라우트에 공유되는 UI로, 페이지 이동 시 state를 보존하고 인터랙티브한 요소들이 유지되며 리렌더링이 되지 않는다.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body>루트 레이아웃{children}</body>
</html>
)
}
위의 코드에서 children
하위에 또 다른 레이아웃이 있다면 레이아웃이 들어오게 되고 없다면 해당 폴더의 페이지가 children으로 들어오게 된다. 그렇기때문에 Layout 파일은 루트 레이아웃을 제외하고 children을 받아 리턴하는 형태가 기본이며, 루트 레이아웃은 html
, body
태그가 필수이다.
정확한 폴더의 이름을 미리 정할 수 없어 동적으로 경로를 정의해야하는 경우 다이나믹 라우트를 사용하게 된다.
app
├─dashboard
│ ├─ [id]
│ │ │ page.tsx
// ex.com/dashboard/123
// id === 123
다이나믹 라우트는 []
대괄호를 감싸서 폴더를 만들고 page.tsx를 추가해주면 URL에서 접근이 가능하다.
export default function Page({ params }: { params: { id: string } }) {
return <p>{params.id}</p>
}
다이나믹 라우트 값은 props의 params를 사용하여 가져올 수 있으며, params안에는 폴더에 정의한 다이나믹 라우트 이름으로 들어오게 된다.
Route = /dashboard/[...id]/page.tsx
URL = /dashboard/a/b/1
params = { id: ['a', 'b', 1] }
다이나믹 라우트를 전개연산자를 사용한 [...id]
로 정의하면 하위에 있는 모든 url이 params안에 배열로 담긴다.
Route = /dashboard/[[...id]]/page.tsx
URL = /dashboard
params = {}
또한 [[...id]]
한번 더 대괄호로 감싸면 하위 모든 url에 매칭되는 동시에 id가 없을 때 빈 객체가 담기게 할 수도 있다.
app 하위에 있는 폴더는 URL path에 맵핑이 되지만 라우트 그룹은 URL path에 포함되지 않기 때문에 구조에 영향을 끼치지 않고 라우트 세그먼트와 파일들을 그룹으로 구성한다.
app
├─ (auth)
│ ├─ signin
│ │ │ page.tsx
│ ├─ components
// ex.com/auth/signin
// Not Found
// ex.com/signin
// page.tsx UI
보통 공통되는 UI나 컴포넌트를 사용할 때 상위에 세그먼트를 만들지 않고 라우트 그룹을 활용한다.
라우트 핸들러는 지정한 경로에 대해 들어오는 HTTP 요청을 처리하는 커스텀 Request 핸들러 파일이다.
app
├─ api
│ ├─ test
│ │ │ route.ts
export async function GET(request: Request) {
...
return NextResponse.json(data)
}
api 세그먼트 외의 폴더에 위치한 파일들에서는 /api/test
로 API 요청이 가능하다.
Next.js 14버전까지는 캐시가 되는 것이 default이고 15버전에서는 캐시를 하지 않는 것이 default로 바뀌어졌다.
app
├─ api
│ ├─ test
│ │ ├─ [id]
│ │ │ │ route.ts
export default function POST(request: Request, { params }: { params: { id: string } }) {
...
return NextResponse.json(data)
}
라우트 핸들러에서 다이나믹 라우트 값을 받는 것도 가능하다. 같은 방식으로 []
대괄호로 감싸주고 첫번째 인자로는 request, 두번째 인자 params로 다이나믹 라우트 값이 들어오게 된다. 이렇게하면 /api/test/${id}
로 API 요청이 가능하다.
loading.js
파일과 React Suspense
를 통해 로딩 UI를 구성할 수 있는데, 해당 컨벤션을 사용하면 라우트 세그먼트의 컨텐츠가 로드되는 동안 즉시 로딩 상태를 보여주고 렌더링이 완료되면 컨텐츠가 자동으로 교체된다.
<Suspense fallback={ <Loading /> }>
<Page />
</Suspense>
로딩 파일을 추가하면 서스펜스가 페이지를 감싸는 형태와 동일하게 된다.
SSR은 서버에서 fetch를 하고 HTML 생성한 이후, 생성된 HTML과 CSS,JS를 클라이언트로 전송한다. 그리고 클라이언트는 non-interactive한 상태의 HTML과 CSS를 먼저 보여준 이후에 React가 hydrates를 해서 페이지를 인터랙티브하게 만든다. 하지만 이 단계들은 순차적이라 어느 한 단계에서 지연이 발생하게 되면 그만큼 전체 페이지를 보여주게 되는 시간도 늦어지게 된다.
이러한 한계로 Next.js에는 스트리밍이 있는 것인데, 스트리밍은 페이지의 HTML을 더 작은 청크로 나눠 클라이언트에 점진적으로 전송을 하기때문에 모든 부분이 완성될때까지 기다리지 않고 완성된 부분 먼저 사용자한테 보여주어 SSR의 한계를 개선한다.
import Card from "@/components/Card"
import { Suspense } from "react"
export default async function Page() {
return (
<p>
<Suspense fallback={<div>card1 loading중...</div>}>
<Card />
</Suspense>
<Suspense fallback={<div>card2 loading중...</div>}>
<Card />
</Suspense>
<Suspense fallback={<div>card3 loading중...</div>}>
<Card />
</Suspense>
</p>
)
}
Card 컴포넌트에 API 응답에 대해 delay를 랜덤하게 걸어주고 페이지에서 Suspense로 감싸주면 응답이 오지 않은 컴포넌트는 로딩 UI가 보여지고 응답이 오면 Card 컴포넌트로 교체된다.
Next.js에서 Error는 예상되는 에러와 예상되지 않는 에러로 나뉜다. 먼저 예상되는 에러는 server action 안에서 try-catch로 잡는 에러로 useActionState를 사용해서 처리한다. 반면 예상되지 않는 에러는 error 파일과 global-error 파일들로 에러 바운더리를 구현해 처리한다. error파일의 형태는 로딩 파일과 동일하게 <ErrorBoundary fallback={ <Error /> }>
로 감싼 형태이고 global-error파일은 루트 레이아웃처럼 html
, body
태그를 포함시키면 된다.
notFound 함수가 라우트 세그먼트에서 throw 되었을 때나 URL에 일치하는 라우트가 존재하지 않을 때 표시된다.
Not Found페이지도 app폴더 하위에 not-found.tsx
파일을 추가하여 커스텀이 가능하다.