정리/Web

[Web] Next.js의 App Router

루미미 2024. 10. 20. 21:40

Next.js의 App Router는?

Next.js 13부터 새롭게 추가된 기존의 Page Router를 대체하는 완전히 새로운 개념의 router다.

  • routing을 설정하는 방식이 기존의 Page Router 버전과는 완전히 달라지게 된다.
  • 페이지의 레이아웃을 설정하는 방법이나 데이터를 불러오는 방식에도 다양한 변화가 생기게 된다.
  • React 18부터 새롭게 추가된 리액트 서버 컴포넌트나 스트리밍 등의 최신 기능들도 함께 사용할 수 있게 된다.

 

Page Routing

  • page라는 이름을 갖는 파일만 page 파일로써 취급된다.
  • 만약 test 경로에 대응하는 페이지를 만들려면 페이지를 만들고자 하는 폴더 안에 page 파일을 만들면 된다.

동적 경로

  • URL 파라미터를 사용하는 동적 경로에 대응하게 되는 페이지를 만들려면 페이지를 만들고자 하는 폴더 안에 대괄호로 감싸진 폴더를 만들고 그 안에 page 파일을 만들면 된다.
  • 쿼리 스트링이나 URL 파라미터처럼 경로와 함께 명시되는 값들은 해당 페이지 컴포넌트에 props로 전달이 된다.

Layout

  • 레이아웃을 설정하려면 레이아웃을 만들고자 하는 폴더 안에 layout 파일을 만들면 된다.
  • 레이아웃 컴포넌트를 구성 할 때에는 children이라는 props를 통해서 페이지 컴포넌트를 전달 받아 return 문 안에 어디에 렌더링 할 건지 작성해야만 페이지도 잘 렌더링 될 수 있다.

 

React Server Component

React Server Component란?

  • React 18부터 새롭게 추가된 새로운 유형의 리액트 컴포넌트로, 기존의 컴포넌트들과는 달리 오직 서버 측에서만 실행이 되는 컴포넌트다.

React Server Component가 등장한 이유

  • Page Router의 렌더링 과정에서 상호작용이 없는 컴포넌트들은 굳이 브라우저 측에서 한 번 더 실행되어 불필요하게 많은 컴포넌트들이 JS 번들에 포함이 돼서 JS 번들의 용량이 쓸데없이 커진다.
  • 상호작용이 없는 컴포넌트들은 Next.js 서버 측에서 사전 렌더링을 진행할 때 한 번만 UI를 렌더링하기 위해 한 번만 실행이 되어야 하고 그 이후에는 더 이상 실행될 필요가 없도록 만들어 줘야 한다.

App Router의 React Server Component

App Router에서는 상호작용의 유무에 따라서 서버 컴포넌트와 클라이언트 컴포넌트로 분류를 한다.

  • 서버 컴포넌트 : 상호작용이 없어서 서버 측에서만 실행되는 컴포넌트
  • 클라이언트 컴포넌트 : 상호작용이 있어서 하이드레이션이 필요하기 때문에 서버와 브라우저에서 한 번씩 실행되는 컴포넌트

HTML 페이지를 한 번 생성해야 하기 때문에 서버 컴포넌트, 클라이언트 컴포넌트 모두 동일하게 한 번은 실행이 되지만 그 이후에 하이드레이션을 위해서 컴포넌트들을 모아 JS 번들로 전달하는 과정에서는 서버 컴포넌트들은 제외되게 되고 클라이언트 컴포넌트들만 JS 번들에 포함되어서 브라우저에게 전달이 된다.

 

Next.js에서는 페이지의 대부분을 서버 컴포넌트로 구성할 것을 권장한다. 페이지 내부에 클라이언트 컴포넌트의 개수가 줄어들수록 Next.js 서버가 브라우저에게 전달하게 되는 JS 번들의 용량도 함께 줄어들기 때문이다.

React Server Component 사용

  • 서버 컴포넌트는 따로 만들어 줄 필요가 없다. 기본적으로 모든 컴포넌트가 서버 컴포넌트로 작동하기 때문이다.
  • 서버 컴포넌트에서는 보안에 민감한 작업이나 데이터 페칭 등의 작업들은 진행할 수 있지만 React Hooks 같은 브라우저에서만 할 수 있는 작업들은 서버 컴포넌트 내부에서는 진행할 수 없다.

React Server Component 주의사항

  • 서버 컴포넌트에는 브라우저에서 실행될 코드가 포함되면 안된다.
    • React Hooks, 이벤트 핸들러, 브라우저에서 실행되는 기능을 담고 있는 라이브러리 등
  • 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다.
    • 클라이언트 컴포넌트는 일반적인 컴포넌트처럼 Next.js 서버 측에서 사전 렌더링을 진행할 때 한 번 먼저 실행이 되고 그 이후 하이드레이션을 진행할 때 브라우저 측에서 한 번 더 실행되기 때문에 서버에서도 실행된다.
  • 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다.
    • 서버 컴포넌트의 코드들은 JS 번들의 용량을 줄이기 위해 JS 번들로부터 제외된다. 그래서 아예 서버 컴포넌트의 코드들은 브라우저에게 전달조차 되지 않는다.
    • 클라이언트 컴포넌트에서 서버 컴포넌트를 import 해서 사용하려고 하면 Next.js에서 자동으로 해당 서버 컴포넌트를 클라이언트 컴포넌트로 변환한다.

 

Navigating

  • 페이지 이동이나 프리페칭 시 Next.js 서버가 브라우저에게 JS 번들만 전달하는 게 아니고 서버 컴포넌트의 결과물인 RSC Payload도 함께 전달한다.
  • useSearchParams 함수는 현재 페이지에 전달된 쿼리 스트링의 값을 꺼내올 수 있는 기능을 제공한다.
    • useSearchParams 함수에서 searchParams 객체를 꺼내 get 메서드를 활용해서 인수로 불러오고 싶은 쿼리 스트링을 작성하면 된다.
import { useRouter, useSearchParams } from "next/navigation";

const searchParams = useSearchParams();

const q = searchParams.get("q");

 

Data Fetching

Page Router의 데이터 페칭 문제점

  • 서버 측에서 불러온 모든 데이터는 컴포넌트 트리의 최상단에 위치하는 페이지 컴포넌트에게만 props로 전달돼서 최상단의 페이지 컴포넌트로부터 데이터를 필요로 하는 말단의 모든 컴포넌트들까지 props나 컨텍스트 API 등을 활용해서 데이터를 넘겨줘야 된다는 문제점이 있다.

App Router의 데이터 페칭

  • 서버 컴포넌트는 오직 서버에서만 실행되기 때문에 async 키워드를 붙여서 비동기 함수로 만든 다음 그 안에서 await 키워드와 fetch 메서드를 활용해서 데이터를 직접 불러오도록 하는 데이터 페칭 로직을 작성하면 된다.
export async function Page(props) {
  const data = await fetch('...');
	
  return <div>...</div>
};

 

 

  • 컴포넌트 내부에서 필요로 하는 데이터를 불러와서 바로 렌더링에 사용할 수 있도록 데이터를 페칭하는 방식 덕분에 Page Router의 문제점을 해결할 수 있다.

 

Data Cache 옵션

fetch 메서드의 두 번째 인수로 객체 형태의 추가적인 옵션을 설정해 줌으로써 적용할 수 있다.

 

  • { cache: "no-store" }
    • 캐싱을 하지 않도록 설정하는 옵션
  • { cache: "force-cache" }
    • 요청의 결과를 무조건 캐싱
    • 한 번 호출 된 이후에는 다시는 호출되지 않는다.
  • { next: { revalidate:시간(초) } } (3은 예시 숫자)
    • 특정 시간을 주기로 캐시를 업데이트 한다.
    • Page Router의 ISR 방식과 유사하다.
  • { next: { tags: ["a"] } }
    • On-Demand Revalidate
    • 요청이 들어왔을 때에만 데이터를 최신화 한다.
    • Page Router의 On-Demand ISR 방식과 유사하다.

 

Request Memoization

Request Memoization이란?

하나의 페이지를 서버 측에서 렌더링하는 과정에서 레이아웃 컴포넌트나 페이지 컴포넌트처럼 하나의 페이지를 이루고 있는 여러 개의 컴포넌트에서 발생하는 다양한 API 요청들 중에 중복적으로 발생하는 요청들을 캐싱해서 중복된 요청을 다시는 발송하지 않을 수 있도록 자동으로 데이터 페칭을 최적화 해주는 기능이다.

Request Memoization 과정

  • 첫 번째 API 요청 발생
  • 메모이제이션에 아무것도 캐싱된 데이터가 없기 때문에 캐시 miss
  • 백엔드 서버에게 데이터 요청
  • 요청된 데이터는 리퀘스트 메모이제이션에 캐싱
  • 두 번째 API 요청 발생
  • 메모이제이션 안에 캐시된 데이터가 있기 때문에 그 데이터를 그대로 사용

Request Memoization과 Data Cache의 차이점

  • 리퀘스트 메모이제이션은 하나의 페이지를 렌더링하는 동안 중복된 API의 요청을 캐싱하기 위해서 존재하는 것이기 때문에 렌더링이 종료되면 모든 캐시가 소멸된다.
  • 데이터 캐시의 목적은 백엔드 서버로부터 불러온 데이터를 거의 영구적으로 보관하기 위해서 존재하는 것이기 때문에 데이터 캐시에 저장된 캐시 데이터들은 서버가 중단되기 전까지는 영구적으로 보관된다.

Request Memoization을 제공하는 이유

  • Page Router에서는 서버 측에서만 실행되는 함수를 통해서 데이터를 불러와서 페이지 컴포넌트에게는 데이터를 props로 넘겨두는 방식을 사용했다.
  • App Router에서는 컴포넌트가 각각 자신이 필요한 데이터를 직접 페칭해오는 방식을 사용한다. 그렇기 때문에 서로 다른 컴포넌트에서 동일한 데이터를 필요로 하는 예외적인 경우가 생길 수 밖에 없어서 리퀘스트 메모이제이션이 등장한 것이다.

 

Full Route Cache

Full Route Cache란?

  • Next.js 서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능이다.
  • Page Router의 SSG 방식과 유사하게 빌드 타임에 정적으로 페이지를 미리 다 만들어 놓고 캐시에 보관한 다음 브라우저에서 요청이 들어왔을 때 캐시에 저장된 페이지를 그대로 응답해준다.
  • 만들어둔 모든 페이지들은 자동으로 정적인 페이지와 동적인 페이지로 나뉘어지게 된다. 이때 정적 페이지들에만 풀 라우트 캐시가 적용된다.

동적 페이지와 정적 페이지로 분류하는 기준

  • 동적(Dynamic) 페이지로 설정되는 기준
    • 특정 페이지가 접속 요청을 받을 때마다 매번 변화가 생기거나 데이터가 달라질 경우
      • 캐시되지 않는 데이터 페칭을 사용할 경우
      • 동적 함수(쿠키, 헤더, 퀴리 스트링)를 사용하는 컴포넌트가 있을 때
  • 정적(Static) 페이지로 설정되는 기준
    • 동적 페이지가 아니면 모두 기본적으로 정적 페이지가 된다.
동적 함수 데이터 캐시 페이지 분류
O X Dynamic Page
O O Dynamic Page
X X Dynamic Page
X O Static Page

 

동적 경로에 Full Route Cache 적용

  • 동적 경로를 갖는 페이지를 정적 페이지로써 빌드 타임에 생성되도록 설정하려면 해당 페이지에 어떤 경로들이 존재할 수 있는지 알려줘야 된다.
  • generateStaticParams 함수를 만들어 파일로부터 내보낸 다음 함수 안에 배열로 경로들을 작성하여 여러 개의 정적인 URL 파라미터들을 반환하면 된다.
export function generateStaticParams() {
  return [{ id: "1"}, {id: "2"}, {id: "3"}];
}

 

Route Segment Config

특정 페이지에 캐싱이나 revalidate 등의 동작을 직접 강제로 동작하도록 설정하는 추가적인 옵션이다.

dynamic 옵션

  • auto
    • 기본값으로, 아무것도 강제하지 않는 옵션
  • force-dynamic
    • 페이지를 강제로 dynamic 페이지로 설정하는 옵션
  • force-static
    • 페이지를 강제로 static 페이지로 설정하는 옵션
    • 동적 함수는 undefined를 반환하도록 설정되고 데이터 페칭도 데이터 캐싱되도록 설정된다.
  • error
    • 페이지를 강제로 static 페이지로 설정하는 옵션
    • 동적 함수 또는 캐싱되지 않는 데이터 페칭 등의 static으로 설정하면 안되는 이유가 있다면 빌드 오류를 발생시킨다.

 

Client Router Cache

  • 브라우저에 저장되는 캐시로, 페이지 이동을 효율적으로 진행하기 위해서 페이지의 일부 데이터를 보관해두는 기능이다.
  • 브라우저 측에 클라이언트 라우터 캐시라는 새로운 캐시 공간을 추가해서 페이지에 접속하려고 할 때 서버로부터 전달받게 되는 RSC Payload의 값들 중에 레이아웃에 해당하는 데이터만 따로 추출해서 클라이언트 라우터 캐시라는 이름으로 보관하도록 자동으로 설정해주게 된다.
  • 한 번 접속한 페이지의 레이아웃들만 따로 보관해둠으로써 나중에 페이지 이동이 추가로 발생하게 되었을 때 해당하는 페이지의 공통된 레이아웃들을 서버로부터 다시 중복되게 불러오지 않을 수 있도록 페이지의 이동을 최적화 해준다.

 

Streaming

Streaming이란?

  • 데이터를 여러 개의 조각으로 쪼갠 다음에 작은 용량의 데이터들을 연속적으로 클라이언트에게 전송하는 기술이다.
  • 서버에서 클라이언트로 데이터를 넘겨줘야 될 때 만약 보내줘야 되는 데이터의 크기가 너무 크거나 서버 측에서 데이터를 준비하는 데 오래 걸려서 빠르게 전송해주기 어려울 때 사용된다.
  • 스트리밍 기술을 이용하게 되면 클라이언트의 입장에서는 모든 데이터가 다 불러와 지지 않은 상태에서도 지금까지 전달 받은 데이터에 접근할 수 있게 되기 때문에 결과적으로 사용자에게 긴 로딩 없이 좋은 경험을 제공할 수 있다.

Streaming 적용

  • 비동기 컴포넌트를 Suspense 컴포넌트로 감싸서 사용할 수 있다.
  • fallback이라는 props로 ReactNode를 전달하면 대체 UI를 렌더링 할 수 있다.
import { Suspense } from 'react';

<Suspense fallback={<div>...</div>}>
  <Test />
</Suspense>

 

Error Handling

  • Next.js의 에러 핸들링을 사용하려면 해당 컴포넌트와 같은 위치 또는 상위에 error 파일을 생성하면 된다.
  • error 파일에는 상단에 use client 지시자를 작성해야 한다. 오류라는 건 서버나 클라이언트 어떤 환경이든 발생할 수 있기 때문에 다 대응할 수 있도록 클라이언트 컴포넌트로 설정을 해줘야 한다. 클라이언트 컴포넌트는 서버 측과 클라이언트 측 두 곳에서 동일하게 실행되기 때문이다.

 

Server Actions

Server Actions이란?

  • 브라우저에서 특정 폼의 제출 이벤트가 발생했을 때 서버에서만 실행되는 함수를 브라우저가 직접 호출하면서 데이터까지 폼 데이터 형식으로 전달할 수 있게 해주는 기능이다.
  • 기존엔 API를 통해서만 진행했어야 하는 브라우저와 서버 간의 데이터 통신을 오직 JS 함수 하나만으로 쉽고 간결하게 설정할 수 있다.

Server Actions 사용

  • 비동기 함수 안에 ‘use server’ 지시자를 작성하고 그 함수를 form 태그의 action 속성에 작성하면 된다.
  • 서버 액션을 만들게 되면 코드를 실행하는 API가 하나 생성이 되고 API는 브라우저에서 form을 제출했을 때 자동으로 호출이 된다.

useActionState 훅

클라이언트 컴포넌트에서 폼 태그의 상태를 쉽게 핸들링할 수 있는 여러가지 기능을 가지고 있다.

 

인수

  • 첫 번째 인수로 핸들링하려는 폼의 액션 함수를 넣는다.
  • 두 번째 인수로 상태의 초기값을 넣는다.

반환값

  • 첫 번째 반환값으로 폼의 state가 반환된다.
  • 두 번째 반환값으로 폼의 actions을 의미하는 formAction이라는 함수가 반환된다.
  • 세 번째 반환값으로 폼의 로딩 상태를 의미하는 isPending이라는 값이 반환된다.

 

실시간 재검증

revalidatePath 메서드는?

  • Next.js 서버 측에게 revalidatePath 함수의 인수로 전달한 경로에 해당하는 다시 생성해 줄 것을 요청하는 메서드다.
  • 오직 서버 측에서만 호출할 수 있기 때문에 클라이언트 컴포넌트에서는 호출할 수 없다.
  • 경로 페이지에 포함된 모든 캐시들까지 전부 다 무효화 시키기 때문에 데이터 캐시, 풀 라우트 캐시도 함께 삭제가 된다.

revalidatePath 메서드 옵션

  • 옵션값X (기본값)
    • 특정 주소의 해당하는 페이지만 재검증
  • (”컴포넌트 경로”, “page”)
    • 특정 경로의 모든 동적 페이지를 재검증
    • 실제 브라우저에 나타나는 경로를 명시하는 게 아니라 해당 페이지 컴포넌트가 작성된 폴더 또는 파일의 경로를 명시해야 한다.
  • (”/”, “layout”)
    • 모든 데이터를 재검증

 

이미지 최적화

Next.js의 Image 컴포넌트를 활용하면 적용할 수 있는 최적화 기법

  • webp, AVIF 등의 차세대 형식으로 변환하기
  • 디바이스 사이즈에 맞는 이미지 불러오기
  • 레이저 로딩 적용하기
  • 블러 이미지 활용하기

 

검색 엔진 최적화 (SEO)

검색 엔진 최적화 방법

  • Sitemap 설정하기
  • RSS 발행하기
  • 시멘틱 태그 설정하기
  • 메타데이터 설정하기

metadata라는 변수를 선언해서내 보내면 metadata 변수에 설정된 값이 자동으로 해당 페이지의 메타데이터로 설정된다.

 

generateMetadata라는 함수는 해당 페이지에 필요한 메타데이터를 동적으로 생성해준다.

 

 

출처 : 한 입 크기로 잘라먹는 Next.js