Next.js App Router(3/3)

김재한·2024년 1월 8일
0

Next 14

목록 보기
4/5
post-thumbnail

⬅️ App Router (2/3)

Route Handlers

page Router에서 /api Routes와 비슷한 개념이다. Web Request와 Response API를 사용해 특정 route에 대해 Custom Handler를 작성할 수 있게 해준다.

Convention

Route Handler는 app 폴더 안에 route.js로 정의한다. page나 layout 처럼 중첩이 가능하지만 같은 segment 레벨 내에 page.js를 둘 수 없다. 지원하는 HTTP Methods로는 GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS 가 있다.

Static Route Handlers

GET 메소드와 Response 객체를 함께 사용할때 Static Route 라고 한다.

import { NextResponse } from 'next/server';
 
export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  });
  const data = await res.json();
 
	// Response.json()도 가능한데 이렇게하면 Type 오류가 난다고 한다.
  return NextResponse.json({ data });
}

Dynamic Route Handlers

아래의 경우를 Dynamic Route 라고 한다.

  • Request 객체와 GET 메소드를 사용할 때
  • GET 이외에 다른 메소드를 사용할 때
  • cookies 나 headers 같은 Dynamic 함수를 사용할 때
  • Segment config options로 수동 dynamic mode를 지정할 때

Rendering

Client Side & Server Side 에서 컴포넌트를 렌더링 하는 것 외에도 Next.js는 서버에서 StaticDynamic 렌더링을 통해 최적화 할 수 있는 옵션을 제공한다.
(Static 이 기본으로 사용되기 때문에 Dynamic은 다음에 정리하려한다.)

Static Rendering

Server 와 Client Component는 빌드 타임에 pre-render 된다. 동작의 결과는 캐시되고 추가 요청에 재사용되어 성능상 효율적이다.( 물론 캐시는 무효화 할 수 있다.) 기존의 SSG, ISR 과 같다.

  • Client Component: 서버에서 HTML과 JSON을 pre-render하고 캐시한다. 그 후 캐시된 결과가 hydrate 를 위해 클라이언트로 전송된다.
  • Server Component: 서버에서 렌더링 되고 payload는 HTML을 생성하기 위해 사용된다. 동일한 렌더링된 payload는 클라이언트 컴포넌트를 hydrate 하는 데에도 사용되므로 클라이언트에 JS가 필요하지 않다.

layout 이나 page 에서 동적 기능이나 동적 데이터 Fetching을 통해 동적 렌더링을 사용할 수 잇다. 이렇게 하면 Next.js는 요청 시간에 전체 route를 동적으로 렌더링 한다.

Data Fetching

Page Router에서 사용했던 getServerSidePropsgetStaticProps 가 이제는 fetch() 하나로 처리가 가능해졌다.

Fetchting Data On Server

아래와 같은 이점들로 Server Component에서 데이터를 Fetch 하는 것을 권장한다.

  • backend data 자원에 직접적으로 접근할 수 있다.
  • 민감한 정보가 클라이언트에 노출되는 것을 방지할 수 있다.
  • data fetch와 render를 동일한 환경에서 한다. 이는 클라이언트와 서버간 통신을 줄일 수 있다.

물론 Client Component에서도 가능하며 SWR이나 React Query 사용을 추천한다. App Router 에서는 layout과 page, component 모두에서 data를 Fetch 할 수 있고 Streaming 과 Suspense 또한 호환이 가능하다.

다만, Layout은 부모 자식 간 데이터 전달이 불가능하다. Layout에서 data fetch를 그냥 사용하고, 중복된 데이터 요청은 알아서 캐시하고 중복요청은 제거해주니 큰 이슈는 없어보인다.

Static and Dynamic Data Fetching

데이터에는 두 가지가 종류가 있다.

  • Static Data: 자주 변하지 않는 정적인 데이터
  • Dynamic Data: 자주 변화하고 갱신이 필요한 데이터

next.js는 기본적으로 빌드타임에 fetch되어 각 요청마다 재사용된다. (Static Fetch) pages router 에서의 SSG이다. 하지만 개발자는 필요에 따라 revailidate 시킬 수 있다. (Dynamic Fetch)

fetch는 디폴트로 데이터를 계속 reFetch 하고 캐싱한다. force-cache가 Default 이다.

Static Data Fetch

특정 주기마다 캐시된 데이터를 revalidate하려면 next.revalidate 옵션을 사용할 수 있다.

// 10초마다 데이터를 갱신한다.
fetch('https://~~~', {next:{revalidate:10}})

Dynamic Data Fetching

데이터를 요청할 때마다 갱신하고 싶으면 cache:'no-store' 옵션을 사용한다.

fetch('https://~~', {cache:'no-store'})

Data Fetching Patterns

Data Fetching 패턴에는 두 가지가 있다.

  • Parallel: route의 요청이 시작되고 동시에 데이터를 로드한다. 이는 client-server waterfall과 로드에 소요되는 총 시간을 줄인다. ( 권장 )
  • Sequential: route의 요청이 각각 따로 수행되고 waterfall을 만든다. 의존관계가 있는 fetch를 실행하는 경우 주로 사용한다.

Parallel Data Fetching

사용자 경험을 향상시키기 위해 Suspense Boundary를 추가해 렌더링 작업을 분할 하였다.

import { type Album } from './api';

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`);
  return res.json();
}
 
async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`);
  return res.json();
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string };
}) {
  // 두 요청을 병렬로 초기화한다. (Parallel)
  const artistData = getArtist(username);
  const albumData = getArtistAlbums(username);
 
  // 먼저 artist의 promise가 resolve되기를 기다린다.
  const artist = await artistData;
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* 먼저 artist 정보를 전송하고
          albums는 susnpense boundary로 감싼다.
					이렇게 렌더링 작업을 분할한다. */}
      <Suspense fallback={<div>Loading...</div>}>
        <Albums promise={albumData} />
      </Suspense>
    </>
  );
}
 
// Albums Component
async function Albums({ promise }: { promise: Promise<Album[]> }) {
  // album promise가 resolve되기를 기다린다.
  const albums = await promise;
 
  return (
    <ul>
      {albums.map((album) => (
        <li key={album.id}>{album.name}</li>
      ))}
    </ul>
  );
} 

Sequential Data Fetching

데이터를 순차적으로 가져오기 위해 필요한 컴포넌트에서 직접 fetch(). 즉, 부모 컴포넌트가 우선적으로 fetch() 된 이후 자식 컴포넌트 fetch()

// ...
 
async function Playlists({ artistID }: { artistID: string }) {
  // Wait for the playlists
  const playlists = await getArtistPlaylists(artistID);
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  );
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string };
}) {
  // Wait for the artist
  const artist = await getArtist(username);
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  );
}

Automatic fetch() Request Deduping

트리 내 다수의 컴포넌트에서 동일한 input을 가지는 fetch() 요청 시 Next.js는 중복된 요청을 제거한다.

참고
Route Handlers
Randering
Fetching, Caching
ServerAction
Fetching Pattern
Middleware
@asdf99245_AppRouter
@asdf99245_MiddleWare

0개의 댓글