2️Vercel 리더십 블로그

Vercel의 CEO인 Guillermo Rauch와 VP인 Lee Robinson의 블로그 아키텍쳐를 살펴보면서 Next.js를 이용해서 어떤 방식으로 블로그를 만들 수 있는지 살펴보려 합니다.

두 사람의 블로그는 GitHub에 공개되어 있습니다.

데이터 관리

글을 코드로

Guillermo의 경우엔 글을 코드로 관리합니다. 바꿔 말하면 작성된 코드가 글이 된다는 의미입니다.

이 방식에선 글을 app 디렉토리 하위에 작성합니다. Guillermo가 app/(post)/2021/making-the-web-faster/page.mdx와 같은 경로에 작성하는 것처럼요. 마크다운으로 작성된 파일이 글로 해석될 수 있는 비밀은 설정 파일에서 찾을 수 있습니다.

next.config.js
const withMDX = require("@next/mdx")();

module.exports = withMDX({
  pageExtensions: ["ts", "tsx", "md", "mdx"],
});

Next.js에서 제공하는 마크다운 라이브러리를 사용해 설정을 완성하고 있습니다. pageExtensions 설정을 통해 md, mdx와 같은 마크다운 확장자를 ts, tsx와 같은 방식으로 다루려고 한다는 걸 알 수 있습니다. 이는 일반적인 블로그 아키텍쳐에서 확인했던 getPostData 와 같은 함수를 없이도 마크다운 파일을 글로 해석할 수 있게 된다는 의미입니다.

Guillermo는 useMDXComponents를 활용해서 글 중간에 삽입한 트윗 컴포넌트를 스타일링하고 있습니다.

mdx-components.ts
export function useMDXComponents(components: {
  [component: string]: React.ComponentType;
}) {
  return { ...components, Tweet };
}

글을 데이터로

Lee의 경우엔 글을 데이터로 관리합니다. 일반적인 Next.js 블로그와 동일한 방식을 사용하는 것이죠.

이 방식엔 글의 위치가 app 디렉토리 하위에 있을 필요가 없습니다. Lee는 content/developer-marketing.mdx와 같은 경로에 글을 작성합니다.

getPostData와 유사한 함수를 구현해서 사용합니다. 파일시스템에 접근해서 content 디렉토리에 있는 데이터를 읽어내죠. 이후 메타데이터와 컨텐츠를 나눠서 결과를 반환하는 방식입니다.

app/blog/[slug].tsx
export default function Blog({ params }) {
  let post = getBlogPosts().find((post) => post.slug === params.slug);

  return (
    <section>
      <CustomMDX source={post.content} />
    </section>
  );
}

Lee는 MDXRemote를 사용해서 컴포넌트를 스타일링해서 글 중간에 트윗과 같은 컴포넌트를 첨부하고 있습니다.

components/mdx.tsx
import { MDXRemote } from 'next-mdx-remote/rsc';
import { TweetComponent } from './tweet';

export function CustomMDX(props) {
  return (
    <MDXRemote
      {...props}
      components={{ StaticTweet: TweetComponent }}
    />
  );
}

라우팅과 렌더링

Route Groups & ISR

Guillermo는 Route Groups를 활용하고 있습니다. app/(post) 하위에 있는 다른 폴더에서 같은 레이아웃을 공유하고 있죠. 글을 작성한 년도를 2021, 2022와 같이 폴더로 구분해서 폴더가 같은 레이아웃을 공유하도록 만들고 있습니다.

또 ISR을 사용해서 SSG의 한계를 극복하려고 하고 있습니다. 아래와 같이 레이아웃 파일에서 revalidate을 60으로 지정해 1분에 한 번씩 페이지를 재생성합니다.

app/(post)/layout.tsx
export const revalidate = 60;

export default async function Layout({ children }) {
  return <article>{children}</article>;
}

Dynamic Routes & PPR

Lee는 Dyanmic Routes를 활용하고 있습니다. app/blog/[slug]/page.tsx와 같은 방식이죠.

렌더링의 경우 Partial Prerendering을 사용하고 있습니다. 이는 블로그를 빌드해보면 확인할 수 있는데요. 빌드 결과를 알려주는 메시지가 아래와 같이 출력됩니다.

Route (app)                              Size     First Load JS
┌ ◐ /                                    1.59 kB         102 kB
├ ○ /blog                                178 B          95.5 kB
├ ◐ /blog/[slug]                         16.7 kB         328 kB

(... 중략)

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

글을 렌더링하는 /blog/[slug] 경로가 ◐ 표시로 Partial Prerendering 되었다고 알려주고 있습니다. Next.js 14 버전을 기준으로 Partial Prerendering을 실험적인 기능입니다. 이를 사용하기 위해선 아래와 같은 추가 설정이 필요합니다.

next.config.js
export default {
  experimental: {
    ppr: true,
  },
};

메타데이터

Static Metadata

Guillermo는 메타데이터를 정적인 방식으로 생성하고 있습니다. metadata 객체를 만드는 방식이죠.

// app/(post)/2021/making-the-web-faster/page.mdx

export const metadata = {
  title: 'Making the Web. Faster',
  description: 'Our vision of the Web is a global realtime medium for both creators and consumers, where all friction and latency are eliminated',
  openGraph: {
    title: 'Making the Web. Faster',
    description: 'Our vision of the Web is a global realtime medium for both creators and consumers, where all friction and latency are eliminated',
    images: [{ url: '/og/making-the-web-faster' }]
  }
}

일반적인 Next.js 블로그에서 살펴본 Front Matter와 유사한 방식으로 메타데이터를 정의하고 있습니다. 차이점이 있다면 우리에겐 친숙한 객체 형태라는 점입니다. 이번 App Router에서는 이렇게 정적으로 metadata라는 이름으로 객체를 정의하면 그걸 메타데이터로 적용해줍니다.

Dynamic Metadata

Lee는 메타데이터를 동적인 방식으로 생성하고 있습니다. 함수로 메타데이터를 정의하는 방식입니다.

// app/blog/[slug]/page.tsx

export async function generateMetadata({
  params,
}): Promise<Metadata | undefined> {
  let post = getBlogPosts().find((post) => post.slug === params.slug);
  
  let {
    title,
    publishedAt: publishedTime,
    summary: description,
    image,
  } = post.metadata;
  let ogImage = image
    ? `https://leerob.io${image}`
    : `https://leerob.io/og?title=${title}`;

  return {
    title,
    description,
    openGraph: {
      title,
      description,
      type: 'article',
      publishedTime,
      url: `https://leerob.io/blog/${post.slug}`,
      images: [
        {
          url: ogImage,
        },
      ],
    },
  };
}

generateMetadata라는 이름의 함수가 실행되며 metadata 객체를 생성합니다. 이때 생성되는 객체는 앞서 정적으로 만들던 metadata와 같은 타입의 객체입니다.

이 함수가 실행되면서 조건에 따라 다른 metadata 객체를 만들어 내고 있습니다. 원한다면 API를 호출할 수도 있습니다. 말 그대로 호출할 때마다 다른 결과를 반환할 수도 있는 동적인 방식입니다.

정리

Vercel 리더십의 블로그 아키텍쳐를 표로 정리하면 다음과 같습니다.

GuillermoLee

데이터 관리

글을 코드로

글을 데이터로

라우팅

Route Groups

Dynamic Routes

렌더링

Incremental Static Regeneration

Partial Prerendering

메타데이터

Static Metadata

Dynamic Metadata

Last updated