nextjs.org/docs 공식문서의 내용을 번역, 정리한 내용입니다.
사진 출처는 nextjs.org 공식문서입니다.
loading.js
파일은 리액트 Suspense와 함께 의미 있는 Loading UI를 만드는 것을 도와준다. 이 컨벤션을 통해 route segment의 컨텐츠가 로딩되는 동안 일시적인 로딩 상태를 보여주는 컴포넌트를 보여줄 수 있고, 이후 컨텐츠 로딩이 완료되면 자동으로 교체된다.
폴더 안에 loading.js
파일을 추가하여 loading state를 만들어 줄 수 있는데, 페이지의 구조나 spinner 혹은 로딩이 완료되었을 때 화면에 보여질 컨텐츠의 일부분 등을 pre-render 하여 보여줄 수 있다.
// app/loading.js
export default function Loading() {
// 로딩 상태를 나타내주는 UI를 추가하면 된다
return <LoadingSkeleton />;
}
같은 폴더 안에 있다면 loading.js
는 layout.js
안에 중첩된다. 또, 자동으로 <Suspense>
의 fallback
으로 loading 컴포넌트가 지정되며 page.js
나 <Suspense>
의 하위 요소를 감싸준다.
UI 컴포넌트를 위한 Suspense Boundary를 따로 생성해 줄 수도 있다. (Suspense가 궁금하다면 이곳에서)
기존 SSR 방식은 페이지에서 사용되는 데이터가 모두 fetch 되었을 때 서버쪽에서 HTML을 렌더링할 수 있었고, 클라이언트 쪽에서는 페이지의 모든 컴포넌트와 관련된 코드가 다운로드 되어야 UI hydration이 적용되었다. 이 단계들은 순서대로 진행되며 각 단계가 blocking(앞 단계가 처리되기 전까지 다음 단계로 넘어가지 않음) 되었다.
이에 Next.js와 React를 활용한 SSR은 사용자에게 상호작용이 불가능한 페이지라도 최대한 빠르게 보여주는 방식으로 이 단점을 극복하고자 했다. 하지만 서버에서 데이터 fetching 시간이 오래 걸릴 경우 여전히 UX 측면에서 좋지 않았다.
이를 해결하기 위해 나온 방안이 Streaming 이다. Streaming을 통해 페이지의 HTML을 작은 덩어리로 쪼개어 생성한 뒤 서버에서 클라이언트로 넘겨주게 되었다.
중요도가 높거나 서버에서 데이터 fetching이 따로 필요하지 않은 컴포넌트가 먼저 클라이언트로 전달되어 hydration이 먼저 진행되고 그렇지 않은 컴포넌트는 천천히 전달되는 방식으로 동작한다.
모든 데이터가 fetch 될 때까지 기다리지 않고 페이지의 부분부분을 빨리 보여질 수 있도록 해주기 때문에 UX 측면에서 좋다. 또한 Time To First Byte, First Contentful Paint, Time to Interactive 시간을 줄여주는 효과도 있다.
Streaming을 Suspense와 함께 사용하면
1. 서버 렌더링을 Streaming 할 수 있고
2. 리액트가 유저 상호작용을 바탕으로 어떤 컴포넌트를 먼저 상호작용 가능하도록 만들지 우선순위를 정하는 Selective Hydration이 가능하다
는 장점이 있다.
마지막으로 SEO 측면에서 Next.js는 generateMetadata
함수 내의 데이터 fetching이 완료될 때까지 기다린 후 streaming을 시작하기 때문에 첫 response가 반드시 <head>
태그를 포함하는 것을 보장할 수 있다.
error.js
의 동작 방식런타임에 에러가 발생하게 되면 UX 측면에서 큰 문제가 생기기 때문에 에러 발생을 어떻게 처리할지 고민하는 것은 매우 중요하다. Next.js는 런타임 에러 처리를 매우 쉽게 처리할 수 있도록 도와준다.
단순히 error.js
파일을 폴더에 추가해 주면 자동으로 런타임 에러 처리가 가능하다. 동작 방식은 다음과 같다.
error.js
파일이 export 하는 컴포넌트를 fallback 컴포넌트로 사용한다.// app/error.js
'use client'; // Error 컴포넌트는 반드시 클라이언트 컴포넌트여야 함
import { useEffect } from 'react';
export default function Error({ error, reset }) {
useEffect(() => {
console.log(error);
}, [error]);
return (
<div>
<h2>에러 알림</h2>
<button
onClick={
// 에러 발생 부분 리렌더링을 시도하여 에러 복구를 시도
() => reset()
}
>
다시 시도하기
</button>
</div>
);
}
위의 reset()
함수는 에러 복구를 할 수 있도록 도와주는 함수이다. 함수가 실행되었을 때 Error Boundary 내의 컨텐츠를 리렌더링하는 것을 시도하게 된다. 만약 성공적으로 리렌더링이 된다면 fallback에 등록된 컴포넌트가 리렌더링된 컴포넌트로 대체된다.
error.js
와 같은 special file을 통해 만들어진 컴포넌트는 정해진 위계에 따라 렌더링 된다. 예를 들어 layout.js
와 error.js
파일 모두를 포함한 2개의 segment를 가진 중첩 route는 아래 사진과 같은 위계를 가진다.
중첩 route에서 error.js
는 다음과 같이 동작한다.
error.js
가 내부의 모든 중첩된 자식 segment의 에러를 처리하는 것이다.layout.js
컴포넌트는 Error Boundary 보다 상위에 위치해 있으므로 layout.js
에서 발생한 에러는 처리해 주지 못한다.앞서 말했듯이 special file들은 정해진 위계가 있기 때문에 layout의 에러는 동일 segment의 error.js
파일로 처리해주지 못한다. 따라서, layout이나 template의 에러를 처리해 주기 위해서는 그 부모 segment에 error.js
파일을 만들어 줘야 한다(error는 가장 가까운 부모 Error Boundary까지 타고 올라가서 처리되기 때문에...).
Root Layout일 경우 상황은 조금 다르다. 해당 레이아웃이 위계상 가장 최상단에 위치하므로 app/
에 작성된 error.js
파일도 레이아웃에서 발생하는 에러는 처리하지 못한다. 이 때 사용하는 것이 app/global-error.js
파일이다. 이 파일은 전체 애플리케이션을 감싸주게 되고 만약 에러가 발생하면 fallback 컴포넌트가 root 레이아웃을 대체하게 된다. 이 때문에 global-error.js
파일은 반드시 <html>
과 <body>
태그를 포함해야 한다.
// app/global-error.js
'use client';
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>에러 알림</h2>
<button onClick={() => reset()}>다시 시도하기</button>
</body>
</html>
);
}
추가로, global-error.js
파일이 있더라도 root error.js
파일을 정의해 주는 것이 추천된다고 한다.
데이터 fetching 동안 또는 서버 컴포넌트에서 에러가 발생한 경우 Next.js는 Error 객체를 가장 가까운 error.js
파일에 error
prop으로 전달해 준다.
production 과정에서는 보안을 위해 에러 메세지의 해시값을 포함한 .digest
와 함께 error를 보내준다고 한다. 이 해쉬값을 서버 로그와 대응시켜 확인할 수 있다.
Parallel Routing은 동일한 레이아웃에 동시에 또는 특정 조건에 따라 하나 이상의 페이지를 렌더하는 것을 도와준다. 예를 들어 아래와 같이 대시보드에서 두 개의 페이지를 동시에 렌더링 해줘야 할 때 사용한다.
Parallel Routing은 각각의 route에 독립적인 에러나 로딩 상태를 정의할 수 있도록 해준다.
또, 로그인 상태에 따라 다른 화면을 렌더링 하는 것처럼 특정 조건에 따라 조건부로 어떤 슬롯을 렌더링 할지 정해줄 수 있다. 이는 동일한 URL에서 각 슬롯의 코드를 분리할 수 있게 해준다.
Parallel Route는 'slot' 이라는 것을 활용하여 만들어진다. 슬롯은 @folder
의 컨벤션을 따르며 동일 위계에 존재하는 레이아웃에 props로 전달된다.
만약 @example1
@example2
라는 Parallel Route가 만들어진다면 레이아웃에서
export default function Layout(props) {
return (
<>
{props.children}
{props.example1}
{props.example2}
</>
);
}
와 같이 사용할 수 있다.
아래와 같은 디렉토리 구조가 있다고 가정해 보자
만약 /settings
라는 URL로 이동을 하면 @team
슬롯에는 settings 디렉토리가 있지만 @analytics
슬롯에는 존재하지 않는다. 이 때는 default.js
파일의 존재 여부와 navigation의 종류에 따라 컨텐츠가 어떻게 렌더링 되는지에 차이가 생긴다.
위 디렉토리 구조를 예시로 차이를 정리해보면 다음 표와 같다.
@analytics/default.js O | @analytics/default.js X | |
---|---|---|
Soft Nav | @team/setting/page.js & @analytics/page.js | @team/setting/page.js & @analytics/page.js |
Hard Nav | @team/setting/page.js & @analytics/default.js | 404 error |
useSelectedLayoutSegment
는 클라이언트 컴포넌트에서 활용할 수 있는 Hook으로, 레이아웃이 호출되는 곳 한 단계 아래의 활성화 된 단일 route segment를 읽어올 수 있도록 해준다. 뒤에 s
가 붙은 Hook은 활성화 된 모든 segment들을 배열 형식으로 읽어온다.
// app/layout.js
'use client';
import { useSelectedLayoutSegments } from 'next/navigation';
export default async function Layout(props) {
const segment = useSelectedLayoutSegment();
// ~~
}
만약 /login
이라는 URL로 이동하면 segment
의 값은 문자열 'login'
이 된다.
Intercepting Routes를 한국어로 직역하자면 '경로 가로채기' 가 될 것이다. 이는 현재 페이지의 context를 유지하며 현재 레이아웃 안에 route를 로딩할 수 있도록 해준다. Intercepting Routes는 어떤 특정 route를 가로채고 다른 route를 보여주고 싶을 때 사용하면 좋다.
이렇게 말하면 사실 이해가 잘 되지 않는다. 예시를 통해 알아보자.
/feed
URL 경로를 가진 피드에서 사진을 클릭해서 photo/123
URL 경로를 모달창으로 띄우는 상황을 예로 들면, Next.js는 /feed
route를 가로채 /photo/123
URL을 보여주도록 가림 처리를 한다.
하지만, 공유된 링크를 통해 이미지 URL로 직접 접근하거나 페이지를 새로고침하는 경우에는 모달을 통하지 않고 전체 photo 페이지가 렌더링 되어야 한다. route interception이 일어나지 않아야 하는 것이다.
Intercepting Route는 (..)
컨벤션으로 정의된다. 폴더 이름 앞에 붙여주면 된다. 이 때 (..)
컨벤션은 파일 시스템이 아닌 route segments를 기준으로 삼는다.
(.)
동일 단계의 segment와 연결(..)
한 단계 위의 segment와 연결(..)(..)
두 단계 위의 segment와 연결(...)
root인 app
디렉토리의 segment와 연결가령 아래와 같이 Intercepting Route를 생성하면 feed
segment 내에서 photo
segment를 intercept 할 수 있다.