lechuck.dev

Astro를 사용한 기술 블로그 만들기

마지막 업데이트:

블로그를 만들면서 어떤 주제로 첫 포스팅을 해 볼까 고민하다가, 블로그 설정 과정을 주제로 첫 포스팅을 해 봅니다.

블로그 만들기 첫 시도는 많이들 사용하고 추천하 Gatsby 였습니다. 프레임워크 자체는 많은 기능을 제공하긴 하지만, 이러한 기능들을 사용하기 위해서 읽어야하는 문서의 양도 많고 설정도 복잡한 것이 문제였습니다. 간단한 블로그를 만들고 싶었는데, 너무 많은 정보와 설정에 압도된 느낌이랄까?

결국 한동안 블로그 만들기는 접어두고 있다가, State of JS 2022 Rendering Frameworks에서 가장 높은 Retention 을 받은 Astro를 보고 다시 블로그 만들기에 도전하게 됩니다.

참고로 Gatsby 는 38%라는 처참한 Retention 비율을 받은걸로 봐서 저만 그런게 아니었다는 것에 큰 위로를 받았습니다. 😀

Gatsby를 사용하다 Astro 로 옮겨오면서 느낀 장점은 다음과 같습니다.

이제 다음과 같은 순서대로 기술 블로그를 만들어 보겠습니다.

  1. Astro 를 사용한 프로젝트 생성 및 실행
  2. 배포 (정적 사이트 또는 SSR)
  3. 개인 도메인 연결
  4. 댓글 기능 추가
  5. 페이지 미리보기를 위한 OpenGraph 지원

1. 프로젝트 생성

Astro 에서 제공하는 CLI 도구를 사용해 프로젝트를 생성할 수 있습니다. 빈 프로젝트를 생성해서 하나씩 설정해가면서 추가해도 되지만, 귀찮으니 일단 있는 템플릿 중에서 쓸만한 걸 찾아서 프로젝트를 생성합니다. astro.new 에서 여러가지 템플릿을 찾을 수 있는데, 저는 Blog 를 골랐습니다.

$ pnpm create astro@latest --template blog

Themes 에서 맘에 드는 테마를 골라서 생성하는 방법이 있긴 한데, Astro v2 가 출시된지 얼마되지 않은 현 시점에서는 대부분 v1 기반 템플릿이라 v2 를 사용하기 위해서는 직접 업그레이드를 해야하는 문제가 있습니다. 일단은 v2 용 공식 템플릿을 사용해 생성한 후, 원하는 테마의 스타일과 구조를 참고하는 식으로 진행을 합니다.

디렉토리 구조

├── public/
├── src/
│   ├── components/
│   ├── content/
│   ├── layouts/
│   └── pages/
├── astro.config.mjs
├── README.md
├── package.json
└── tsconfig.json

위와 같은 디렉토리 구조로 프로젝트가 생성됩니다. 주요 파일들을 살펴보면:

실행

# 로컬 개발 서버 실행. localhost:3000 으로 접속
$ pnpm run dev

배포용 빌드는 다음과 같이 실행합니다.

# 배포용 빌드
$ pnpm run build
# 빌드된 배포판을 사용해 서버 실행. localhost:3000 으로 접속
$ pnpm run preview

2. 배포

이제 블로그를 정적사이트로 배포할 것인지, SSR 서버로 배포할 것인지 결정해야 합니다.

정적 사이트 배포 (Cloudflare Pages)

먼저 무료 버전에서도 사용량을 넉넉하게 주는 Cloudflare Pages 에 정적 사이트를 배포하도록 하겠습니다.

기타 다른 서비스별 배포 방법은 Deploy your Astro Site 페이지를 참고하시면 됩니다.

  1. 먼저 Cloudflare 에 계정이 없다면 가입부터 해야 합니다.

  2. 코드 저장소는 GitHub 이나 Gitlab 저장소에 푸시되어 있어야 합니다. 저는 GitHub 계정을 사용합니다.

  3. Cloudflare 대시보드 -> Pages 화면으로 이동한 다음, 프로젝트 생성을 실행합니다.

  4. GitHub 계정에서 연결할 저장소를 선택한 다음, 설정 시작을 실행합니다.

  5. 다음과 같이 빌드 설정을 구성합니다.

    • 프레임워크 미리 설정: Astro
    • 빌드 명령:
      • npm install -g pnpm && pnpm run build
      • Cloudflare 빌드 이미지에 pnpm 이 설치되어 있지 않기 때문에 설치가 필요합니다.
    • 빌드 출력 디렉터리: dist
    • 환경 변수 (고급):
      • NODE_VERSION: v16.19.0
      • Cloudflare 에서 기본 제공하는 node.js 버전이 Astro 최소 요구 버전v16.12.0 보다 낮을 수 있기 때문에 사용할 node.js 버전을 직접 지정해야 합니다.
  6. 저장 및 배포 를 클릭합니다.

  7. 배포가 성공하게 되면 https://<projectName>.pages.dev 로 접속해서 배포된 것을 확인할 수 있습니다.

미리보기 배포 구성의 기본값은 모든 브랜치에 커밋이 푸시될 때마다 배포가 진행됩니다. 자주 커밋하는 경우 무료 사용량을 초과할 수 있으므로, 본인의 상황에 맞게 미리 보기 배포 구성을 변경하는 것이 좋습니다.

SSR 사이트 배포 (Deno Deploy)

SSR 사이트를 배포하려면 렌더링을 위한 서버가 필요합니다. 개인적으로 사용중인 서버(예: AWS EC2, …)가 있다거나 NAS가 있다면 그 곳에 배포를 할 수 있겠지만, 관리의 귀찮음과 함께 서버 비용도 필요하기 때문에 여기서는 무료로 제공되는 서버리스 서비스를 사용해 SSR 사이트를 배포해 보겠습니다.

Deploy your Astro Site 페이지에서 SSR 배포를 지원하는 서비스를 확인해보면 여러가지가 있습니다.

저는 여기서 Deno Deploy 를 사용하도록 하겠습니다. Deno Deploy 를 선택한 이유는:

  1. 무료 버전 사용 가능
  2. 배포하지 않고 로컬에서 실행 가능

서버리스를 사용하는 경우 대부분 배포를 해야 실행이 가능하지만, Deno Deploy는 로컬에서 Deno 를 사용해 실행이 가능하기 때문에 개발 이터레이션이 단축되는 장점이 있습니다. 물론 블로그 특성상 일단 사이트를 구축하고 나면 추가로 손댈 일이 별로 없는 경우가 대부분이라서 개인 취향에 따라 다른 서비스를 사용해도 됩니다.

  1. Deno 사이트에 로그인 합니다. 처음 로그인이라면 GitHub 계정과 연동하는 화면이 표시됩니다.

  2. Astro 프로젝트에서 SSR 을 사용하려면 Deno 어댑터를 추가해야 합니다.

    $ pnpx astro add deno
  3. 위와 같이 실행하면 다음과 같이 변경됩니다.

    • package.json 파일에 @astrojs/deno 디펜던시 추가
    • astro.config.mjs 파일에 adapter, output 설정 변경
  4. package.json 파일을 열고, preview 스크립트를 다음과 같이 변경합니다:

    {
      // ...
      "scripts": {
        // ...
        "preview": "deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs"
      }
    }
  5. SSR 을 사용하는 경우 getStaticPaths() 함수를 사용할 수 없기 때문에, src/pages/blog/[...slug].astro 파일의 frontmatter를 다음과 같이 수정해야 합니다.

    import { CollectionEntry, getCollection } from "astro:content";
    import BlogPost from "../../layouts/BlogPost.astro";
    
    const posts = await getCollection("blog");
    type Props = CollectionEntry<"blog">;
    
    const { slug } = Astro.params;
    const post = posts.find((page) => page.slug === slug);
    if (!post) {
      return Astro.redirect("/404");
    }
    const { Content } = await post.render();
  6. 이제 로컬에서 SSR 프리뷰 모드로 실행합니다. 실행된 서버는 http://localhost:8085 로 접속할 수 있습니다.

    $ pnpm build && pnpm preview

GitHub 액션을 사용해 Deno Deploy 배포

  1. 배포할 코드를 먼저 GitHub 저장소에 푸시합니다. 저장소는 공개/비공개 상관없습니다.

  2. Deno Deploy에 로그인 한 다음, New Project를 실행합니다.

  3. 저장소 선택 -> 배포할 브랜치 선택 -> Github Action 모드 선택 -> 프로젝트 이름 설정 후 Link를 실행합니다.

  4. 프로젝트를 추가하면 사용할 GitHub Actions 예제 파일이 표시됩니다. .github/workflows/deploy.yml 파일을 만들고 표시된 예제 파일을 다음과 같이 수정해서 저장합니다.

    name: Deploy
    on: [push]
    
    jobs:
      deploy:
        name: Deploy
        runs-on: ubuntu-latest
        permissions:
          id-token: write
          contents: read
    
        steps:
          - name: Clone repository
            uses: actions/checkout@v3
    
          - uses: pnpm/action-setup@v2
            name: Install pnpm
            with:
              version: 6.0.2
    
          - name: Install dependencies
            run: pnpm install
    
          - name: Build Astro
            run: pnpm build
    
          - name: Upload to Deno Deploy
            uses: denoland/deployctl@v1
            with:
              project: my-project # 생성한 프로젝트 이름으로 변경합니다.
              entrypoint: server/entry.mjs
              root: dist
  5. 이제 변경사항을 GitHub 에 푸시하면 배포가 시작됩니다. GitHub 저장소의 Actions 탭에서 배포 진행 상황을 확인할 수 있습니다.

  6. 배포가 끝나면 Deno Deploy에서 배포 상태를 확인할 수 있습니다. 배포된 도메인도 대시보드에서 확인할 수 있으며, https://<projectName>.deno.dev 로 접속하면 됩니다.

3. 개인 도메인 연결

개인 도메인을 보유하고 있다면 배포 사이트에서 제공하는 주소 대신 개인 도메인을 연결할 수 있습니다. 개인 도메인이 없는 경우 이번 단계는 건너뛰고 다음 단계로 진행합니다.

저는 lechuck.dev 도메인을 보유하고 있기 때문에 구글 도메인을 기준으로 설명하도록 하겠습니다.

Cloudflare 도메인 연결

  1. Cloudflare 대시보드 -> Pages 화면으로 이동 -> 프로젝트 선택 -> 사용자 설정 도메인 -> 사용자 설정 도메인 설정 버튼을 클릭합니다.
  2. 사용할 도메인을 입력합니다. 여기서는 tech.lechuck.dev 를 입력한 후 계속 버튼을 클릭합니다.
  3. DNS 설정 방법을 선택하는데 내 DNS 공급자를 사용하기 위해 CNAME 설정 시작 버튼을 클릭하면 DNS 공급자에 등록할 CNAME 레코드가 표시됩니다.
  4. 표시된 레코드를 구글 도메인에 등록합니다.
  5. DNS 레코드 확인 버튼을 클릭하면, Cloudflare 에서 DNS 소유 여부를 확인한 후 인증서를 생성합니다.
  6. 확인이 되었다면 사용자 설정 도메인이 활성화된 것을 확인할 수 있습니다.
  7. 이제 등록한 도메인으로 접속해서 배포된 페이지가 정상적으로 표시되는지 확인합니다.

Deno Deploy 도메인 연결

  1. Deno Deploy 대시보드로 접속한 후, 생성한 프로젝트를 선택합니다.
  2. Settings -> Domains 섹션에 있는 + Add Domain 버튼을 클릭합니다.
  3. 연결할 개인 도메인 주소를 입력합니다. 사용할 blog.lechuck.dev 를 입력한 다음 Save -> Setup 버튼을 클릭하면 DNS RECORDS 정보가 표시됩니다.
  4. 표시된 레코드들을 모두 구글 도메인에 등록합니다. 현재는 A, AAAA, CNAME 3개를 등록해야 하는걸로 나옵니다.
  5. 레코드를 모두 등록했다면 Deno Deploy 에서 Validate 버튼을 눌러 DNS 소유 여부를 확인합니다.
  6. 도메인 소유여부가 확인되면 인증서 생성 방법을 선택합니다.
    • Get automatic certificates: Let’s Encrypt 를 사용해 인증서를 자동 발급합니다.
    • Upload your own certificates: 보유하고 있는 인증서가 있다면 업로드합니다.
  7. 인증서 발급은 시간이 좀 걸리며, 인증서가 발급되면 인증서가 발급된 것을 확인할 수 있습니다.
  8. 이제 등록한 도메인으로 접속해서 배포된 페이지가 정상적으로 표시되는지 확인합니다.

4. 댓글 기능 추가

블로그 포스팅에 대해서 피드백을 주고 받고 싶다면 댓글 기능을 추가하는 것이 좋습니다.

utterance

utterances는 바닐라 타입스크립트로 작성된 오픈 소스 라이브러리이며, GitHub Issues 에 댓글을 저장합니다. 별도의 서버를 필요로 하지 않기 때문에, 정적 사이트로 배포하는 경우 유용합니다.

utterances 의 작동 방식은 다음과 같습니다:

  1. 페이지 로드시 GitHub issue search API를 사용해 페이지 url, pathname 이나 title 에 연결된 issue 를 검색합니다.
  2. 연결된 issue가 없는 상태에서 누군가 댓글을 추가하게 되면 utterances-bot 이 자동으로 issue를 생성합니다.
  3. 댓글을 달기 위해서는 GitHub OAuth flow를 사용해 utterance 앱이 댓글을 추가할 수 있도록 허용하거나, GitHub issue 에 직접 댓글을 달아야 합니다.

블로그 연동 순서는 다음과 같습니다:

  1. 댓글을 저장할 GitHub 공개 저장소를 생성합니다. 여기서는 blog-comments 로 생성합니다.

  2. utterances app을 설치해야 합니다. blog-comments 저장소에 읽기/쓰기 권한을 추가합니다.

  3. App 을 추가하고 나면 설정 페이지로 이동하게 되는데 Repository 섹션의 repo 설정에 “owner/repo” 형식으로 방금 추가한 저장소를 입력합니다. 여기서는 lechuckroh/blog-comments 를 입력하겠습니다.

  4. Blog Post ↔️ Issue Mapping 에서 블로그 포스트와 GitHub Issue 연결 방법을 설정합니다.

    • Issue title contains page pathname: 제목에 블로그 포스트의 URL 경로명이 포함된 Issue를 찾아 연결합니다.
    • 예를 들어, 이 페이지에서 댓글을 달게 되면 blog/create-blog-with-astro/ 라는 제목으로 GitHub Issue 가 생성됩니다.
  5. Theme 에서 블로그의 컬러에 맞는 테마를 선택합니다. 지금까지 만든 기본 블로그 템플릿을 사용하고 있다면 GitHub Light 테마를 선택합니다.

  6. Enable Utterances 에는 지금까지 입력한 설정을 기반으로 레이아웃에 삽입할 스크립트가 표시됩니다.

    <script
      src="https://utteranc.es/client.js"
      repo="lechuckroh/blog-comments"
      issue-term="pathname"
      theme="github-light"
      crossorigin="anonymous"
      async
    ></script>
  7. 이 스크립트를 그대로 사용하는 대신, 댓글을 위한 컴포넌트를 별도로 만들어 보겠습니다. src/components/Utterances.astro 파일을 다음과 같이 추가합니다.

    • 컴포넌트에 utterances-container라는 id를 가지는 div 엘리먼트를 추가합니다.
    • 제공된 자바스크립트 태그를 frontmatter(---)에 추가하는게 아니라 <script> 태그안에 작성해야 합니다.
    • script 엘리먼트를 작성한 후, 컨테이너 엘리먼트에 추가합니다.
    <div id="utterances-container"></div>
    
    <script>
      const script = document.createElement("script");
      const container = document.querySelector("#utterances-container");
    
      Object.entries({
        src: "https://utteranc.es/client.js",
        repo: "lechuckroh/blog-comments",
        "issue-term": "pathname",
        theme: "github-light",
        crossorigin: "anonymous",
      }).forEach(([key, value]) => {
        script.setAttribute(key, value);
      });
    
      container?.appendChild(script);
    </script>
  8. 블로그 포스트 아래에 댓글을 추가하기 위해서 pages/blog/[...slug].astro 레이아웃에 Utterances 컴포넌트를 추가합니다.

    <BlogPost {...post.data}>
      <Content />
      <Utterances />
    </BlogPost>
  9. 댓글을 달기 위해서는 GitHub 계정으로 로그인해야합니다.

  10. 로그인 버튼을 클릭하면 utterances 에서 대신 GitHub Issue 에 댓글을 추가할 수 있는 권한을 요청하는데, 승인을 하면 댓글을 추가할 수 있게 됩니다.

  11. 앞에서 승인을 한 다음 리다이렉션되는 URL 은 astro.config.mjs 파일의 site에 설정된 URL 입니다. 로컬 개발 서버에서 댓글 기능을 테스트하기 위해서는 site 설정을 다음과 같이 변경합니다.

    export default defineConfig({
      site: import.meta.env.DEV
        ? "http://localhost:3000"
        : "https://blog.lechuck.dev",
      // ...
    });

5. 페이지 미리보기를 위한 OpenGraph 지원

OpenGraph 프로토콜은 HTML 메타 태그를 사용해 소셜 미디어 공유시에 웹사이트 콘텐츠에 대한 정보를 제공합니다.

소셜 미디어에 블로그 포스트를 공유했을 때, 포스트의 제목, 설명, 이미지를 표시하기 위해서는 다음과 같은 메타 태그가 필요합니다.

1단계에서 Blog 템플릿을 사용해 프로젝트를 생성했었다면, src/components/BaseHead.astro 파일을 열어보면 다음과 같이 설정되어 있을 것입니다.

const {
  title,
  description,
  image = "/images/placeholder-social.jpg",
} = Astro.props;
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="{Astro.url}" />
<meta property="og:title" content="{title}" />
<meta property="og:description" content="{description}" />
<meta property="og:image" content="{new" URL(image, Astro.url)} />

OpenGraph 지원을 위한 메타 태그가 이미 추가되어 있으며, title, description, image 값을 사용하는 것을 볼 수 있습니다.

image는 미지정시 /images/placeholder-social.jpg 이미지를 사용하기 때문에, 블로그 포스트별로 별도의 이미지를 사용하려면 마크다운 문서에서 이미지를 지정해야 합니다.

이 글을 쓰는 시점에 우리가 사용한 템플릿에 버그가 있어서 마크다운 문서에서 heroImage 속성을 추가하더라도 image 값이 전달되지 않는 문제가 있습니다. 이를 수정해 보도록 하겠습니다.

src/layouts/BlogPost.astro 컴포넌트에서 BaseHead 컴포넌트를 사용하는 곳을 보면, image 속성이 전달되고 있지 않습니다.

<BaseHead title={title} description={description} />

이제 다음과 같이 image 속성에 heroImage 값을 전달하도록 수정합니다.

<BaseHead title={title} description={description} image={heroImage}> />

이제 마크다운 문서의 Frontmatter(---)에 heroImage 를 추가합니다.

---
title: "Astro를 사용한 기술 블로그 만들기"
description: "Astro 기반의 기술 블로그 생성, 배포, 도메인 연결, 댓글 기능 추가, 페이지 미리보기를 위한 OpenGraph 지원까지의 방법에 대해 알아봅니다."
...
heroImage: "/images/create-blog-with-astro/astro-hero.jpg"
---

이제 pnpm dev 를 실행해 개발 서버를 실행한 다음, 브라우저 개발자 도구를 통해 메타 태그가 제대로 설정되었는지 확인합니다.

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta
  property="og:url"
  content="http://localhost:3000/blog/create-blog-with-astro/"
/>
<meta property="og:title" content="Astro를 사용한 기술 블로그 만들기" />
<meta
  property="og:description"
  content="Astro 기반의 기술 블로그 생성, 배포, 도메인 연결, 댓글 기능 추가, 페이지 미리보기를 위한 OpenGraph 지원까지의 방법에 대해 알아봅니다."
/>
<meta
  property="og:image"
  content="http://localhost:3000/images/create-blog-with-astro/astro-hero.jpg"
/>

제대로 표시되는 것을 확인했다면, 이제 배포해서 미리보기가 제대로 나오는지 확인합니다.

슬랙에 공유한 경우 다음과 같이 표시되는 것을 볼 수 있습니다.