nextjs 문서의 server component 관련 내용을 살펴보다 더 자세히 이해하고 싶은 부분ㅇ이 있었습니다.
살펴보던 중,Server Component의 장점에 대해 설명하는 부분이 있는데요.
또한 문서에서는 server component에서 data fetching하는 것을 권장하고 있어요.
장점 중 하나로써 다음과 같이 설명하는 부분이 있습니다.
데이터 페칭: Server Components를 사용하면 데이터 소스에 더 가까운 서버에서 데이터 페칭을 수행할 수 있습니다. 이는 렌더링에 필요한 데이터를 가져오는 시간을 줄이고 클라이언트에서 요청해야 하는 횟수를 줄여 성능을 향상시킬 수 있습니다.
위 내용 중 조금 더 이해하고 싶은 부분은 다음이에요.
위 주장에 대한 이유는 문서가 자세히 설명치 않고 있는데요.
저는 위 주장에 대해 의문이 들었죠.
a. 클라이언트 컴포넌트(브라우저) => 백엔드 API 통신 (서버)
b. 서버컴포넌트 (nextjs 서버) => 백엔드 서버(서버)
위와 같이, a와 b라는 옵션이 있다고 하고 브라우저,next서버,백엔드 API 서버 3개다 물리적 위치가 동일한 한국에 있다고 했을 때, 시간적인 측면에서는 네트워크 응답시간이 동일하지 않을까?
라는 의문이 들었어요.
그렇다면 서버컴포넌트에서 통신하는 것이
이러한 의문이 들었던 것이죠.
그러면
a. 클라이언트 컴포넌트(브라우저) => 백엔드 API 통신 (서버)
b. 서버컴포넌트 (nextjs 서버) => 백엔드 서버(서버)
b가 a보다 데이터를 가져오는 시간을 줄일 수 있다는 것인데 어떻게 그럴 수 있는 것일까요?
다음과 같을 때, 데이터 가져오는 시간을 클라이언트보다 더 빨리 가져올 수 있습니다.
첫번째는 Static Rendering을 이유로 들 수 있습니다.
Static Rendering, 즉 SSG(Static Site Generation)방식인데요.
빌드 타임때, 페이지들을 완성하고 사용자가 해당 페이지로 요청하면 data fetching,rendering 필요없이 빌드타임 때 완성된 페이지를 응답해줍니다.
빌드타임때, 페이지 서버컴포넌트에 필요한 data fetching이 완료되어, 해당 페이지에서는 불필요하게 data fetching 일어날 필요가 없는 것이죠.
해당 페이지에 속해있는 클라이언트 컴포넌트 예외로 data fetching이 그대로 일어납니다.
클라이언트 컴포넌트에서는 위와 같은 상황에서 페이지를 구성한다고 하면 해당 페이지 진입할 때마다 data fetching 이루어져야합니다.
하지만 서버컴포넌트와 SSG 방식으로 페이지를 구성한다면 data fetching이 필요없는 것이죠.
즉, SSG 방식으로 페이지를 구성한다면 클라이언트 컴포넌트에 비해 데이터를 가져오는 시간을 줄일 수 있다고 말할 수 있죠.
예시 코드로 확인해보겠습니다.
아래 코드는 nextjs의 SSG 방식으로 이루어진 서버 컴포넌트입니다.
import ButtonContainer from "@/app/(page)/quiz/[detailUrl]/explanation/_components/buttonContainer";
import NextQuizButton from "@/app/(page)/quiz/[detailUrl]/explanation/_components/nextQuizButton";
import ReturnButton from "@/app/(page)/quiz/[detailUrl]/explanation/_components/returnButton";
import {quizApiHandler} from "@/app/services/quiz/QuizApiHandler";
import {Metadata} from "next";
import React from 'react';
import 'prismjs/themes/prism.css';
export async function generateStaticParams() {
const {data} = await quizApiHandler.fetchQuizDetailUrlList();
return data.map((url) => ({detailUrl:url}))
}
// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
params
}:{
params:{
detailUrl:string
}
}):Promise<Metadata>{
const detailUrl = (await params).detailUrl
const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)
return {
title:`해설-${data.metaTitle}`,
description: `해설-${data.metaDescription}`,
alternates:{
canonical:`/quiz/explanation/${data.detailUrl}`
}
}
}
async function Page({
params
}:{
params:{
detailUrl:string
}
}) {
const { detailUrl } = await params
const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)
return (
<>
<h1 className={"text-title1"}>{data.metaTitle} 해설</h1>
<div
className={"prose"}
dangerouslySetInnerHTML={{__html:data.explanation}}></div>
<ButtonContainer>
<ReturnButton returnUrl={detailUrl}/>
<NextQuizButton currentUrl={detailUrl}/>
</ButtonContainer>
</>
);
}
export default Page;
generateStaticParams
을 통해 정적 페이지를 생성할 params를 전달해주면 빌드 타임때, params에 대한 페이지를 빌드해줍니다.
빌드시, quizApiHandler.fetchQuizDetailByUrl
페이지 정보를 가져오는 API 호출도 같이 하여 페이지를 완성하여주죠.
빌드하고 배포를 한 뒤, 사용자가 해당 페이지에 접속하면 사용자는 API 요청을 할 필요가 없습니다.
API 요청도 완료되고 렌더링도 완료된,빌드 타임 때 완료된 페이지만 응답받으면 되는 것이죠.
위 페이지를 클라이언트 컴포넌트로 구성한다면 얘기가 달라집니다.
사용자가 해당 페이지에 들어갈 때마다 상세 데이터에 대한 data fetching을 해야하는 것이죠.
만약, 수천명의 사용자가 있다고 하면, 이는 많은 API 요청이 동시다발적으로 일어날 수 있는거죠.
하지만 SSG 방식으로 서버컴포넌트를 구성하면, 수천명이 동시다발적으로 페이지에 접근하여도 API 요청은 일어나지 않아도 됩니다. 완성된 페이지만 응답해주면 되니깐요.
즉, 다시 말해, SSG 방식으로 서버 컴포넌트를 구성한다면 클라이언트 컴포넌트를 구성하는 것보다 data fetching을 시간을 전체적으로 봤을 때, 줄일 수 있다고 말할 수 있겠습니다.
두번째로는 Cache의 사용을 이유로 들 수 있겠습니다.
nextjs 서버컴포넌트에서 활용할 수 있는 캐싱 전략으로 data fetching을 빨리 가져올 수 있다
의 뒷받침을 할 수 있습니다.
Nextjs 서버컴포넌트,data fetching 관점에서 두가지 캐싱 이점을 확인할 수 있습니다.
request-memoization은 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션해줍니다. React 컴포넌트 트리의 여러 곳에서 동일한 데이터를 가져오기 위한 fetch 함수를 호출할 때 한 번만 실행된다는 것을 의미합니다.
경로 전체에서 동일한 데이터를 사용해야 하는 경우(예: Layout, Page 및 여러 컴포넌트에서), 같은 API 요청을 여러번 해야할텐데요.
request-memoization은 여러번 API 요청을 하지않고 초기 API 요청 한번만 하고 그 뒤에 나머지 API 요청은 캐싱된 데이터를 사용할 수 있게 해줍니다.
API 요청을 할 때, 캐싱된 데이터를 가져온다면 API 서버에서 가져오는 시간보다 더 빠르게 가져올 수 있겠죠.
이를 통해, data fetching을 더 빨리 할 수 있다
라고 말할 수 있습니다.
Data Cache는 서버 컴포넌트에서 사용할 수 있는 전략으로 서버 요청과 배포 간에 데이터 페치를 지속적으로 유지하는 내장 데이터 캐시입니다.
초기에 data fetching을 한 뒤, 해당 API 응답 결과를 next 서버에 저장하고 있습니다.
이는 새로고침하여도 캐시가 날라가지 않고 서버를 종료하여도 날아가지 않습니다.
초기 API 통신을 하고 난 뒤에는 백엔드 API 통신을 통해 데이터를 가져올 필요없이 Data Cache에서 가져오면 됩니다.
이 또한, 캐싱된 데이터를 가져온다면 API 서버에서 가져오는 시간보다 더 빠르게 가져올 수 있겠죠.
이를 통해, data fetching을 더 빨리 할 수 있다
라고 말할 수 있습니다.
즉,서버컴포넌트에서 data fetching을 하면 nextjs의 request-memoization
와 Data Cache
를 통해 클라이언트 컴포넌트보다 더 빠르게 data fetching을 할 수 있다고 말할 수 있습니다.
맨 처음 문서에서 확인하면 다음과 같은 말이 있는데요.
데이터 페칭: Server Components를 사용하면 데이터 소스에 더 가까운 서버에서 데이터 페칭을 수행할 수 있습니다.
이것은 항상 더 가깝다고 보장할 수는 없습니다. 하지만 보통은 데이터 소스에 더 가깝게 서버들을 구성하죠.
예를 들어, 저의 경우, 프로젝트를 구성할 때 aws을 통해 서버 인스턴스들을 구성했는데요.
다음과 같이 구성했어요.
3개의 서버 다 aws 서울 데이터 센터에 존재합니다.
위와 같이 서버들이 구성되어있고 부산에 있는 사용자가 저의 웹사이트에 접속했다고 해볼게요.
서버 컴포넌트에서 data fetching할 때
페이지가 서버컴포넌트로 구성되어있을 때, 처음 페이지에 접근하면 부산에 있는 사용자의 컴퓨터에서 서울에 있는 next서버로 접근하겠죠.
next서버는 같은 데이터 센터에 있는 API서버로 data fetching을 하여 데이터를 받아와 페이지를 구성하여 부산에 있는 사용자에게 전달합니다.
사용자는 단지, 페이지에 대한 요청만 하면 되죠.
사용자 측면에서 렌더링에 필요한 통신 한번이면 됩니다.
클라이언트 컴포넌트에서 data fetching할 때
페이지가 클라이언트 컴포넌트로 구성되어있다면, 부산에 있는 사용자는 서울에 있는 next 서버로 우선 page에 대한 요청을 합니다.다음으로 페이지에 있는 API 요청을 서울에 있는 API 서버로 요청을 합니다.
사용자 측면에서 렌더링에 필요한 통신을 2번하게 되는 것이죠.
위와 같은 상황을 보았을 때에도 서버 컴포넌트에서 data fetching을 하는 것이 더 빠를 수 있다고 할 수 있을 것 같습니다.
next 서버와 API 서버가 같은 데이터 센터에서 통신을 하니 어디에 있을지 몰라 사용자가 API 통신하는 것보다 더 빠르게 응답해줄 수 있는거죠.
뿐만 아니라,만약 하나의 페이지에서 여러 API 요청을 해야한다면 어떨까요?
만약 특정 페이지에서 5개의 API 통신을 해야한다고 해보죠.
클라이언트 컴포넌트에서 통신한다면 미국에 있는 사용자가 5번을 서울에 있는 API 서버와 통신해야해요.
하지만 서버 컴포넌트에서 통신한다면 1번만 통신하면 됩니다. 서버 컴포넌트는 API 통신들을 병렬적으로 처리합니다.사용자가 서버컴포넌트로 이루어진 페이지에 대해 요청을 하면 서버는 5개의 API 통신을 병렬적으로 처리하여 사용자에게 응답해줍니다.
즉, 클라이언트 컴포넌트처럼 5번 일일히 요청을 할 필요가 없다는 것이죠.
위처럼 서버컴포넌트에서 통신을 하면 물리적 거리 단축의 장점과 API 에 대한 일괄 처리가 있기 때문에 data fetching 시간 절약에 도움이 될 수 있겠어요.
다만 브라우저와 서버, 데이터 소스가 동일한 물리적 위치에 있다면, 물리적 거리 단축에 따른 장점은 상대적으로 작을 수 있습니다.
다음으로 의문들었던 부분을 살펴보죠. 이미 답은 위에서 한번 언급되었네요.
서버 컴포넌트는 API 통신들을 병렬적으로 처리합니다.
위에서 언급했듯이 서버 컴포넌트는 API 통신들을 일괄적으로 처리합니다. 사용자가 특정 페이지에 접근하였을 때, 페이지에 있는 API들을 모두 통신한 다음, 해당 응답 정보를 html 렌더링한뒤, 사용자에게 전달해줍니다.
정확히 말하면 스트리밍 렌더링 기능을 사용하여 데이터가 모두 준비되지 않았더라도 가능한 HTML 부분을 먼저 사용자에게 전송할 수 있습니다.
아래와 같이 서버 컴포넌트로 구성된 페이지와 클라이언트 컴포넌트가 있다고 가정해보죠.
// server component
"use server"
import React from 'react';
async function Page() {
await fetch("https://api.server.com/a")
await fetch("https://api.server.com/b")
await fetch("https://api.server.com/c")
await fetch("https://api.server.com/d")
await fetch("https://api.server.com/e")
return (
<div></div>
);
}
export default Page;
서버 컴포넌트로 구성된 위 페이지에 사용자가 접근을 하면 사용자는 우선 위 페이지에 대한 요청을 먼저 합니다.
그 다음엔 서버에서는 해당 페이지에 대해 요청이 들어왔으니 페이지 내부에 있는 API 요청들을 백엔드 API와 통신을 합니다. 그리고 API 응답 처리를 다 한 페이지를 사용자에게 응답해줍니다.
사용자는 단 한번의 페이지에 대한 네트워크 요청만 하면 됩니다.
// client component
"use client"
import React from 'react';
async function Page() {
useEffect(() => {
await fetch("https://api.server.com/a")
await fetch("https://api.server.com/b")
await fetch("https://api.server.com/c")
await fetch("https://api.server.com/d")
await fetch("https://api.server.com/e")
},[])
return (
<div></div>
);
}
export default Page;
위와 같은 경우에는 우선 페이지에 대한 요청을 해야겠죠.
그 다음으로 5번의 API 요청을 해야합니다.
페이지 요청까지 하면 6번의 네트워크 통신을 해야하죠.
만약 사용자가 저 멀리 미국에 있다면 6번의 통신은 꽤 오래 걸릴 수도 있어요.
위처럼 서버컴포넌트에서 data fetching을 하면 API 요청들을 다 해결한뒤, 해결한 페이지만 사용자에게 응답해주면 되기 때문에 클라이언트에서 요청해야 하는 횟수를 줄일 수 있다고 말할 수 있습니다.