이 글에서는...
- Next.js 15.2+ 버전에서 도입된 Streaming metadata에 대해 설명합니다.
- 어떤 논제와 문제가 있었는지, 기존과 무엇이 다른지를 함께 이야기합니다.
다음은 Next.js 13 버전에서 generateMetadata를 사용했을 때와 하지 않았을 때를 비교한 표입니다.
각 페이지는 로딩 시간이 3초가 걸리는 API를 사용해서 렌더링됩니다.
미사용(정상) | 사용 |
---|---|
![]() | ![]() |
클릭하는 즉시 페이지에 접속됨 | generateMetadata가 끝난 3초 후에야 페이지에 접속됨 |
// 문제의 코드 : generateMetadata에 느린 작업이 있는 경우 페이지 로딩 자체가 지연됨
export async function generateMetadata() {
await new Promise((resolve) => {
setTimeout(() => {
resolve(void 0);
}, 3000);
});
return {
title: 'Example',
description: `example metadata`,
};
}
app router에서 동적인 metadata를 설정하는 방식을 generateMetadata
라는 하나의 함수로 제공하게 되면서, 해당 함수에서 불러오는 API가 느린 경우 페이지의 UI가 사용자에게 느리게 서빙되는 이슈가 있었습니다.
Next.js v13에서 발생한 해당 이슈를 해결하기 위해 v15에서 도입한 개념이 바로 Streaming metadata입니다.
해당 기능은 스트리밍을 통해 UI를 그리는 로직과 동시에 metadata를 불러오고, 스트리밍이 끝난 경우 최종적인 메타데이터를 업데이트합니다.
이 경우 메타데이터를 등록하는 함수의 속도가 느리더라도 페이지의 로딩이 블락되는 문제는 없어집니다.
Next.js가 메타데이터를 처리하는 실제 코드 중 일부를 가져와 설명해보겠습니다. (아래 참조 문서에서 원문을 확인하실 수 있습니다)
async function Metadata() {
return await resolveFinalMetadata()
}
기존에는 이런 식으로 generateMetadata를 사용하는 경우, 단순히 await을 통해 무조건 먼저 메타데이터를 처리했습니다. 그러나,
async function Metadata() {
const promise = resolveFinalMetadata()
if (serveStreamingMetadata) {
return (
<Suspense fallback={null}>
<AsyncMetadata promise={promise} />
</Suspense>
)
}
return await promise
}
Next.js 15.1.8 버전부터 streamingMetadata를 제공하며 코드가 변경되었습니다.
플래그에 따라서 메타데이터를 streaming해야 한다고 판단되면 비동기적으로 메타데이터를 가져옵니다.
메타데이터를 resolve하는 로직을 직접 살펴보겠습니다.
async getServerInsertedMetadata(): Promise<string> {
if (!metadataResolver || metadataToFlush) {
return ''
}
metadataToFlush = metadataResolver()
const html = await renderToString({
renderToReadableStream,
element: <>{metadataToFlush}</>,
})
return html
},
위 코드처럼 metadataResolver 함수와, renderToReadableStream 함수를 사용하여 메타데이터를 스트리밍으로 서빙합니다.
renderToReadableStream은 React에서 제공하는 함수로, 렌더할 데이터를 청크 단위로 잘게 쪼개 나누어 작업할 수 있도록 도와줍니다. 기본적으로 Web API인 ReadableStream을 사용해 구현되어 있는데, 이는 HTTP 1.1의 Chunked Transfer Encoding를 이용해 데이터를 분할하여 보낼 수 있도록 합니다. (뭐랄까 웹소켓이나 SSE와 비슷합니다. 다음에는 이 주제를 메인으로도 글 써보겠습니다)
그렇기 때문에 다른 작업(페이지 렌더링)을 진행하면서 동시에 처리가 가능한 것입니다. 리액트 19에서 도입된 서버 컴포넌트 또한, 페이지 단위가 아닌 컴포넌트 단위로 데이터를 스트리밍할 수 있는 이유가 이런 기능 때문입니다. (서버 컴포넌트에서는 이와 비슷한 renderToPipeableStream을 사용합니다)
아무튼 이와 같이 metadataResolver가 for of
를 사용해 청크로 쪼개진 메타데이터 스트림을 readableStream 함수와 연결해 처리합니다.
이렇게 되면 페이지를 먼저 렌더링하고, 나중에 metadata가 준비되면 보여줄 수 있습니다.
DOM을 해부해보면 streaming된 metadata와 그렇지 않은 metadata의 태그 위치가 다르다는 것 또한 알 수 있습니다. metadata가 스트리밍되지 않은 경우에는 상식처럼 head 내부에 title, description이 주입되지만, 스트리밍된 경우 메타데이터 관련 태그들이 body 내부에 위치합니다.
크롤러에게는 적합하지 않을 수 있으나, 결론적으로 태그가 주입되어 유저가 브라우저의 title을 확인할 때 문제가 발생하지 않습니다. (이와 관련된 자세한 이슈는 아래 참조 문서에서 확인할 수 있습니다.)
메타데이터가 중요한 이유는 무엇보다 검색 엔진이 사이트를 방문했을 때 얼마나 많은 정보를 크롤링하는지로 SEO가 결정되기 때문입니다.
그런데 generateMetadata를 스트리밍으로 변경하면서 자바스크립트를 실행해야지만 메타데이터가 불러와지도록 변경되었고, 검색 엔진이 자바스크립트를 실행하지 않는 경우 메타데이터가 아예 노출되지 않는다는 문제가 생겼습니다.
그러나 기존 로직을 그대로 가져가게 되면 이전처럼 generateMetadata의 처리 속도가 느린만큼 페이지가 늦게 로드된다는 문제가 있습니다.
이를 어떻게 해결해야 할까요?
그래서 vercel은 htmlLimitedBots라는 옵션을 추가합니다. 요청값으로 들어오는 user-agent값을 기반으로 크롤러 봇인 경우 이전처럼 느리더라도 풀 metadata를, 유저인 경우 스트리밍한 metadata를 제공하여 SEO와 성능(UX)을 모두 잡은 것입니다.
next.config.js에서 htmlLimitedBots 옵션에 RegExp를 사용해 메타데이터를 스트리밍하면 안되는 UserAgent를 설정할 수 있습니다.
/** @type {import("next").NextConfig} */
module.exports = {
reactStrictMode: true,
htmlLimitedBots: /Mac|Windows/,
};
위 예제와 같이 설정한 경우, 접속하는 기기의 UserAgent가 Mac 또는 Windows인 경우 기존처럼 메타데이터를 스트리밍하지 않는 식으로 작동합니다.
해당 옵션을 통해 Next.js 내장 함수인 shouldServeStreamingMetadata가 스트리밍이 필요한지를 결정합니다.
export function shouldServeStreamingMetadata(
userAgent: string,
{
streamingMetadata,
htmlLimitedBots,
}: {
streamingMetadata: boolean
htmlLimitedBots: string | undefined
}
): boolean {
if (!streamingMetadata) {
return false
}
const blockingMetadataUARegex = new RegExp(
htmlLimitedBots || HTML_LIMITED_BOT_UA_RE_STRING,
'i'
)
return (
// When it's static generation, userAgents are not available - do not serve streaming metadata
!!userAgent && !blockingMetadataUARegex.test(userAgent)
)
}
UserAgent에 htmlLimitedBots에 기재한 Agent가 하나라도 포함되는 경우 true, 아닌 경우 false를 반환합니다. (userAgent가 없는 경우 false를 반환합니다)
그런데 사용자가 검색 엔진에 어떤 크롤러들이 존재하는지를 숙지하고 매번 추가하는 것은 너무 복잡한 일입니다. 그렇기 때문에 htmlLimitedBots 옵션을 입력하지 않은 경우, Next.js는 흔히 웹 상에서 떠다니는 크롤러들에게 스트리밍을 제공하지 않도록 옵션을 설정합니다.
그렇기 때문에 아무런 설정을 하지 않아도 사실 개발자가 받는 피해는 없습니다. 기본적으로 사용자에게는 스트리밍, 크롤러 봇에게는 스트리밍되지 않은 메타데이터를 제공하기 때문입니다.
개발자가 아무런 설정을 하지 않은 경우 스트리밍을 진행하지 않는 봇은 다음과 같습니다.
export const HTML_LIMITED_BOT_UA_RE_STRING =
'Mediapartners-Google|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview'
그러나 크롤러 중에서도 스트리밍된 메타데이터를 보여주는 경우가 있습니다. 이는 다음과 같습니다.
const HEADLESS_BROWSER_BOT_UA_RE =
/Googlebot|Google-PageRenderer|AdsBot-Google|googleweblight|Storebot-Google/i
대체적으로 구글 크롤러 봇인데요, 구글 크롤러는 무조건 자바스크립트를 실행시키기 때문에 문제가 없어 스트리밍을 진행합니다.
그렇기 때문에 개발자가 모든 크롤러 봇이나 종류를 알고 있지 않아도 사용할 수 있는 것입니다.
그러나 아직 반대 여론은 존재합니다. 메타데이터를 스트리밍으로 제공하면 결국 크롤러에게 잡히는 페이지 지연 시간이 늘어나고, 로딩 시간이 너무 오래 걸리면 검색 엔진이 해당 사이트에게 페널티를 부과하기 때문입니다.
그렇기에 이는 단순하게 성능 문제를 가리려는 눈속임이라고도 평가됩니다.
또한 개발자가 선택할 수 있어야 하거나 표준을 따라야한다고 평가되는 기능들을 계속해서 vercel의
기술과 스타일대로 건드린다면, 오픈소스임에도 벤더 락인이 크게 걸린다는 비판 또한 존재합니다.
또한 공식적으로 등록되지 않은 알 수 없는 크롤러들에게 사이트가 크롤링되고 공유되어 SEO가 향상될 가능성도 존재하는데, 그런 면에서도 메타데이터를 스트리밍하는 일은 위험성이 일부 존재합니다.
저도 긱뉴스를 통해서 해당 이슈를 처음 접했는데요, 깃허브 디스커션이나 레딧 등 여러 곳에서 찬반 여론이 많아 조사해보았는데 꽤 재밌는 경험이었습니다.
현재는 Next.js의 15.1.8+ 이상 버전에서부터 고정으로 도입된 로직이기에, 만약 마이그레이션을 생각 중이시거나 SEO 관련 이슈를 안전하게 가져가야 하는 경우 stable한 Next.js 14 버전을 먼저 사용하시는 것을 추천드립니다.
저번 React의 Suspense 동작 이슈처럼, vercel이 협의점을 찾아 기술적으로 문제를 해결하거나 stable해지고 로직이 검증될 때까지 업데이트 소식을 지켜보아야 할 필요가 있어 보입니다.
Next.js 공식 문서 : htmlLimitedBots
Next.js 공식 문서 : Streaming metadata
Github : shouldServerStreamingMetadata
Github : serveStreamingMetadata
GeekNews : Next.js 15.1+는 Vercel 외 환경에서 사실상 쓸 수 없다
Next.js 15.1+ is unusable outside of Vercel
HTML_LIMITED_BOT_UA_RE_STRING
Reland "[metadata] new metadata insertion API and support PPR #75366" #75873
next.js/packages/next/src/lib/metadata/metadata.tsx
next.js/packages/next/src/lib/metadata/resolve-metadata.tsx
Google의 일반 크롤러 목록
Async generateMetadata hangs the app without any visible sign of loading
Title tag position error.
vercel 벤더 락인 이슈와 불안정한 SEO로 인해 여전히 해결해야 할 문제점이 있어보이네요
좋은 글 감사합니다!