ALLSSU

블로그 개발 #2 - Next.js 정적 사이트 개발하고 Gitlab으로 배포하기

마크다운 파일을 읽어서 서빙하는 정적 블로그 사이트 개발해보기

2022년 9월 20일 화 22:25

블로그 아키텍처

Next.js와 여러가지 디펜던시들을 조합해서 만든 마크다운 기반 블로그

Next.js로 블로그를 만들어보고 싶어서 개발을 시작했다. 인프라 구성을 먼저 하고(1편에서 이어서), 인프라 위에 올라갈 블로그 어플리케이션을 개발하기 시작했다.

Next.js 정적 사이트를 만들자

CLI를 통해 next export를 하게 되면, Next.js를 통해 정적사이트가 출력(export)된다. package.json에 next build와 next export를 추가해서 정적 파일이 생성되도록 했다.

{
  "name": "blog-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build && next export"
  }
}
Next.js에서, next export로 정적 사이트는 생성된다.

Gitlab CI/CD에서 빌드하고 배포하기

Next.js를 통해 정적사이트를 빌드하고 출력하는 방법을 알았으니, Gitlab CI/CD를 통해 빌드하고 배포하는 스크립트를 작성하기 시작했다. Gitlab CI/CD는 그룹별로 월 400분의 무료 CI/CD 시간을 부여하기 때문에, 블로그 정도의 CI/CD는 무료로 사용할 수 있다. 깃랩에서 단계(Stage)를 빌드(build)와 배포(deploy)로 나눴고, 빌드하고 나서 artifact에 있는 정적 파일들을 Amazon S3에 배포하도록 간단하게 구성했다.

# /.gitlab-ci.yml

stages:
  - build
  - deploy

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .next/cache/

build:
  stage: build
  only:
    - main
  image: node:16-alpine
  script:
    - yarn
    - yarn build
  artifacts:
    expire_in: 6 hrs
    paths:
      - ./out

deploy:
  stage: deploy
  only:
    - main
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
  dependencies:
    - build
  script:
    - aws s3 rm s3://S3주소/ --recursive
    - aws s3 cp ./out s3://S3주소/ --recursive
    - aws cloudfront create-invalidation --distribution-id CloudFront배포아이디 --paths "/*"
빌드 단계에서는 ./out 디렉토리로 정적파일을 출력하고, 배포 단계에서는 ./out 디렉토리를 S3에 업로드한다.

빌드할때는 가벼운 alpine 리눅스가 포함 된 Node.js 이미지만 있으면 되고 배포할 때는 AWS CLI가 포함 된 이미지가 필요해서 단계별로 도커 이미지를 구분했다. 처음에는 AWS CLI가 설치되어있는 이미지에 Node.js를 설치하다보니 빌드/배포가 3분 조금 넘게 걸렸는데, 아래와 같이 최적화 해서 빌드 및 배포에 약 1분 정도의 시간이 줄게 되었다.

초기 빌드타임

도커 이미지를 최적화 하기 전의 Gitlab CI/CD

최근 빌드타임

조금이나마 최적화 한 Gitlab CI/CD

개인 블로그에서 포스팅하는 빌드시간으로 과금이 될 일은 없지만(그룹별로 매월 CI/CD 400분), 깃랩같은 SaaS 서비스를 사용하면 빌드시간을 포함해서 모든 것이 비용이다보니 최적화 하는 습관을 항상 들여야 한다고 생각된다.😂

Next.js에서 마크다운 파일을 어떻게 읽을까?

이전에 구성했던 인프라와 Gitlab CI를 통해 소스를 커밋하고 푸시하면 배포되는 CI/CD 까지는 구성이 됐다. 이제 마크다운 파일 기반의 블로그를 만들어야 하는데, Next.js에서 만들어 본적은 없고, 어떻게 만드는지 찾아야 했다.

블로그를 어떻게 만드는지 궁금해서 nextjs markdown blog 라고 구글에 검색했고, 다음과 같은 next.js 공식 블로그의 포스팅을 찾게 되었다. 글을 쭉 읽어보니, remark-html을 통해서 마크다운을 HTML로 변환하고, Tailwind Typography를 통해서 HTML을 스타일링 하는 내용의 포스팅이었다. 해당 포스트와 포스팅에서 예시로 든 blog-starter을 참고로 블로그에서 마크다운 파일을 읽어가기 시작했다.

remark-html

remark-html은 마크다운으로 된 문서를 HTML로 변환해준다. remark의 플러그인으로, remark-html은 remark와 같이 설치해주면 된다.

블로그 포스팅을 위해 Remark를 다음과 같이 사용할 수 있다. (Markdown to HTML)

const result = await remark()
  .use(html, { sanitize: false }) // html : remark-html
  .process(post.content); // content : 블로그 Markdown 문서
const content = result.toString();
remark는 보통 use와 process 함수를 통해서 문서를 처리하는데, use 함수는 어떤 플러그인을 사용할지 Method Chaining을 통해서 주입하게 되고, process 함수에서 문서를 가공하고 처리한다.

@tailwind/typography

tailwindcss-typography는 Tailwind CSS를 사용하고 있을 때, prose CSS 클래스 하나만으로 HTML 태그들을 그럴싸한 문서로 보이게 해주는 스타일링 플러그인이다.

Tailwind Typography를 적용하지 않은 모습

Tailwind Typography를 적용하지 않은 모습

마크다운을 HTML로 변환했다고 해서, 스타일링까지 내가 원하는 모습으로 적용되진 않는다. 지금 보이는 블로그처럼, 마크다운에 맞는 스타일링으로 보이려면 블로그에 어떠한 스타일링이라도 적용해야 한다.

아래와 같이 className에 prose와 다크모드에서의 prose-invert 두 개만 추가해주면 tailwind에서 정의한 스타일링이 적용된다. 물론 커스텀도 된다.

<article className="prose dark:prose-invert">{content}</article>
이 정도 CSS만 추가하면 블로그에서 사용할만한 문서 스타일링이 적용된다. 지금 보고 있는 블로그가 그렇다.

highlight.js

highlight.js는 코드구문을 에디터처럼 구문강조 해주는 툴이다.

마크다운 문서를 HTML로 변환하고 스타일링까지 했는데, 개발자 블로그를 위해서는 코드 구문의 강조가 필요했다. 코드구문 하이라이트를 위한 도구로는 highlight.js와 prismJS가 많이 보였다. 아무 이유 없이, highlight.js로 내가 원하는 스타일링이 되고 많이 쓰고 유명해서 선택했다.

적용할 때는, React의 useEffect를 통해 DOM을 조작해서 포스팅 페이지 중에 pre와 code 태그가 있는 모든 구문에 먹히도록 했다.

useEffect(() => {
  document.querySelectorAll("pre code").forEach((element: any) => {
    hljs.highlightElement(element);
  });
}, []);
마크다운 문법으로 코드를 작성하면 하이라이트가 적용되도록 했다.

gray-matter

gray-matter은 마크다운 문서에서 일정한 포멧팅으로 제목과 요약, 날짜 등을 구문분석 할 때 도움이 되는 툴이다.

---
title: "Amazon API Gateway 프라이빗 통합(Amazon VPC 리소스 연결)"
excerpt: "VPC에서만 API Gateway를 통신하고, API Gateway에서 VPC를 통신하는 방법."
coverImage: "/assets/blog/api-gateway-private-integration/architecture.webp"
date: "2022-08-08T21:40:00+09:00"
ogImage:
  url: "/assets/blog/api-gateway-private-integration/architecture.webp"
---
블로그의 정보를 노출시키기 위해서는 이러한 포멧팅이 필수다.

어떠한 블로그 예제든 마크다운을 쓰고 있다면 gray-matter을 통해서 파싱을 하고 있었고, 그만큼 마크다운 기반의 사이트를 운영 할 때 편하다보니 다들 쓰고 있었다. 나 또한 일정한 형식의 마크다운 양식이 필요했고 사용하게 되었다. 원하는 포멧이 있다면 가감해서 사용하면 된다.

next-themes

다크모드 예시

다크모드 예시. 버튼 하나로 다크모드를 On/Off 할 수 있도록 개발했다.

next-themes를 사용하면 Next.js 사이트의 다크모드를 useTheme라는 React Hook으로 손쉽게 구현할 수 있다. Tailwind CSS를 사용하더라도, 함께 적용하는 방법을 친절하게 알려준다. useTheme를 통해서 set함수를 이용하니 버튼을 통한 다크모드를 너무 손쉽게 구현했다.

import { FC, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Moon, Sun } from "react-feather";

const ThemeButton: FC = () => {
  const { theme, systemTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  const currentTheme = theme === "system" ? systemTheme : theme;

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted)
    return <div className="ml-1 w-6 h-6 mr-1 sm:ml-4 cursor-pointer" />;
  return (
    <div
      className="ml-1 mr-1 sm:ml-4 cursor-pointer"
      onClick={(e) => {
        setTheme(currentTheme === "dark" ? "light" : "dark");
        e.preventDefault();
      }}
    >
      {currentTheme === "dark" ? <Moon /> : <Sun />}
    </div>
  );
};

이슈가 있었는데, 카카오톡에 링크를 올렸을 때 카카오톡 전용 브라우저에서 다크모드가 적용되지 않는 버그가 있었다. 카카오톡 브라우저를 제외한 어디에서도 발생하지 않아서 고칠까 하다가, globals.css에서 body에 bg-black을 먹여버리는 것으로 해결하고 치웠다.

body {
  @apply dark:bg-black;
}

SEO (Meta Tag)

검색엔진에서 블로그가 잘 노출되기 위해서 메타태그를 적용하기 시작했다. 또한 페이스북이나 트위터같은 SNS를 통해 블로그 링크를 보냈을 때, Open Graph를 통해서 웹페이지의 미리보기가 될 수 있도록 구성했다.

구글 검색 예시

구글 검색에 적용 된 타이틀, 날짜, 요약과 이미지 미리보기 예시.

트위터 오픈그래프 예시

트위터에 적용 된 OpenGraph 예시.

SEO같은 경우, Next.js 블로그 예제들을 참고해서 SEO 컴포넌트를 따로 만들었다. 페이지에서 적용 될 SEO와 포스팅에서 적용될 SEO 컴포넌트를 분리해서, 각각 맞춤으로 적용되도록 했다.

export const BlogSEO: FC<BlogSEOProps> = ({
  title,
  description,
  ogImage,
  publishedAt,
  modifiedAt,
}) => {
  return (
    <SEO
      title={title}
      description={description}
      ogType="article"
      ogImage={ogImage}
      twImage={ogImage}
      publishedAt={publishedAt}
      modifiedAt={modifiedAt}
    />
  );
};
공통 SEO 컴포넌트를 만들고, 블로그와 페이지 SEO에 맞는 데이터를 주입하는 방식으로 해결

블로그 포스팅은 이미 구현된 Dynamic Routing 페이지에서 MD파일만 읽으니 더 이상 추가할 필요가 없고, 만약 새로운 라우팅이 추가되면, 추가되는 페이지마다 필요한 SEO를 Head 태그 안에 추가하면 된다.

next-sitemap

누가 언제 무슨 키워드로 블로그에 들어왔는지 확인하기 위해 Google Search Console을 등록했다.

구글 검색 예시

누군가는 구글에서 내 블로그를 찾아줄테고, 그에 대한 키워드와 통계가 필요하다.

사이트맵을 등록해야 구글이 열심히 읽어갈텐데, 블로그 사이트에는 아직 사이트맵이 없었고, Next.js를 위한 사이트맵 생성기가 있나 봤더니 next-sitemap이라는게 바로 검색됐다. 블로그를 만들면서 놀랐던건, 세상 많은 개발자들이 블로그를 포함한 마크다운 사이트를 만들고자 노렸했고, 이러한 블로그 사이트 하나 만들기에는 관련된 기술이 없는게 없었다(그만큼 넘쳐났다). 사이트맵과 로봇 또한 만들기 쉬워서, package.json에 postbuild 하나만 추가하면 바로 사이트맵과 robots.txt 파일이 출력되었다. 깃랩에서는 더 설정할 것도 없이 바로 적용되었고, 이렇게 블로그 어플리케이션이 완성되었다.

정리

블로그를 만들면서 마크다운 기반의 Next.js 개발에 대한 경험치가 생겼다. 또한 즐겨쓰던 CDK에서 Static Website 인프라 개발도 경험하게 되었다. 앞으로 어떤 기능이 필요할진 모르지만, 더 개선해 나가야겠고 개선점을 작성해보면서 기를 모아야겠다. (유튜브 영상도 찍어서 올리고 싶은게 많은데..😭)

블로그 개선 사항

  • 내 블로그의 검색엔진 최적화는 아직 부족하다. 구글 검색했을 때, 프리티(연관 글, 글 요약/미리보기 등)하게 나오지 않는 문제가 있는 것 같다.
  • SNS에 퍼갈 수 있는 링크가 필요하다. 페이스북, 트위터 등. 언젠가 나의 개발을 좋아해주고, 글을 잘 쓰게 된다면 내 글을 좋아해주고 퍼갈 수도 있지 않을까?
  • RSS를 아직도 쓰나? RSS를 많이 사용해서 RSS를 통해서 글을 구독하나? 구현을 해야하는건가? 잘 모르겠다. 이에 대한 통계를 검색해봐야겠다.
  • 글에 대한 커뮤니케이션(댓글 등) 기능이 필요하다. 방명록이라도. 누군가는 블로그에 와서 욕 또는 광고를 하고 싶어할 수도 있고. 소통할 길이 없어 답답할 수도 있으니까.