11.1 app 디렉터리의 등장
Next.js 12버전까지는 페이지 공통 레이아웃을 유지하려면 _app을 사용하는 것이 유일했다. 이 방식은 제한적이고 각 페이지별로 다른 레이아웃을 유지하는 방법이 부족했다. 이를 극복하기 위해 나온 것이 app 레이아웃이다.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
appDir: true, //이 옵션을 experimental 아래에서 활성화해야 한다.
},
}
module.exports = nextConfig
Next.js 13.4.0 버전 이하라면 next.config.js에 위와 같이 옵션을 활성화해야 app기반 라우팅을 사용할 수 있다.
11.1.1 라우팅
/pages -> /app 라우터 방식의 변화
파일명으로 라우팅하는 것이 불가능함
라우팅을 정의하는 법
Next.js 13은 파일명이 무시되며 폴더명까지만 주소로 변환된다.
layout.js
페이지의 기본적인 레이아웃을 구성하는 요소로 해당 폴더에 layout이 있다면 그 하위 폴더 및 주소에 모두 영향을 미친다.
주소별 공통 UI를 포함할 수 있을 뿐만 아니라 _app과 _document를 대신해 웹페이지를 시작하는 데 필요한 공통 코드를 삽입할 수도 있다. 이 공통 코드는 기존의 _app과 _document처럼 모든 애플리케이션에 영향을 미치지 않고 오로지 자신과 자식 라이팅에만 미치게 된다.
또 _document.js를 사용하지 않아도 되며, HTML에서 기본으로 제공하는 <html/> 등의 태그를 추가하고 수정함으로써 별도로 import하는 번거로움이 사라졌다.
layout 사용 시 주의할 점
- app 디렉터리 내부에서는 예약어다. 무조건 layout.{js|jsx}ts|tsx}로 사용해야 한다.
- children을 props로 받아서 렌더링해야 한다.
- layout 내부에는 반드시 export default로 내보내는 컴포넌트가 있어야 한다.
- layout 내부에서도 API 요청과 같은 비동기 작업을 수행할 수 있다.
page.js
page도 예약어이며, 이전에 다뤘던 페이지 그대로를 의미한다.
page가 받는 props는 다음과 같다.
- params: 옵셔널 값으로, 앞서 설명한 [...id]와 같은 동적 라우트 파라미터를 사용할 경우 해당 파라미터에 값이 들어온다.
- searchParams: URL에서 ?a=1과 같은 URLSearchParams를 의미한다. ?a=1로 접근할 경우 searchParams에는 자바스크립트 객체 값이 오게 된다. 이 값은 layout에서 제공되지 않는다. layout은 페이지 탐색 중에는 리렌더링을 수행하지 않기 때문이다. search parameter에 의존적인 작업을 해야 한다면 반드시 page 내부에서 수행해야 한다.
page 사용 규칙
디렉터리 내부의 예약어로, 무조건 page.{js|jsx}ts|tsx}로 사용해야 한다.
내부에는 반드시 export default로 내보내는 컴포넌트가 있어야 한다.
error.js
해당 라우팅 영역에서 사용되는 공통 에러 컴포넌트다. 특정 라우팅별로 서로 다른 에러 UI를 렌더링할 수 있다.
에러 정보를 담고 있는 error: Error 객체와 에러 바운더리를 초기화할 reset: ()=> void를 props로 받는다.
에러 바운더리는 클라이언트에서만 작동하므로 error 컴포넌트도 클라이언트 컴포넌트여야 한다.
같은 수준의 layout에서 에러가 발생할 경우 해당 error 컴포넌트로 이동하지 않는다. Layout에서 발생한 에러를 처리하고 싶다면 상위 컴포넌트의 error를 사용하거나, app의 루트 에러 처리를 담당하는 app/global-error.js 페이지를 생성하면 된다.
not-found.js
특정 라우팅 하위의 주소를 찾을 수 없는 404 페이지를 렌더링할 때 사용된다.
전체 애플리케이션에서 404를 노출하고 싶다면 app/not-found.js를 생성해 사용하면 된다.
서버 컴포넌트로 구성하면 된다.
loading.js
리액트 Suspense를 기반으로 해당 컴포넌트가 불러오는 중임을 나타낼 때 사용한다.
route.js
/app/api를 기준으로 디렉터리 라우팅을 지원하며 파일명에 대한 라우팅이 없어진 것처럼 /api에 대한 파일명 라우팅도 없어졌다. 대신 디렉터리가 라우팅 주소를 담당하며 파일명은 route.js로 통일됐다.
route.ts 파일 내부에 REST API의 get, post와 같은 메서드명을 예약어로 선언해 두면 HTTP 요청에 맞게 해당 메서드를 호출하는 방식으로 작동한다. 이는 app/api 외에서 선언해도 작동한다.
route.ts가 존재하는 폴더 내부에는 page.tsx를 만들면 안된다.
route의 함수들이 받을 수 있는 파라미터는 다음과 같다.
- request: NextRequest 객체이며, fetch의 Request를 확장한 Next.js만의 Request라고 보면 된다. 이 객체는 API 요청과 관련된 cookie, headers 등 뿐만 아니라 nextUrl 같은 주소 객체도 확인할 수 있다.
- context: params만을 가지고 있는 객체이며, 이 객체는 앞서 파일 기반 라우팅에서 언급한 것과 동일한 동적 라우팅 파라미터 객체가 포함된다. 주소의 필요에 따라 원하는 형식으로 선언하면 된다.
11.2 리액트 서버 컴포넌트
11.2.1 기존 리액트 컴포넌트와 서버 사이드 렌더링의 한계
- 자바스크립트 번들 크기가 0인 컴포넌트를 만들 수 없다.
- 백엔드 리소스에 대한 직접적인 접근이 불가능하다.
- 자동 코드 분할(code split)이 불가능하다.
- 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어렵다.
- 추상화에 드는 비용이 증가한다.
이러한 문제는 모두 리액트가 클라이언트 중심으로 돌아가기 때문에 발생하는 것이다.
서버 사이드 렌더링은 정적 콘텐츠를 빠르게 제공하고, 서버에 있는 데이터에 손쉽게 제공할 수 있지만 사용자의 인터렉션에 따른 다양한 사용자 경험을 제공하기 어렵다.
클라이언트 사이드 렌더링은 사용자의 인터렉션에 따라 다양하게 제공하지만 서버에 비해 느리고 데이터를 가져오는 것이 어렵다.
두 구조의 장점을 모두 취하고자 한 것이 바로 리액트 서버 컴포넌트이다.
11.2.2 서버 컴포넌트란?
하나의 언어, 하나의 프레임워크, 그리고 하나의 API와 개념을 사용하면서 서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법을 의미한다.
서버에서 할 수 있는 일은 서버가 처리하고, 나머지 작업은 클라이언트인 브라우저에서 수행한다.
클라이언트 컴포넌트는 서버 컴포넌트를 import할 수 없다는 것을 주의해야 한다.
이론에 따르면 모든 컴포넌트는 서버 컴포넌트가 될 수 있고 클라이언트 컴포넌트가 될 수도 있다. 위에 그림처럼 혼재된 상황이 가능할 수 있는 이유는 children으로 자주 사용되는 ReactNode가 있기 때문이다.
예제 733p를 보면 서버 컴포넌트와 클라이언트 컴포넌트가 있으며 동시에 두 군데에서 모두 사용할 수 있는 공용 컴포넌트가 있다는 것이다. 이 세 컴포넌트의 차이와 제약 사항은 다음과 같다.
서버 컴포넌트
- 요청이 오면 그 순간 서버에서 딱 한번 실행될 뿐이므로 상태를 가질 수 없다. 리액트 상태 훅을 쓸 수 없다.
- 렌더링 생명주기를 사용할 수 없다. 한번 렌더링되면 끝이다. useEffect, useLayoutEffect를 쓸 수 없다.
- effect나 state에 의존하는 사용자 정의 훅도 사용할 수 없다. 서버에 제공할 수 있는 기능만 사용하는 훅만 쓸 수 있다.
- 서버에서만 실행되기 때문에 DOM API나 window, documenet 등에 접근할 수 없다.
- 서버에만 있는 데이터를 async/await로 접근할 수 있고 컴포넌트 자체에 async가 가능하다.
- 다른 서버 컴포넌트를 렌더링하거나 div, span, p 같은 요소를 렌더링하거나, 혹은 클라이언트 컴포넌트를 렌더링할 수 있다.
클라이언트 컴포넌트
- 브라우저 환경에서만 실행되므로 서버 컴포넌트를 불러오거나, 서버 전용 훅, 유틸리티를 불러올 수 없다.
- 서버 컴포넌트가 클라이언트 컴포넌트를 렌더링하는데, 클라이언트 컴포넌트가 자식으로 서버 컴포넌트를 갖는 구조는 가능하다.
- 위 두가지를 제외하면 리액트 컴포넌트와 같다.
공용 컴포넌트
- 서버와 클라이언트 모두에서 사용할 수 있다. 단 위 두가지의 제약을 모두 받는다.
리액트는 모든 컴포넌트를 공용 컴포넌트로 판단하여 모두 서버에서 실행 가능한 것으로 분류한다. 그래서 클라이언트 컴포넌트라는 것을 명시적으로 선언하려면 파일의 맨 첫 줄에 "use client"라고 작성하면 된다.
11.2.3 서버 사이드 렌더링과 서버 컴포넌트의 차이
서버 사이드 렌더링은 응답받은 페이지 전체를 HTML로 렌더링하는 과정을 서버에서 수행한 후 그 결과를 클라이언트에 내려준다. 이후 클라이언트에서 하이드레이션 과정을 거쳐 서버의 결과물을 확인하고 이벤트를 붙이는 등의 작업을 수행한다. 정적 HTML을 빠르게 내려주는데 초점을 둔다.
서버 컴포넌트를 활용해 서버에서 렌더링할 수 있는 컴포넌트는 서버에서 완성해서 제공받은 다음, 클라이언트 컴포넌트는 서버 사이드 렌더링으로 초기 HTML으로 빠르게 전달 받을 수 있다.
11.2.4 서버 컴포넌트는 어떻게 작동하는가?
예제 737p
- 서버가 렌더링 요청을 받는다. 서버가 렌더링 과정을 수행해야 하므로 리액트 서버 컴포넌트를 사용하는 모든 페이지는 항상 서버에서 시작된다. 즉, 루트에 있는 컴포넌트는 항상 서버 컴포넌트다. 구조는 다음과 같다.
이 예제에서 /react라고 하는 주소로 요청을 보내면 서버는 브라우저의 요청을 받고 서버 렌더링을 시작한다. - 서버는 받은 요청에 따라 컴포넌트를 JSON으로 직렬화한다. 이때 서버에서 렌더링할 수 있는 것은 직렬화해서 내보내고, 클라이언트 컴포넌트로 표시된 부분은 해당 공간을 플레이스홀더 형식으로 비워두고 나타낸다. 브라우저는 이후에 이 결과물을 받아서 다시 역직렬화한 다음 렌더링을 수행한다.
- 브라우저가 리액트 컴포넌트 트리를 구성한다. 브라우저가 서버로 스트리밍으로 받은 JSON 구문을 다시 파싱하여 트리를 재구성한다. 클라이언트 컴포넌트를 받았다면 클라이언트에서 렌더링을 진행하고 서버에서 만들어진 결과물을 받았다면 이를 기반으로 리액트 트리를 그대로 만든다.
서버 컴포넌트의 작동 방식의 특별한 점
- 서버에서 클라이언트로 정보를 보낼 때 스트리밍 형태로 보냄으로써 클라이언트가 줄 단위로 JSON을 읽고 컴포넌트를 렌더링할 수 있어 브라우저에서는 되도록 빨리 사용자에게 결과물을 보여줄 수 있다는 장점이 있다.
- 컴포넌트들이 하나의 번들러 작업에 포함돼 있지 않고 각 컴포넌트별로 번들링이 별개로 돼 있어 필요에 따라 컴포넌트를 지연해서 받거나 다로 받는 등의 작업이 가능해졌다.
- 마지막으로, 서버 사이드 렌더링과는 다르게 결과물이 HTML이 아닌 JSON 형태로 보내진 것 또한 주목해 볼 만하다. 클라이언트의 최종 목표는 리액트 컴포넌트 트리를 서버 컴포넌트와 클라이언트 컴포넌트의 두 가지로 조화롭게 구성하는 것으로, 이는 단순히 HTML을 그리는 작업 이상의 일을 필요로 한다. 따라서 HTML 대신 단순한 리액트 컴포넌트 구조를 JSON으로 받아서 리액트 컴포넌트 트리의 구성으로 최대한 빠르게 할 수 있도록 도와준다.
- 서버 컴포넌트에서 클라이언트 컴포넌트로 props를 넘길 때 반드시 직렬화 가능한 데이터를 넘겨야 하는 제약이 있다.
11.3 Next.js에서의 리액트 서버 컴포넌트
서버 컴포넌트는 클라이언트 컴포넌트를 불러올 수 없으며, 클라이언트 컴포넌트는 서버 컴포넌트를 children props로 받는 것만 가능하다.
page.js와 layou.js는 반드시 서버 컴포넌트여야 한다.
11.3.1 새로운 fetch 도입과 getServerSideProps, getStaticProps, getInitialProps의 삭제
getServerSideProps, getStaticProps, getInitialProps가 /app 디렉터리 내부에서 삭제된 대신, 모든 데이터 요청은 웹에서 제공하는 표준 API인 fetch를 기반으로 이뤄진다.
서버에서 데이터를 직접 불러올 수 있게 되었고, 컴포넌트가 비동기적으로 작동이 가능하다. 그래서 서버 컴포넌트는 데이터가 불러오기 전까지 기다렸다가 데이터가 불러와지면 비로소 페이지가 렌더링되어 클라이언트로 전달된다.
또한 같은 서버 컴포넌트 트리 내에서 동일한 요청이 있을 시 재요청이 발생하지 않도록 중복을 방지한다.
SWR과 React Query와 비슷하게, 해당 fetch 요청에 대한 내용을 서버에서는 렌더링이 한 번 끝날 때까지 캐싱하며, 클라이언트에서는 별도의 지시자나 요청이 없는 이상 해당 데이터를 최대한 캐싱해서 중복된 요청을 방지한다.
11.3.2 정적 렌더링과 동적 렌더링
Next.js에서는 정적인 라우팅에 대해서 기본적으로 빌드 타임에 렌더링을 미리 해두고 캐싱해 재사용할 수 있게끔 해뒀고, 동적인 라우팅에 대해서는 서버에 매번 요청이 올 때마다 컴포넌트를 렌더링하도록 변경했다.
예제 743p에서는 특정 API 엔드 포인트에서 데이터를 불러와 페이지에서 렌더링하는 구조를 가진 서버 컴포넌트인데 주소가 정적으로 결정돼 있어 빌드 시 해당 주소로 미리 요청을 해 데이터를 가져온 뒤 렌더링한 결과를 빌드에 넣어둔다.
정적으로 캐싱하지 않는 방법으로는 아래와 같이 옵션을 추가하는 것이다. 이렇게 하면 Next.js는 해당 요청을 미리 빌드해서 대기시켜 두지 않고 요청이 올 때마다 fetch 요청 이후에 렌더링을 수행하게 된다.
async function fetchData() {
const res = await fetch (
"api 주소",
{ cache: 'no-cache' },
//{ next: {revalidate: 0}} 넥스트에서 제공하는 옵션으로 동일한 기능
)
//생략
}
이 밖에도 함수 내부에서 Next.js가 제공하는 next/headers나 next/cookie 같은 헤더 정보와 쿠키 정보를 불러오는 함수를 사용하면 해당 함수는 동적인 연산을 바탕으로 결과를 반환하는 것으로 인식해 정적 렌더링 대상에서 제외된다.
동적인 주소이지만 특정 주소에 대해서 캐싱하고 싶은 경우 generateStaticParams 함수를 사용하면 된다. 예제 745p
export async function generateStaticParams() {
return ...생략
}
// 생략
fetch 옵션에 따른 작동 방식
- fetch(URL, { cache: 'force-cache' }): 기본값으로 getStaticProps와 유사하게 불러온 데이터를 캐싱해 해당 데이터로만 관리한다.
- fetch(URL, { cache: 'no-store' }), fetch(URL, { next: {revalidate: 0} }): getServerSideProps와 유사하게 캐싱하지 않고 매번 새로운 데이터를 불러온다.
- fetch(URL, { next: {revalidate: 10} }): getStaticProps에 revalidate를 추가한 것과 동일하며, 정해진 유효시간 동안에는 캐싱하고, 이 유효시간이 지나면 캐시를 파기한다.
11.3.3 캐시와 mutating, 그리고 revalidating
Next.js는 fetch의 기본 작동을 재정의해 { next: {revalidate?: number | false}}를 제공하는데, 이를 바탕으로 해당 데이터의 유효한 시간을 정해두고 이 시간이 지나면 다시 데이터를 불러와서 페이지를 렌더링하는 것이 가능하다.
이는 페이지에 revalidate라는 변수를 선언해서 페이지 레벨로 정의하는 것도 가능하다.
// app/page.tsx
export const revalidate = 60
이렇게 하면 하위에 있는 모든 라우팅에서는 페이지를 60초 간격으로 갱신해 새로 렌더링하게 된다. 또 fetch 내부에 옵션으로 제공하는 것도 가능하다. 과정은 다음과 같다.
- 최초로 해당 라우트로 요청이 올 때는 미리 정적으로 캐시해 둔 데이터를 보여준다.
- 이 캐시된 초기 요청은 revalidate에 선언된 값만큼 유지된다.
- 만약 해당 시간이 지나도 일단은 캐시된 데이터를 보여준다.
- Next.js는 캐시된 데이터를 보여주는 한편, 시간이 경과했으므로 백그라운ㄷ에서 다시 데이터를 불러온다.
- 위 작업이 성공적으로 끝나면 캐시된 데이터를 갱신하고, 그렇지 않다면 과거 데이터를 보여준다.
만약 이러한 캐시를 전체적으로 무효화하고 싶다면 router에 추가된 refresh 메서드로 router.refresh();를 사용하면 된다. 이 작업은 오로지 서버에서 루트부터 데이터를 전체적으로 가져와 갱신하고 브라우저나 리액트의 state에는 영향을 미치지 않는다.
11.3.4 스트리밍을 활용한 점진적인 페이지 불러오기
과거 : 서버 사이드 렌더링은 요청 받은 페이지를 모두 렌더링해서 내려줄 때까지 아무것도 보여주지 않음. 페이지를 다 받더라고 정적인 페이지로 인터렉션 불가
현재 : HTML을 작은 단위로 쪼개서 완성되는 대로 클라이언트로 점진적으로 보내는 스트리밍 도입. 모든 데이터가 로드될 때까지 기다리지 않고 먼저 로드된 컴포넌트를 보여줘서 사용자가 일부라도 페이지와 인터렉션 할 수 있음. 핵심 웹 지표인 최초 바이트까지의 시간과 최초 콘텐츠풀 페인티을 개선하는데 큰 도움이 됨.
스트리밍을 활용할 수 있는 방법
- 경로에 loading. tsx 배치: Loading은 앞서 잠깐 소개한 것처럼 예약어로 존재하는 컴포넌트로, 렌더링이 완료되기 전에 보여줄 수 있는 컴포넌트를 배치할 수 있는 파일이다. Loading 파일을 배치한다면 자동으로 다음 구조와 같이 Suspense 가 배치된다.
< Layout>
<Header />
<SideNav />
<!- 여기에 로딩이 온다.-->
<Suspense fallback={<Loading /›}>
<Page />
</Suspense>
</ Layout>
- Suspense 배치: 좀 더 세분화된 제어를 하고 싶다면 직접 리액트의 Suspense를 배치하는 것도 가능하다.
import { Suspense } from 'react'
import { Notes, Peoples } from '. /Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<Skeleton />}>
<Notes />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Peoples />
</Suspense>
</section>
)
}
Loading이 Suspense를 기반으로 만들어진 Next.js의 규칙이기 때문에 직접 Suspense를 사용하는 것도 동일 한 혜택을 누릴 수 있다. 스트리밍을 활용해 서버 렌더링이 가능해지고, 리액트는 로딩이 끝난 컴포넌트 순서대로 하이드레이션을 수행해 가능한 한 사용자에게 빠르게 상호작용이 가능한 페이지를 제공할 수 있다.
11.4 웹팩의 대항마, 터보팩의 등장(beta)
터보팩은 웹팩 대비 최대 700배, Vite 대비 최대 10배 빠르다고 하며, 이는 앞서 소개한 라이브러리와 마찬가지로 러스트 기반으로 작성됐다. 다만 Next.js 13.1.x를 기준으로 베타이며, 현재는 개발 모드에서만 제한적으로 사용 가능하다.
next dev --turbo 명령어로 개발 환경에서 터보팩을 활성화 해서 개발할 수 있고,
next build --turbo 명령어를 통해 최종 빌드 산출물에서도 터보팩을 곧 사용할 수 있다고 한다.
11.5 서버 액션(alpha)
새로운 기능인 서버 액션은 API를 굳이 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있는 기능이다.
서버 컴포넌트와 다르게, 특정 함수 실행 그 자체만을 서버에서 수행할 수 있다는 장점이 있다. 그리고 그 실행 결과에 따라 다양한 작업을 수행할 수도 있다.
이 서버 액션을 활성화하려면 next.config.js에서 실험 기능을 다음과 같이 활성화해야 한다.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
- 함수 내부 또는 파일 상단에 클라이언트 선언과 비슷하게 'use server" 지시자 를 선언해야 한다.
- 함수는 반드시 async여야 한다.
async function serverAction() {
"use server";
//서버에 바로 접근하는 코드
}
// 이 파일 내부의 모든 내용이 서버 액션으로 간주된다.
'use server'
export async function myAction() {
//...
// 서버에 바로 접근하는 코드
}
11.5.1 form의 action
<form/>은 HTML에서 양식을 보낼 때 사용하는 태그로, action props를 추가해서 이 양식 데이터를 처리할 URI를 넘겨줄 수 있다.
export default function Page() {
async function handleSubmit() {
'use server'
console.log('해당 작업은 서버에서 수행합니다. 따라서 CORS 이슈가 없습니다.')
const response = await fetch( 'https://jsonplaceholder.typicode.com/posts', {
method: 'post'
body: JSON. stringify({
title: 'foo',
body: 'bar',
userId: 1,
}),
headers: {
'Content-type': 'application/ison; charset=UTF-8',
},
})
const result = await response.json()
console. log(result)
}
return (
<form action=(handleSubmit} >
<buton type-"Submit">form 요청 보내보기</button>
)
}
form.action에 handleSubmit이라는 서버 액션을 만들어 props로 넘겨줬다. 이 handlesubmit 이벤트를 발생시키는 것은 클라이언트지만 실제로 함수 자체가 수행되는 것은 서버가 된다.
위 예제를 크롬 개발자 도구 네트워크 탭에서 보면 페이로드에는 앞서 코드에서 보낸 post 요청이 아닌 ACTION ID라는 액션 구분자만 있다. 그리고 이를 처리하는 서버에서는 다음과 같이 미리 빌드 돼 있다.
//.next/server/app/server-action/form/page.js
// 해당 페이지에서 수행하는 서버 액션을 모아둔다.
const actions = {
// 앞서 페이로드에서 본 액션 아이디를 확인할 수 있다
dBb22C18e02d4e94b1ed17222f5f945a78603861: () =>
Promise.resolve(/* import() eager */)
.then(__webpack require__.bind(__webpack_require__, 5948))
.then((mod) => mod['$$ACTION_0']),
}
//...
// 해당 페이지
function Page() {
async function handleSubmit() {
return $SACTION_0(handleSubmit.$$bound)
}
//...
}
//...
async function $$ACTION_0(closure) {
console.Log('해당 작업은 서버에서 수행합니다. 따라서 CORS 이슈가 없습니다. ')
const response = await fetch ('https://jsonplaceholder.typicode.com/posts', {
method: 'post',
body: JSON. stringify({
title: 'foo',
body: 'bar',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const result = await response.json()
console.log(result)
}
서버 액션을 실행하면 클라이언트에서는 현재 라우트 주소와 ACTION-ID만 보내고 그 외에는 아무것도 실행하지 않는 것을 알 수 있다.
그리고 서버에서는 요청받은 라우트 주소와 ACTION ID를 바탕으로, 실행해야 할 내용을 찾고 이를 서버에서 직접 실행한다. 이를 위해 'use server'로 선언돼 있는 내용을 빌드 시점에 미리 클라이언트에서 분리시키고 서버로 옮김으로써 클라이언트 번들링 결 과물에는 포함되지 않고 서버에서만 실행되는 서버 액션을 만든 것을 확인할 수 있다.
서버 액션의 장점은 여기서 그치지 않는다. 폼과 실제 노출하는 데이터가 연동돼 있을 때 더욱 효과적이다. 예제 754p 참고
위 예제들을 볼 때 주목해야하는 것은 모든 과정이 페이지 새로고침없이 수행된다는 것이다. 최초에 페이지를 서버에서 렌더링한 이후에 폼에서 handleSubmit으로 서버에 데이터 수정을 요청하는 것, 그리고 수정된 결과를 다시금 조회해서 새로운 결과로 렌더링하는 일련의 과정이 모두 페이지 새로고침 없이 데이터 스트리밍으로 이뤄진다.
따라서 개발자들은 서버에 데이터 수정을 요청하는 한편, 클라이언트에서는 업데이트를 완료한 후 새로운 결과를 받을 때까지 사용자에게 로딩 중이라는 것을 알 수 있는 인터랙션을 구성할 수도 있다.
한 가지 더 주목해야 할 것은 handleSubmit에서 수행했던 revalidatePath다. 이는 인수로 넘겨받은 경로의 캐시를 초기화해서 해당 URL에서 즉시 새로운 데이터를 불러오는 역할을 한다. Next.js에서는 이를 server mutation(서버에서의 데이터 수정)이라고 하는데, server mutation으로 실행할 수 있는 함수는 다음과 같다.
- redirect: import { redirect } from 'next/navigation'으로 사용할 수 있으며, 특정 주소로 리다이렉트할 수 있다.
- revalidatePath: import { revalidatePath } from 'next/cache' 사용할 수 있으며, 해당 주소의 캐시를 즉시 업데이트한다.
- revalidateTag: import { revalidateTag } from 'next/cache'로 사용할 수 있다. 캐시 태그는 fetch 요청 시에 다음과 같이 추가할 수 있다.
fetch ('https://localhost: 8088/api/something', { next: { tags: [''] } })
- 이렇게 태그를 추가해 두면 여러 다양한 fetch 요청을 특정 태그 값으로 구분할 수 있으며, revalidateTag를 사용할 경우 이 특정 태그가 추가된 fetch 요청을 모두 초기화한다.
11.5.2 input의 submit과 image의 formAction
form.action과 마찬가지로 input type="submit" 또는 input type="image"에 formAction prop으로도 서버 액션을 추가할 수 있다.
11.5.3 startTransiton과의 연동
useTransition에서 제공하는 startTransiton에서도 서버 액션을 활용할 수 있다.
예제 758p
에서는 useTransiton이 반환하는 배열의 두 번째 요소인 startTransiton을 사용해 서버 액션을 실행한다. 이전과 동일한 로직을 구현하면서도 page 단위의 loading.jsx를 사용하지 않아도 된다는 것이다. isPending을 활용해 컴포넌트 단위로 로딩 처리도 가능하다. 또 revalidatePath와 같은 server mutation도 처리할 수 있다.
11.5.4 server mutation이 없는 작업
export default function Page() {
async function handleClick() {
'use server'
// server mutation이 필요 없는 작업
}
return <button onclick={handleClickr}>form 요청 보내보기</button>
}
11.5.5 서버 액션 사용 시 주의할 점
- 서버 액션은 클라이언트 컴포넌트 내에서 정의될 수 없다. 클라이언트 컴포넌트에서 서버 액션을 쓰고 싶다면 startTransition 예제처럼 'use server'로 서버 액션만 모여 있는 파일을 별도로 import해야 한다.
- 서버 액션을 import하는 것뿐만 아니라, props 형태로 서버 액션을 클라이언트 컴포넌트에 넘기는 것 또한 가능하다. 서버에서만 실행될 수 있는 자원은 반드시 파일 단위로 분리해야 한다.
11.6 그 밖의 변화
- 프로젝트 전체 라우트 에서 쓸 수 있는 미들웨어가 강화
- SEO(Search Engine Optimization)를 쉽게 작성할 수 있는 기능 이 추가
- 정적으로 내부 링크를 분석할 수 있는 기능 등
11.7 Next.js 13 코드 맛보기
11.7.1 getServerSideProps와 비슷한 서버 사이드 렌더링 구현해 보기
Next.js 13과 리액트 18에서는 서버 컴포넌트라면 어디든 서버 관련 코드를 추가할 수 있다.
서버 컴포넌트에서 fetch를 수행하고, 이 fetch에 별다른 cache 옵션을 제공하지 않는다면 기존의 getServerSideProps와 매우 유사하게 작동한다.
예제 761p ~
Next js 13에서도 여전히 서버 사이드 렌더링과 비슷하게 서버에서 미리 페이지를 렌더링해서 내려받는 것이 가능하다.
여기에 추가로 눈여겨봐야 할 것은 이 HTML 태그 뒤에 오는 <script>다.
과거 getServerSideProps를 사용하는 애플리케이션에서는 <script id="__ NEXT_DATA__" type="application/json">라고 하는 특별한 태그가 추가돼 있었고, 이 서버에서 미리 만들어진 정보를 바탕으로 클라이언트에서 하이드레이션을 수행했었다.
리액트 18에서는 서버 컴포넌트에서 렌더링한 결과를 직렬화 가능한(JsoN.stringify가 가능한) 데이터로 클라이언트에 제공하고, 클라이언트는 이를 바탕으로 하이드레이션을 진행하게 된다. 각 스크립트는 하나의 서버 컴포넌트 단위를 의미하며, 예제 코드의 마지막 스크립트 에서 이 마지막 서버 컴포넌트의 흔적을 발견할 수 있다.
서버 컴포넌트의 렌더링 결과를 컴포넌트별로 직렬화된 데이터로 받아 이 데이터를 바탕으로 클라이언트에서 하이드레이션하는 사용하는 것을 알 수 있다.
11.7.2 getStaticProps와 비슷한 정적인 페이지 렌더링 구현해 보기
Next.js 13에서 app 디렉터리가 생겨나면서 fetch의 cache를 이용해 정적 페이지를 구현할 수 있다.
정적으로 미리 빌드해 두는 것뿐만 아니라 캐시를 활용하는 것도 가능하다. 이러한 방식을 Next.js에서는 'Incremental Static Regeneration'이라고 하는데, 정적으로 생성된 페이지를 점진적으로 갱신하는 것을 의미한다. Next.js에서 제공하는 캐시와 관련된 기능을 활용하면 일정 기간 동안은 캐시를 통해 가져와 빠르게 렌더링하고, 시간이 지나면 새롭게 데이터를 불러오는 방식으로 페이지를 구성할 수 있다.
11.7.3 로딩, 스트리밍, 서스펜스
Next.js 13에서는 스트리밍(streaming)과 리액트의 서스펜스(suspense)를 활용해 컴포넌트가 렌더링 중 이라는 것을 나타낼 수 있다. 직접 Suspense로 감싸 부분적으로 로딩을 보여주는 것 외에도 기본적으로 loading이라고 하는 파일 예약어를 지원하면서 손쉽게 로딩 라우팅별로 로딩 상태를 나타낼 수 있도록 제공 한다. loading과 Suspense 모두 동일한 방식으로 작동하며, Suspense가 조금 더 개발자가 원하는 형태로 쪼개서 보여줄 수 있다는 차이만 있다.
스트림을 통해 내려오는 데이터 단위(chunk)를 확인해보면, 최초 데이터는 서버에서 fetch 등의 작업을 기다릴 필요가 없는 Suspense 내부의 로딩 데이터가, 이후부터 fetch가 끝나면서 렌더링이 완료된 컴포넌트의 데이터를 하나씩 내려주는 것을 볼 수 있다.
이는 Nex.js 13과 리액트 18이 서버 컴포넌트의 렌더링과 이를 클라이언트에 제공하기 위해 스트리밍을 사용하고 있다는 증거다.
11.8 정리 및 주의사항
- 서버 컴포넌트는 파일 단위로 "use client"를 경계로 서버와 클라이언트 컴포넌트를 명확하게 구분해야 하고, 나아가 라이브러리 또한 이 경계에 맞춰서 사용해야 한다.
- Next.js의 패러다임을 공부하고 싶다면 반드시 새로운 문서 페이지인 https://nextjs.org/docs를 방문해서 확인해야 한다. 기존 사용자라면 Next.js 13 미만 버전에 대한 마이그레이션 가이드를 먼저 읽어보고, 한 번도 사용해보지 않았다면 App Router 페이지의 React Essentials를 정독해볼 것을 권한다.
'React-study > dil' 카테고리의 다른 글
[모던 리액트 Deep Dive] 10장 리액트 17과 18의 변경 사항 살펴보기 (3) | 2024.12.09 |
---|---|
[모던 리액트 Deep Dive] 9장 모던 리액트 개발 도구로 개발 및 배포 환경 구축하기 (0) | 2024.12.05 |
[모던리액트 Deep Dive] 7장 크롬 개발자 도구를 활용한 애플리케이션 분석 (2) | 2024.11.20 |
[모던 리액트 Deep Dive] 6장 리액트 개발 도구로 디버깅하기 (0) | 2024.11.18 |
[모던리액트 Deep Dive] 5장 리액트와 상태 관리 라이브러리 (4) | 2024.11.14 |