팀으로서 우리가 믿는 것 중 하나는 우리가 만드는 모든 것에는 블로그 글이 함께해야 한다는 것입니다. 우리가 작업하는 모든 프로젝트에 대해 짧은 발표 글을 작성하도록 강제하는 것은 내장된 품질 검사 역할을 하며, 세상에 공개할 준비가 되기 전까지는 프로젝트를 "완료"라고 부르지 않도록 합니다.
문제는 오늘까지 우리가 실제로 그 글들을 게시할 곳이 없었다는 것이었습니다!
플랫폼 선택
우리는 개발자 팀이기 때문에 당연히 기성 솔루션을 사용하자고 설득할 수 없었고, Next.js를 사용해 간단하고 커스텀한 것을 만들기로 결정했습니다.
Next.js에는 좋은 점이 많지만, 우리가 이를 선택한 주된 이유는 MDX를 잘 지원하기 때문입니다. MDX는 우리가 글을 작성할 때 사용하고 싶은 포맷이었습니다.
# My first MDX postMDX는 정말 멋진 작성 포맷입니다. 마크다운 안에 React 컴포넌트를 직접 삽입할 수 있기 때문이죠:<MyComponent myProp={5} />얼마나 멋진가요?
MDX는 일반 마크다운과 달리 콘텐츠 안에 실시간 React 컴포넌트를 직접 삽입할 수 있어 정말 흥미롭습니다. 이는 글쓰기에서 아이디어를 전달하는 방식에 많은 가능성을 열어줍니다. 이미지, 비디오, 코드 블록에만 의존하는 대신, 인터랙티브 데모를 만들어 마크다운 작성의 편의성을 잃지 않고도 콘텐츠의 두 단락 사이에 바로 삽입할 수 있습니다.
올해 말쯤 Tailwind CSS 문서 사이트를 재설계하고 다시 구축할 계획인데, 인터랙티브 컴포넌트를 삽입할 수 있다는 점은 프레임워크 작동 방식을 설명하는 데 큰 차이를 만듭니다. 그래서 작은 블로그 사이트를 테스트 프로젝트로 사용하는 것이 매우 합리적이었습니다.
콘텐츠 구성하기
처음에는 pages
디렉토리에 간단한 MDX 문서로 글을 작성했습니다. 하지만 시간이 지나면서 거의 모든 글에 관련된 에셋(예: 최소한 Open Graph 이미지)이 필요하다는 것을 깨달았습니다.
이러한 에셋을 다른 폴더에 저장하는 것이 다소 지저분하게 느껴져서, 대신 pages
디렉토리 안에 각 글마다 폴더를 만들고 글 내용을 index.mdx
파일에 넣기로 결정했습니다.
public/src/├── components/├── css/├── img/└── pages/ ├── building-the-tailwindcss-blog/ │ ├── index.mdx │ └── card.jpeg ├── introducing-linting-for-tailwindcss-intellisense/ │ ├── index.mdx │ ├── css.png │ ├── html.png │ └── card.jpeg ├── _app.js ├── _document.js └── index.jsnext.config.jspackage.jsonpostcss.config.jsREADME.mdtailwind.config.js
이렇게 하면 해당 글과 관련된 모든 에셋을 같은 폴더에 함께 배치할 수 있고, 웹팩의 file-loader를 활용하여 글 안에서 직접 에셋을 가져올 수 있습니다.
Metadata
각 MDX 파일 상단에서 내보내는 meta
객체에 각 게시물에 대한 메타데이터를 저장합니다:
import { bradlc } from "@/app/blog/authors";import openGraphImage from "./card.jpeg";export const meta = { title: "Tailwind CSS IntelliSense 린팅 기능 소개", description: `오늘 Visual Studio Code용 Tailwind CSS IntelliSense 확장의 새 버전을 출시합니다. 이 버전은 CSS와 마크업 모두에 Tailwind 전용 린팅 기능을 추가합니다.`, date: "2020-06-23T18:52:03Z", authors: [bradlc], image: openGraphImage, discussion: "https://github.com/tailwindcss/tailwindcss/discussions/1956",};// 게시물 내용은 여기에 작성
여기서 게시물 제목(실제 게시물 페이지의 h1
과 페이지 제목에 사용), 설명(Open Graph 미리보기용), 게시 날짜, 작성자, Open Graph 이미지, 그리고 게시물에 대한 GitHub Discussions 스레드 링크를 정의합니다.
모든 작성자 데이터는 별도의 파일에 저장하며, 각 팀원의 이름, 트위터 핸들, 아바타를 포함합니다.
import adamwathanAvatar from "./img/adamwathan.jpg";import bradlcAvatar from "./img/bradlc.jpg";import steveschogerAvatar from "./img/steveschoger.jpg";export const adamwathan = { name: "Adam Wathan", twitter: "@adamwathan", avatar: adamwathanAvatar,};export const bradlc = { name: "Brad Cornes", twitter: "@bradlc", avatar: bradlcAvatar,};export const steveschoger = { name: "Steve Schoger", twitter: "@steveschoger", avatar: steveschogerAvatar,};
작성자 객체를 게시물에 직접 가져오는 방식의 장점은, 필요할 경우 인라인으로 작성자를 쉽게 추가할 수 있다는 점입니다:
export const meta = { title: "팀에 속하지 않은 게스트 작성자의 예시 게시물", authors: [ { name: "Simon Vrachliotis", twitter: "@simonswiss", avatar: "https://pbs.twimg.com/profile_images/1160929863/n510426211_274341_6220_400x400.jpg", }, ], // ...};
이 방식은 작성자 정보를 중앙 집중식으로 관리하면서도 유연성을 잃지 않게 해줍니다.
포스트 미리보기 표시
홈페이지에 각 포스트의 미리보기를 표시하고 싶었는데, 이게 생각보다 까다로운 문제로 드러났다.
기본적으로 우리가 원했던 것은 Next.js의 getStaticProps
기능을 사용해 빌드 시점에 모든 포스트 목록을 가져오고, 필요한 정보를 추출한 후 실제 페이지 컴포넌트에 전달해 렌더링하는 것이었다.
문제는 모든 페이지를 실제로 불러오지 않고 이 작업을 수행하고 싶었다는 점이다. 모든 페이지를 불러오면 홈페이지 번들에 전체 사이트의 모든 블로그 포스트가 포함되어 불필요하게 큰 번들이 만들어질 수 있다. 지금은 포스트가 몇 개밖에 없어서 별 문제가 아니지만, 나중에 수십 개 또는 수백 개의 포스트가 생기면 낭비되는 바이트가 상당할 것이다.
여러 방법을 시도해봤지만, 결국 webpack의 resourceQuery 기능과 커스텀 로더를 조합해 블로그 포스트를 두 가지 형식으로 불러오는 방법을 선택했다.
- 포스트 페이지에서 사용할 전체 포스트
- 홈페이지에 필요한 최소한의 데이터만 불러오는 포스트 미리보기
이렇게 설정하면 개별 포스트를 불러올 때 ?preview
쿼리를 추가하면 전체 포스트 내용 대신 메타데이터와 미리보기 발췌문만 포함된 더 작은 버전의 포스트를 받을 수 있다.
다음은 그 커스텀 로더의 일부다:
{ resourceQuery: /preview/, use: [ ...mdx, createLoader(function (src) { if (src.includes('<!--more-->')) { const [preview] = src.split('<!--more-->') return this.callback(null, preview) } const [preview] = src.split('<!--/excerpt-->') return this.callback(null, preview.replace('<!--excerpt-->', '')) }), ],},
이 로더를 사용하면 각 포스트의 발췌문을 <!--more-->
태그를 사용해 소개 문단 뒤에 추가하거나, <!--excerpt-->
와 <!--/excerpt-->
태그로 감싸 포스트 내용과 완전히 독립된 발췌문을 작성할 수 있다.
export const meta = { // ...}이 부분은 포스트의 시작 부분이며, 홈페이지에 표시하고 싶은 내용입니다.<!--more-->이 부분 이후의 내용은 해당 포스트를 실제로 볼 때만 번들에 포함됩니다.
이 문제를 우아하게 해결하는 것은 꽤 까다로웠지만, 결국 미리보기와 실제 포스트 내용을 별도의 파일로 나누지 않고 하나의 파일에 모두 담을 수 있는 솔루션을 찾아내는 것은 멋진 경험이었다.
다음/이전 포스트 링크 생성하기
이 간단한 사이트를 구축할 때 마지막으로 해결해야 했던 문제는 개별 포스트를 볼 때마다 다음 포스트와 이전 포스트로의 링크를 포함하는 것이었습니다.
핵심적으로 우리가 해야 할 일은 모든 포스트를 로드하고(가급적 빌드 시점에), 그 목록에서 현재 포스트를 찾은 다음, 이전 포스트와 다음 포스트를 가져와서 페이지 컴포넌트에 props로 전달하는 것이었습니다.
이 작업은 예상보다 어려웠는데, MDX가 현재 getStaticProps
를 일반적으로 사용하는 방식으로 지원하지 않기 때문입니다. MDX 파일에서 직접 getStaticProps
를 내보낼 수 없고, 대신 별도의 파일에 코드를 저장한 후 다시 내보내야 합니다.
우리는 홈페이지에서 포스트 미리보기를 가져올 때 이 추가 코드를 로드하고 싶지 않았고, 모든 포스트에서 이 코드를 반복하고 싶지 않았기 때문에, 커스텀 로더를 사용하여 각 포스트의 시작 부분에 이 내보내기를 추가하기로 결정했습니다:
{ use: [ ...mdx, createLoader(function (src) { const content = [ 'import Post from "@/components/Post"', 'export { getStaticProps } from "@/getStaticProps"', src, 'export default (props) => <Post meta={meta} {...props} />', ].join('\n') if (content.includes('<!--more-->')) { return this.callback(null, content.split('<!--more-->').join('\n')) } return this.callback(null, content.replace(/<!--excerpt-->.*<!--\/excerpt-->/s, '')) }), ],}
또한 이 커스텀 로더를 사용하여 실제로 이 정적 props를 Post
컴포넌트에 전달해야 했기 때문에, 위에서 본 추가 내보내기도 덧붙였습니다.
하지만 이것이 유일한 문제는 아니었습니다. getStaticProps
는 현재 렌더링 중인 페이지에 대한 정보를 제공하지 않기 때문에, 다음 포스트와 이전 포스트를 결정할 때 어떤 포스트를 보고 있는지 알 수 없었습니다. 이 문제는 해결 가능할 것으로 보이지만, 시간 제약으로 인해 빌드 시점보다는 클라이언트 측에서 더 많은 작업을 수행하기로 결정했습니다. 그래서 실제로 현재 라우트를 확인하여 필요한 링크를 파악할 수 있었습니다.
우리는 getStaticProps
에서 모든 포스트를 로드하고, 각 포스트의 URL과 제목만 포함하는 매우 가벼운 객체로 매핑했습니다:
import getAllPostPreviews from "@/getAllPostPreviews";export async function getStaticProps() { return { props: { posts: getAllPostPreviews().map((post) => ({ title: post.module.meta.title, link: post.link.substr(1), })), }, };}
그런 다음 실제 Post
레이아웃 컴포넌트에서 현재 라우트를 사용하여 다음 포스트와 이전 포스트를 결정했습니다:
export default function Post({ meta, children, posts }) { const router = useRouter(); const postIndex = posts.findIndex((post) => post.link === router.pathname); const previous = posts[postIndex + 1]; const next = posts[postIndex - 1]; // ...}
이 방법은 현재로서는 잘 작동하지만, 장기적으로는 전체 포스트를 로드하는 대신 getStaticProps
에서 다음 포스트와 이전 포스트만 로드할 수 있는 더 간단한 솔루션을 찾고 싶습니다.
Hashicorp에서 만든 Next MDX Remote라는 흥미로운 라이브러리가 있습니다. 이 라이브러리는 MDX 파일을 데이터 소스처럼 다룰 수 있게 해주며, 우리가 앞으로 탐구할 가능성이 높습니다. 이를 통해 동적 슬러그 기반 라우팅으로 전환할 수 있어 getStaticProps
에서 현재 경로명에 접근할 수 있고 더 많은 기능을 활용할 수 있을 것입니다.
마무리
전체적으로 Next.js로 이 작은 사이트를 만드는 것은 재미있는 학습 경험이었습니다. 간단해 보이는 것들이 이런 도구들을 사용하면 얼마나 복잡해지는지 항상 놀랍지만, Next.js의 미래에 대해 매우 낙관적이고 앞으로 몇 달 안에 tailwindcss.com의 다음 버전을 Next.js로 구축할 것을 기대하고 있습니다.
이 블로그의 코드베이스를 확인하거나 위에서 언급한 것들을 단순화하기 위해 풀 리퀘스트를 제출하고 싶다면, GitHub 저장소를 확인해 보세요.
이 글에 대해 이야기하고 싶으신가요? GitHub에서 토론하기 →