logo
Next.js 14

Next.js 14를 활용한 블로그 제작 후기

문성석2024년 3월 15일

thumbnail


Next.js 14를 활용하여 개인 블로그를 새롭게 만들었다.


이전에 정적 웹사이트 생성기 중에 하나인 Gatsby를 활용하여 개인 블로그를 제작한 적이 있었다. 그런데 오랜만에 글을 작성하려고 들어갔는데, 디자인도 그렇고 코드도 그렇고 수정하고 싶은 부분들이 너무 많이 보여서 전체적으로 갈아엎고 싶은 충동이 들었다. 그래서 구글에 개인 블로그 제작 관련해서 검색해 봤더니, 여러 가지 템플릿들이 많이 보였다. 템플릿을 사용해서 개발하면 정말 빠르게 제작할 수 있을 것 같았지만, 커스텀을 자유롭게 하고 싶었고 Next.js 14에서 새롭게 변경된 부분들을 A부터 Z까지 경험해보고 싶은 마음에 직접 처음부터 제작하기로 결정하였다.


이번 글에서는 블로그 제작 과정과 새롭게 알게 된 부분들에 대해서 정리해볼 예정이다.



어떤 블로그를 만들고 싶었는가 ?

코드를 작성하기 전에, 먼저 어떤 블로그를 만들고 싶은지에 대해서 생각해보았다.


1) 편안하게 글에 집중할 수 있는 블로그

사람들의 시선을 사로잡는 화려하고 멋진 애니메이션이 포함된 웹사이트보다는 글 자체에 집중할 수 있는 블로그를 만들고 싶었다. 애초에 내가 작성한 글을 내가 보더라도 편안한 느낌이 들어야 다른 사람들에게도 편하게 보이지 않을까 하는 생각이 들었다.


그래서 복잡해 보이는 것들은 생각하지 않고, 단순하고 간결한 미니멀리즘 디자인 컨셉으로 제작하기로 결정하였다.


2) 영어 컨텐츠도 작성할 수 있는 블로그

미디엄이나 레딧 같은 커뮤니티에 영어로 된 글을 올릴 수 있긴 하겠지만, 내가 만든 블로그에도 보기 편하게 관리하고 싶었다. 그래서 프로젝트 설계 할 때부터 다국어 지원을 생각하고 개발하기로 결정하였다.




사용한 기술

  • Next.js 14
  • Typescript
  • SCSS

서버 사이드 렌더링(SSR)을 지원하여 초기 로딩이 빠르고 검색 엔진 최적화(SEO)에 특화된 Next.js와 Typescript를 사용하여 개발하였다. 스타일링은 익숙한 SCSS를 사용하여 개발하였다. 그런데 Next.js 14 관련해서 검색하다보니 정말 많은 예시들이 Tailwind CSS와 함께 작성되어 있었다. 다음 사이드 프로젝트에서 한번 써봐야겠다는 생각이 들었다.




블로그 데이터 렌더링 작업

1) 기본 세팅

나는 이번 프로젝트에서 블로그 정보들이 저장된 마크다운 파일들을 로컬 폴더에 저장하여 사용할 생각이었다. 로컬에 저장된 데이터들을 화면에 렌더링 하기 위해서 next-mdx-remote 라이브러리를 사용하여 구현하였다.

아래는 공식 문서에 나와 있는 사용 예시 코드이다.

import { MDXRemote } from "next-mdx-remote/rsc";
 
export default async function RemoteMdxPage() {
  // MDX text - can be from a local file, database, CMS, fetch, anywhere...
  const res = await fetch("https://...");
  const markdown = await res.text();
  return <MDXRemote source={markdown} />;
}

로컬 파일, database, CMS 등.. 다른 공간에서 관리 되고 있는 마크다운 파일들을 받아와서 사용할 수 있다.


나는 API 요청을 보내서 외부 데이터를 가져오는 것이 아니라, 로컬에 있는 파일들의 데이터를 받아와야 하기 때문에 추가적인 코드가 필요했다.


2) 로컬에 저장된 데이터들을 가져오는 방법

데이터들을 읽기 위해 아래 도구들을 사용하였다.


fs 모듈

  • 파일 시스템에 접근하고 조작할 수 있는 Node.js 모듈이다. 파일을 읽고, 쓰고, 삭제같은 작업들을 수행할 수 있다.
  • 이번 프로젝트에서는 주로 폴더 내에 들어 있는 파일들의 리스트를 가져오는 작업과 해당 파일 내부 데이터를 읽는 작업을 위해 사용하였다.
fs.readdirSync(PATH);
fs.readFileSync;

path 모듈

  • 파일 및 디렉터리 경로를 조작하고 처리하는 데 사용하는 모듈이다.
  • 이번 프로젝트에서는 주로 경로 결합을 하기 위해 사용하였다.
path.join(POSTS_DIRECTORY_PATH, fileName);
// /Users/moon/Desktop/new-moon-log/posts/브라우저-동작-원리.mdx

gray-matter 라이브러리

  • JavaScript 및 Node.js 환경에서 YAML Front matter를 파싱하는 데 사용되는 라이브러리이다.
  • YAML Front matter는 일반적으로 마크다운 파일의 맨 위에 위치하며, 파일의 메타데이터를 정의하는 데 사용된다.
---
title: 브라우저 동작 원리
description: ...
category: Web
thumbnailUrl: ...
createdAt: 2021-08-03
---
 
여기부터 마크다운 내용 시작

Posts 데이터 가져오기 위해 작성한 코드

// data-access/getAllPosts
 
import fs from "fs";
import path from "path";
import matter from 'gray-matter';
import { PostForList } from 'utils/types';
import { POSTS_DIRECTORY_PATH } from 'utils/constants';
import { formatDate } from 'utils/formatDate';
 
// 모든 포스트 데이터 가져오기
export const getAllPosts = (locale: Locale): PostForList[] => {
  // /Users/moon/Desktop/new-moon-log/posts/ + en 또는 ko
  const targetDirectoryPath = path.join(POSTS_DIRECTORY_PATH, locale);
 
  // 파일 이름 리스트 가져오기
  const postFileNames = fs
    .readdirSync(targetDirectoryPath)
    .filter((path) => /\.mdx?$/.test(path));
 
  // 파일 이름과 폴더 경로를 합쳐서 해당 파일 데이터들을 읽은 후, 배열로 map 처리
  const mappedPosts = postFileNames.map((fileName) => {
    const source = fs.readFileSync(path.join(targetDirectoryPath, fileName));
 
    const { data } = matter(source);
    const { title, description, createdAt, category, thumbnailUrl } = data as PostForList;
 
    return {
      title,
      description,
      category,
      thumbnailUrl,
      createdAt: formatDate(new Date(createdAt), locale),
      fileName: fileName.replace(/\.mdx?$/, "")
    }
  });
 
  // createdAt 데이터를 사용해서 내림차순 정렬
  return mappedPosts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
};

서버 컴포넌트인 PostsPage 컴포넌트에서 getAllPosts 함수를 사용하여 데이터를 받아와 렌더링 할 수 있었다. (getStaticProps 메소드 사용 X)

export default function PostsPage({ params }: PostPageProps) {
  const posts = getAllPosts(params.locale);
 
  return (
    <GeneralLayout>
      <ul>
        {posts.map((post) => {
          return <PostListItem key={post.fileName} {...post} />;
        })}
      </ul>
    </GeneralLayout>
  );
}



반응형 작업

모바일부터 먼저 작업하여 레이아웃 틀을 잡아둔 후, max-width 속성을 이용하여 특정 크기 이상으로 올라가면 고정이 되게끔 작업하였다.


이후에 데스크탑에서 더 크게 보여야 하는 부분들만 미디어 쿼리를 사용하여 추가적으로 작업하였다.


video




다국어 처리 작업

이전까지 리액트 프로젝트를 진행하면서 다국어 처리를 할 때는 react-i18next 라이브러리를 사용했었는데, 이번에는 next-international 라이브러리를 사용하여 구현하였다.


next-international 라이브러리를 사용한 이유

  1. 100% Type-safe
  2. No dependencies, lazy-loaded
  3. Next.14 지원

따로 타입스크립트를 위한 세팅 없이 바로 텍스트 타입 추론이 가능하고, 의존성이 없고 lazy-loaded를 지원하여 더 가볍고 빠른 장점이 있다. 그리고 이 라이브러리를 만든 사람이 Vercel에서 일하는 개발자인데, 어떤 식으로 만들었을지 궁금해서 이 라이브러리를 사용해보았다.


사용 후기

“100% Type-safe” 이게 진짜 큰 장점이었다. 기본 세팅만 마치면 추가적인 세팅 없이 바로 간단하게 타입 추론이 가능했다.

// locales/en.ts
 
export default {
  "navigation.title": "기록소",
} as const
 
// components/Navigation
 
const Navigation = () => {
  const scopedT = useScopedI18n("navigation");
  const title = scopedT("title"); // 기록소
}

따로 제공되는 useScopedI18n 훅을 사용하면 범위를 좁힐 수가 있어서 컴포넌트 단위로 텍스트를 분리하기가 굉장히 편했었다.


그리고 Locale을 변경하는 useChangeLocale 훅이 제공되는데, 이 훅도 마찬가지로 타입이 자동적으로 안전하게 보장된다.

description code


확실히 react-i18next에서는 기본적으로 제공되지 않는 것들을 바로 사용할 수 있어서 빠르게 개발하는데 도움이 됐던 것 같다.




이미지 관리 방법

블로그 포스팅에 사용 되는 이미지들을 관리하기 위해 클라우드 기반 이미지 관리 서비스인 Cloudinary를 사용하였다.

Amazon S3와 같은 다른 서비스들도 있지만, 기본적으로 이미지 최적화도 제공하고 세팅에 걸리는 시간이 적은 Cloudinary 서비스를 사용하였다.


directories for images


서비스 내에서도 폴더를 따로 분리하여 헷갈리지 않게 이미지들을 분리하였다.




검색 엔진 최적화(SEO)

Next.js 14 버전에서는 검색 엔진 최적화를 위한 메타 데이터 설정 방식이 변경 되었다. 변경된 방식을 공식 문서를 통해 확인하면서 작업을 진행하였다.


1) 정적 메타 데이터 설정

정적 메타 데이터 설정 방법은 아주 간단했다. 그냥 서버 컴포넌트에 metadata 변수를 선언해두기만 하면, Next 프레임워크가 해당 데이터를 읽는다.

// layout.tsx | page.tsx
 
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: '...'
}
 
export default Page {
  ...
}

2) 동적 메타 데이터 설정

Next.js 14에서는 동적 메타 데이터 설정을 위한 generateMetadata 함수 제공한다. 나는 이번 프로젝트에서 설정된 언어에 따라 메타 데이터도 변경이 필요했어서 모든 데이터를 이 함수 작업하였다.

const TITLES = {
  ko: "기록소",
  en: "Programmer's Log",
} as const;
 
const DESCRIPTIONS = {
  ko: "소프트웨어 개발과 독서를 통해 ...",
  en: "Exploring personal growth ..."
} as const;
 
type PostPageProps = {
  params: {
    locale: Locale;
  }
}
 
export default function PostsPage({ params }: PostPageProps) {
  ...
}
 
export async function generateMetadata(
  { params }: PostPageProps,
): Promise<Metadata> {
  const locale = params.locale;
 
// 포스팅 상세보기 화면 같은 경우, 여기서 이렇게 데이터를 받아와서 동적으로 메타 데이터 설정이 가능하다.
// const post = await getPostBySlug(decodeURIComponent(slug), locale);
 
  return {
    title: TITLES[locale],
    description: DESCRIPTIONS[locale],
    openGraph: {
      type: 'website',
      description: DESCRIPTIONS[locale],
    },
  }
}

메타 데이터 설정 이외에도 검색 엔진 최적화를 위해서 신경 써야 되는 것들이 더 있는데, 나중에 따로 SEO 관련해서 글 정리 해봐야겠다.




마치며

완성된 블로그를 보니 도파민이 솟아오른다. 양질의 글을 더 많이 쓰고 싶고, 아직 다 추가하지 못한 기능들을 더 작업해보고 싶다.


일단 이 글을 쓰면서 생각이 들었던 추가 기능들은 다크 모드와 TOC(Table of Contents) 기능이다. 밤에 작업 할 때도 있는데 다크 모드 지원이 안되니 눈이 금방 피로해졌다.. 그리고 글의 길이가 길어지면 글 전체 구성을 파악하기가 조금 어려워지는 것 같아서 TOC의 필요성을 느꼈다. 우선은 아직 노션에만 정리해두고 블로그에 업로드 하지 못한 글들이 있는데, 정리가 마무리 되면 추가 기능들을 업데이트 해봐야겠다.


이 블로그가 다른 사람들에게 유익한 정보들을 제공하는 블로그가 되었으면 하고, 나 스스로 배우고 깨우치는 것들이 많았으면 한다. 꾸준하게 글을 써보자!