Partial Pre-Rendering(이하 PPR)은 Next.js v14.0에서 발표된 SSR과 SSG에 필적하는 새로운 렌더링 모델입니다.
PPR은 개발 중인 기능으로, v15의 RC(Release Candidate) 버전에서 experimental 플래그를 활성화하여 사용할 수 있습니다.
ppr: true
로 설정하면 모든 페이지가 PPR의 대상이 되며, ppr: "incremental"
로 설정하면 exort const experimental_ppr = true
로 설정된 라우트만 PPR의 대상이 됩니다.
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: "incremental", // ppr: boolean | "incremental"
},
};
export default nextConfig;
// page.tsx(layout.tsx도 가능)
export const experimental_ppr = true;
export default function Page() {
// ...
}
PPR은 Next.js 코어팀에게도 중요한 기능 개발이며, 개인적으로도 매우 주목할 만한 토픽이라고 생각합니다.
하지만 제가 관찰한 바로는 일부에서만 화제가 되고 있으며, 크게 주목받지는 않는 것 같습니다.
저는 PPR로 인해 렌더링 모델의 시대가 또 한 번 새로운 국면을 맞이할 것이라고 생각합니다.
이번 글에서는 PPR이 무엇인지, 무엇을 해결하려고 하는지, 그리고 PPR 시대의 도래로 인해 무엇이 바뀔지에 대해 고찰해보겠습니다.
렌더링 모델의 역사 돌아보기
PPR 이야기를 하기 전에, 지금까지의 Next.js 렌더링 모델에 대해 되짚어보겠습니다.
Next.js가 지원하는 렌더링 모델은 총 세 가지가 있었습니다.
- SSR: Server-Side Rendering
- SSG: Static-Site Generation
- ISR: Incremental Static Regeneration
이 세 가지 모델이 지원된 역사적 배경을 간단히 살펴보겠습니다.
Pages Router 시대
Next.js는 원래 SSR이 가능한 React 프레임워크로 2016년 10월에 등장했습니다.
Next.js는 v1 발표 이후 오랫동안 SSR을 위한 프레임워크였지만, 약 3년 반 후인 2019년에 등장한 v9.3에서 SSG가, v9.5에서 ISR이 도입되면서 여러 렌더링 모델을 지원하는 프레임워크로 발전했습니다.
당시 Gatsby의 인기도 높아 SSR만 가능했던 Next.js 사용자들이 Gatsby로 이동하는 경우도 많았던 것 같습니다.
npm trends를 보면 2019년경 Gatsby가 더 인기가 많았던 것을 알 수 있습니다.
npm trends(Gatsby vs Next.js)
실제로 저도 당시에는 Gatsby를 선호했습니다.
하지만 Next.js v9 시리즈에서 동적 라우팅 기능, TypeScript 지원, 그리고 SSG와 ISR 지원이 추가되면서 Next.js가 급격히 주목받기 시작했습니다. 이러한 기능들이 현재 Next.js의 인기에 큰 기여를 했다고 생각합니다.
SSR과 SSG라는 렌더링 모델에 대한 논쟁은 많은 사용자들의 관심을 끌었으며, 이러한 논쟁을 모두 지원한 Next.js의 선택이 현재의 인기를 지탱하는 중요한 요소가 되었습니다.
App Router 등장 이후
v9 시점에서는 Next.js에 단일한 Pages Router만 존재했습니다.
그 후 v13에서 발표된 App Router는 RSC(React Server Components)와 Server Actions, 다층 캐시 등 많은 패러다임 변화를 요구했습니다.
App Router에서 렌더링 모델에는 어떤 변화가 있었을까요?
결론부터 말하면 App Router는 기존과 마찬가지로 SSR, SSG, ISR에 상응하는 기능을 지원하지만, App Router 문서에서는 기본적으로 SSR, SSG, ISR 등의 용어를 사용하지 않습니다.
현재 App Router는 SSR, SSG, ISR 대신 static rendering과 dynamic rendering이라는 두 가지 개념을 사용하여 많은 기능을 설명하고 있습니다.
- static rendering: 기존 SSG 또는 ISR에 해당하며, 빌드 시 또는 revalidate 실행 후에 렌더링
- revalidate 없이: SSG에 해당
- revalidate 있음: ISR에 해당
- dynamic rendering: 기존 SSR에 해당하며, 요청마다 렌더링
Pages Router에서는 SSG와 ISR을 빌드 시에 실행하는 함수로 설정해야 했기 때문에 정적으로 결정되었습니다.
그러나 App Router의 revalidate는 revalidatePath나 revalidateTag를 통해 동적으로 수행할 수 있어, SSG와 ISR을 구분할 필요가 없어졌습니다.
따라서 Next.js는 SSG와 ISR 용어를 사용하지 않게 되었습니다.
또 다른 이유로는 ISR이 Vercel 외부에서 운영하기 어려운 기능으로 평가받아 부정적인 이미지가 붙었기 때문입니다.
오늘날 Cache Handler를 통해 캐시의 영구 저장 위치를 선택할 수 있어, ISR 등장 시점보다 셀프 호스팅 환경에서 운영하기 쉬워졌습니다. 자세한 내용은 제 이전 글을 참조해 주세요.
Streaming SSR
App Router 등장 시점, SSR에서도 기술적 진화가 있었습니다.
현재 App Router의 SSR은 Streaming SSR을 지원합니다.
Pages Router에는 v12 알파 기능으로 구현되었지만, 현재는 삭제되어 Streaming SSR을 지원하지 않습니다.
Streaming SSR은 페이지 렌더링의 일부를 <Suspense>
로 지연 렌더링할 수 있으며, 렌더링이 완료될 때마다 점진적으로 결과가 클라이언트로 전송됩니다.
import { Suspense } from "react";
import { PostFeed, Weather } from "./Components";
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
);
}
위 예제에서 처음에는 fallback(Loading feed... 또는 Loading weather...)이 표시되고, 서버 측에서 <PostFeed>
나 <Weather>
의 렌더링이 완료되면 순차적으로 클라이언트에 렌더링 결과가 전송되어 fallback이 대체됩니다.
또한, 이 모든 과정이 하나의 HTTP 응답으로 이루어져 SEO 측면에서도 이점이 있습니다.
SSG/SSR에 있어 정적·동적 데이터의 혼재
페이지를 구성하는 데 필요한 데이터가 정적 데이터(캐시 가능)와 동적 데이터(캐시 불가능)로 혼재할 수 있습니다.
예를 들어, EC 사이트에서 상품 정보는 빌드 시점이나 revalidate 시점에 가져와 캐시할 수 있지만, 로그인 정보는 캐시할 수 없어 동적으로 가져와야 합니다.
이처럼 정적 데이터와 동적 데이터가 혼재하는 경우, App Router에서는 크게 다음 두 가지 구현 패턴이 있었습니다.
- SSG + Client fetch: 페이지 자체는 SSG로 처리하고, 클라이언트 측에서 동적 데이터를 fetch
- Streaming SSR: 정적 데이터는 캐시(Data Cache)를 이용해 속도를 높이고, 페이지의 일부를
<Suspense>
로 지연 렌더링
하지만 이들 구현 방식은 각각 장·단점이 있어 상황에 따라 최적의 선택이 달라집니다.
따라서 이러한 선택에 대한 논의나 설명 시 SSG나 Streaming SSR에 대한 고도의 이해가 필요합니다.
이들의 장단점을 간단히 정리해보면 다음과 같습니다.
관점 | SSG + Client fetch | Streaming SSR |
---|---|---|
TTFB | 유리 | 다소 불리 |
HTTP 라운드트립 | 여러 번 | 한 번 |
CDN 캐시 | 가능 | 불가능 |
구현 | 중복되기 쉬움 | 단순 |
App Router는 Vercel 또는 셀프 호스팅 서버를 사용하는 것이 기본적인 운영 패턴이므로, "서버가 필요/불필요"와 같은 관점은 생략합니다.
일반적인 SSR에 비해 Streaming SSR은 TTFB가 개선되지만, 여전히 렌더링 시 정적 파일을 반환하는 SSG가 더 유리합니다.
반면 구현 측면에서는 Client fetch의 경우 클라이언트 측 처리와 서버 측 엔드포인트를 연결하는 처리(API Routes, tRPC, GraphQL 등)가 필요하기 때문에, Streaming SSR이 더 단순하다고 생각합니다.
또한, Streaming SSR은 HTTP 라운드트립이 한 번에 끝나므로 동적 요소가 표시될 때까지의 시간이 짧아져 성능 면에서도 평가할 만합니다.
이처럼 Streaming SSR은 많은 장점을 가지고 있지만, SSG가 가진 TTFB의 속도는 얻을 수 없다는 것이 트레이드오프였습니다.
이를 해결하기 위해 등장한 것이 이번 글의 주제인 PPR입니다.
PPR이란?
PPR은 Streaming SSR을 더욱 발전시킨 기술로, 페이지를 static rendering으로 처리하면서도 부분적으로 dynamic rendering을 할 수 있는 렌더링 모델입니다.
SSG/ISR 페이지의 일부에 SSR 부분을 결합한 것 같은 이미지, 또는 Streaming SSR의 스켈레톤 부분을 SSG/ISR로 처리하는 이미지입니다.
공식 설명에 따르면 EC 사이트의 상품 페이지 예시를 보면 다음과 같은 구성이 가능합니다.
상품 페이지 전체나 내비게이션은 static rendering으로 처리하고, 한편으로 카트나 추천 정보와 같이 사용자마다 다른 UI 부분은 dynamic rendering으로 처리할 수 있습니다.
물론 상품 정보 자체가 갱신될 수 있지만, 이 예시에서는 필요에 따라 revalidate하도록 설정하고 있습니다.
정적화와 Streaming 렌더링의 혜택
Streaming SSR에서는 <Suspense>
의 외부에 대해 다음과 같은 처리가 요청마다 수행됩니다.
- Server Components(다단계 계산의 1단계)를 실행
- Client Components(다단계 계산의 2단계)를 실행
- 1과 2의 결과(React 트리)에서 HTML을 생성
- 3의 결과를 응답으로 전송
PPR에서는 이 1~3을 빌드 시에 실행하여 정적화하므로, Next.js 서버는 초기 표시용 HTML 전송을 더 빠르게 응답할 수 있습니다.
PPR의 동작 관찰
PPR에서 dynamic rendering되는 부분을 지연시키는 경우, 이를 dynamic hole, async hole 또는 단순히 hole이라고 부릅니다.
PPR을 활성화하고 실제로 dynamic hole이 대체되는 모습을 관찰해보겠습니다.
다음 샘플 코드를 기반으로 동작을 관찰해봅시다.
// app/ppr/page.tsx
import { Suspense } from "react";
import { setTimeout } from "node:timers/promises";
// 📍PPR를 활성화
export const experimental_ppr = true;
export default function Home() {
return (
<main>
<h1>PPR Page</h1>
<Suspense fallback={<>loading...</>}>
<RandomTodo />
</Suspense>
</main>
);
}
async function RandomTodo() {
const todoDto: TodoDto = await fetch("https://dummyjson.com/todos/random", {
// v15.0.0-rc.0 시점에서는 기본적으로 no-store이지만, 명시적으로 지정하지 않으면 dynamic rendering이 되지 않음
cache: "no-store",
}).then((res) => res.json());
await setTimeout(3000);
return (
<>
<h2>Random Todo</h2>
<ul>
<li>id: {todoDto.id}</li>
<li>todo: {todoDto.todo}</li>
<li>completed: {todoDto.completed ? "true" : "false"}</li>
<li>userId: {todoDto.userId}</li>
</ul>
</>
);
}
type TodoDto = {
id: number;
todo: string;
completed: boolean;
userId: number;
};
<RandomTodo>
는 요청마다 랜덤한 TODO 정보를 가져오는 컴포넌트입니다.
페이지 자체인 <Home>
은 static rendering이지만, API 호출에 no-store
를 지정했기 때문에 <RandomTodo>
는 dynamic rendering이 됩니다.
이번에는 스트림의 상태를 관찰하기 위해 요청 후 3초 지연시키도록 설정했습니다.
초기 표시 시점에서는 <Suspense>
의 fallback으로 지정한 loading...
이 표시되고, 그 후 <RandomTodo>
의 렌더링 결과가 전송되면 loading...
이 대체됩니다.
DevTools를 확인하면, 응답도 초기 표시용 HTML이 전송된 시점에 한 번 멈추는 것을 알 수 있습니다.
초기 표시 시점에 전송된 <body>
하위의 HTML은 다음과 같습니다.
<main>
<h1>PPR Page</h1>
<!--$?-->
<template id="B:0"></template>
loading...
<!--/$-->
</main>
<script
src="/_next/static/chunks/webpack-b5d81ab04c5b38dd.js"
async=""
></script>
Streaming SSR에서는 <Home>
을 요청마다 매번 계산해야 했지만, PPR에서는 정적화되어 빌드 시점이나 revalidate 후에만 렌더링되며, 요청마다 계산되는 것은 <RandomTodo>
뿐입니다.
따라서 Next.js 서버는 위의 HTML을 포함하는 정적 파일을 즉시 클라이언트에 전송할 수 있습니다.
dynamic rendering인 <RandomTodo>
이후의 HTML은 Streaming SSR과 마찬가지로 렌더링이 완료되는 대로 전송됩니다.
<div hidden id="S:0">
<h2>Random Todo</h2>
<ul>
<li>
id:
<!-- -->
253
</li>
<li>
todo:
<!-- -->
Try a new fitness class like aerial yoga or barre
</li>
<li>
completed:
<!-- -->
true
</li>
<li>
userId:
<!-- -->
21
</li>
</ul>
</div>
<script>
$RC = function (b, c, e) {
c = document.getElementById(c);
c.parentNode.removeChild(c);
var a = document.getElementById(b);
if (a) {
b = a.previousSibling;
if (e) (b.data = "$!"), a.setAttribute("data-dgst", e);
else {
e = b.parentNode;
a = b.nextSibling;
var f = 0;
do {
if (a && 8 === a.nodeType) {
var d = a.data;
if ("/$" === d)
if (0 === f) break;
else f--;
else ("$" !== d && "$?" !== d && "$!" !== d) || f++;
}
d = a.nextSibling;
e.removeChild(a);
a = d;
} while (a);
for (; c.firstChild; ) e.insertBefore(c.firstChild, a);
b.data = "$";
}
b._reactRetry && b._reactRetry();
}
};
$RC("B:0", "S:0");
</script>
<script>
(self.__next_f = self.__next_f || []).push([0]);
self.__next_f.push([2, null]);
</script>
<script>
self.__next_f.push([
1,
'1:I[4129,[],""]\n3:"$Sreact.suspense"\n5:I[8330,[],""]\n6:I[3533,[],""]\n8:I[6344,[],""]\n9:[]\n',
]);
</script>
<script>
self.__next_f.push([
1,
'0:[null,["$","$L1",null,{"buildId":"u-TCHmQLHODl6ILIXZKdy","assetPrefix":"","initialCanonicalUrl":"/ppr","initialTree":["",{"children":["ppr",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],"initialSeedData":["",{"children":["ppr",{"children":["__PAGE__",{},[["$L2",["$","main",null,{"children":[["$","h1",null,{"children":"PPR Page"}],["$","$3",null,{"fallback":"loading...","children":"$L4"}]]}]],null],null]},["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children","ppr","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","styles":null}],null]},[["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\\"Segoe UI\\",Roboto,Helvetica,Arial,sans-serif,\\"Apple Color Emoji\\",\\"Segoe UI Emoji\\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null],null],"couldBeIntercepted":false,"initialHead":[false,"$L7"],"globalErrorComponent":"$8","missingSlots":"$W9"}]]\n',
]);
</script>
<script>
self.__next_f.push([
1,
'a:"$Sreact.fragment"\n7:["$","$a","yuyzwuCpBYflRRVYLHWqg",{"children":[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Create Next App"}],["$","meta","3",{"name":"description","content":"Generated by create next app"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}]]}]\n2:null\n',
]);
</script>
<script>
self.__next_f.push([
1,
'4:[["$","h2",null,{"children":"Random Todo"}],["$","ul",null,{"children":[["$","li",null,{"children":["id: ",253]}],["$","li",null,{"children":["todo: ","Try a new fitness class like aerial yoga or barre"]}],["$","li",null,{"children":["completed: ","true"]}],["$","li",null,{"children":["userId: ",21]}]]}]]\n',
]);
</script>
주목할 점은 <script>
의 $RC
주변입니다.
처음에 전송된 HTML에 있는 <template>
의 id가 B:0
이고, 후반에 전송된 <RandomTodo>
의 HTML이 S:0
입니다.
이것을 $RC("B:0", "S:0")
로 치환하는 것을 알 수 있습니다.
또한, <script>
가 직접 기록되어 있어 앞서 언급한 대로 이것들이 하나의 HTTP 응답으로 완료된다는 것을 알 수 있습니다.
PPR 고찰
PPR의 동작에 대해 대략적으로 이해하셨을 것이라 생각합니다.
실제로 우리가 이 PPR을 어떻게 받아들여야 할까요? 기능을 아는 것과 역할을 아는 것은 별개의 논의입니다.
필자는 PPR이 가져올 변화에 대해 몇 가지 고찰해보겠습니다.
SSG + Client fetch / Streaming SSR과의 비교
SSG/SSR에 있어 정적·동적 데이터의 혼재에서 제시한 표에 PPR을 추가하여 비교해보겠습니다.
관점 | PPR | SSG + Client fetch | Streaming SSR |
---|---|---|---|
TTFB | 유리 | 유리 | 다소 불리 |
HTTP 라운드트립 | 1회 | 여러 번 | 1회 |
CDN 캐시 | 불가 | 가능 | 불가 |
구현 | 단순 | 중복되기 쉬움 | 단순 |
PPR은 SSG + Client fetch에 해당하는 TTFB와 구현의 단순함을 동시에 얻을 수 있습니다.
HTML 내에 동적 요소가 포함되므로 CDN 캐시는 불가능하지만, 다른 점에서는 SSG + Client fetch와 Streaming SSR의 장점을 모두 가지고 있습니다.
PPR을 통한 React다운 설계 책임
RSC 이후의 React에서는 데이터 페칭을 포함한 서버 측 처리도 컴포넌트 책임으로 하는 등 "필요한 것은 모두 컴포넌트에 캡슐화한다"는 방향성이 강해지고 있습니다.
그리고 렌더링에 경계를 만들어 병렬성을 높이는 것이 <Suspense>
입니다.
따라서 <Suspense>
경계를 가지고 dynamic rendering과 static rendering을 전환할 수 있는 PPR은 매우 최근의 React다운 설계라고 생각합니다.
실제로 PPR을 사용하기 위해서는 앞서 언급한 experimental 설정을 제외하고 새로운 API를 학습할 필요가 없다는 점도 React다운 설계에 따르고 있음을 뒷받침합니다.
SSR/SSG 논쟁의 종결
최근의 Next.js도 "필요한 것은 모두 컴포넌트에 캡슐화한다"는 방향성으로 인해 페이지 단위로 생각하는 것이 줄어드는 경향이 있습니다.
물론 웹의 구조상 URL을 기반으로 하기 때문에 페이지 단위로 생각해야 하는 meta 정보나 URL에 포함된 동적 경로나 매개변수 등 "페이지"라는 개념을 없앨 수는 없습니다.
그러나 렌더링 모델에 관해서는 반드시 페이지라는 개념이 필요하지 않습니다.
기존에는 SSR, SSG, ISR 어느 것을 선택하든 페이지 단위로 생각할 필요가 있었지만, PPR 이후에는 더 세분화된 <Suspense>
경계를 기반으로 한 UI 단위로 생각할 수 있게 됩니다.
이로 인해 "SSR로 할 것인가 SSG로 할 것인가"라는 논쟁은 과거의 것이 되고, PPR 이후에는 더 세분화된 "어디까지를 static으로, 어디부터를 dynamic으로 할 것인가"라는 논의로 전환될 것입니다.
PPR의 단점 고찰
여기까지 PPR을 마치 만능 해결책인 것처럼 설명했지만, PPR에도 당연히 주의할 점이 있습니다. 필자가 생각하는 주의점 몇 가지를 소개합니다.
- 페이지는 반드시 200 상태가 된다
- PPR은 화면의 정적화된 부분을 반환하기 때문에, 페이지의 HTTP 상태는 반드시 200이 됩니다. 이것이 실제 문제로 다가오는 것은 모니터링 측면입니다. 애초에 App Router를 사용하는 경우 스트림을 통한 응답이 기본이 되므로, HTTP 상태만으로 모니터링하는 것은 불완전합니다. 따라서 PPR에 국한되지 않고 App Router로 구현된 애플리케이션의 모니터링은 HTTP 상태가 아닌 개별 에러 발생률 등을 기준으로 해야 합니다.
- 정적 렌더링으로 완료할 수 있다면 그게 더 좋다
일부를 정적화할 수 있다 = SSR에서도 SSG와 동일하게 빠르다, 라는 오해가 없었으면 좋겠다는 이야기였습니다 (저도 이전에는 그런 오해를 했었기 때문에)
- 확실히 dynamic rendering을 포함한 페이지에서도 PPR을 사용하면 TTFB를 SSG에 가깝게 만들 수 있습니다. 그러나 성능은 TTFB만으로 측정하는 것이 아닙니다. 예를 들어 Time to Interactive에서는 PPR이나 SSR이나 큰 차이가 없으므로, 페이지 전체를 SSG로 처리할 수 있다면 그게 더 유리합니다. PPR은 성능을 위한 만능 해결책은 아니지만, 일부 성능이 개선되는 것은 사실입니다. PPR에서도 성능에 대한 이야기는 어떤 속도 지표에 대한 이야기인지 주의할 필요가 있습니다.
- PPR은 어디까지나 dynamic rendering이 필요한 경우에 최적화된 것입니다.
감상
PPR은 현존하는 렌더링 모델 중 가장 이상적이라고 생각합니다.
물론 내부적으로는 매우 복잡한 처리를 하고 있지만, Next.js 사용자 입장에서는 "기본은 static rendering, 일부는 dynamic rendering"이라는 규칙을 따르기만 하면 되는 점이 단순하고 좋습니다.
그리고 그 경계를 <Suspense>
로 정의하는 사용 방법도 매우 인상적입니다.
v15의 GA 시점에 PPR의 로드맵이 발표될 예정입니다.
PPR이 앞으로 어떻게 발전할지 매우 기대됩니다.
'Javascript' 카테고리의 다른 글
Deno v2를 향하여 - Deno v2, deno_std v1, Fresh v2에 대하여 (0) | 2024.06.10 |
---|---|
PPR은 island 아키텍처인가? (0) | 2024.06.10 |
2024년에 Remix 앱을 위한 최적의 호스팅 옵션 가이드 (0) | 2024.06.06 |
Next.js 버전 15의 핵심 변경 사항 - 기본적으로 비활성화된 라우팅 및 데이터 캐싱 (0) | 2024.05.25 |
자바스크립트 배열 완전 정복: 희소 배열부터 다양한 메서드 활용까지 (0) | 2024.05.17 |