10년 묵은 jekyll 블로그를 astro로 옮기기

2026-07-17

이 블로그는 2016년에 jekyll로 만들었다. 그리고 10년 동안 글만 몇 개 올렸을 뿐, 블로그 자체에는 거의 손을 대지 않았다. 잘 돌아가는 것처럼 보였기 때문이다. 정적 사이트라 서버가 죽을 일도 없고, GitHub Pages가 알아서 빌드해주니 신경 쓸 일이 없었다. 그런데 오랜만에 열어보니 상태가 처참했다.

마지막이 특히 뼈아팠다. 애널리틱스는 종료된 지 3년이 지났으니, 그동안 수집된 데이터가 하나도 없었다. 방문자 수를 확인해 본 적이 없어서 몰랐던 것이다. 정적 사이트가 안 죽는다는 말은 틀린 게 아니라 부정확하다. 사이트는 안 죽지만, 사이트가 의존하는 것들은 조용히 죽는다. 그리고 정적 사이트는 그게 죽어도 여전히 200을 응답하기 때문에 알아차릴 방법이 없다.

이왕 손대는 김에 astro로 갈아엎기로 했다.

이번 포스팅을 간략하게 정리하면 아래와 같은 내용이다.

왜 astro인가

요즘 정적 블로그를 만든다면 선택지는 대략 astro, hugo, eleventy 정도다. gatsby는 확실히 저물었다. 2020년쯤엔 이 바닥의 정답처럼 보였지만, 지금은 유지보수가 사실상 멈췄고 GraphQL로 마크다운을 읽는 방식은 과했다는 평가가 굳어졌다.

hugo는 진지하게 고민했다. 빌드가 비교가 안 되게 빠르고, 단일 바이너리라 node_modulesGemfile도 없다. 무엇보다 jekyll과 사고방식이 비슷해서 — 폴더가 곧 분류이고 설정 파일에 URL 템플릿을 적는 식이다 — 이전이 가장 수월했을 것이다. 지금 와서 보면 이 글에 나오는 삽질의 절반은 hugo를 골랐다면 안 겪었을 일이다.

그럼에도 astro를 골랐다. 이유는 세 가지였다.

결과부터 말하면 잘한 선택이었다. 다만 jekyll을 쓰던 감각으로 접근하면 반드시 헤맨다. 뒤에 나오겠지만, astro에는 블로그라면 당연히 있을 거라고 생각한 기능이 없다.

옮기기 전에: 죽은 의존성 걷어내기

프레임워크를 바꾸기 전에 먼저 jekyll 상태에서 라이브러리부터 정리했다. 옮기면서 동시에 하면 뭐가 깨졌을 때 원인이 프레임워크인지 라이브러리인지 알 수 없기 때문이다.

jQuery가 하던 일

jQuery 3.1.1이 CDN에서 로드되고 있었다. 그럼 이걸로 뭘 하고 있었나 하고 열어봤더니, 전부 합쳐서 이 정도였다.

30KB를 받아서 하는 일이 클래스 토글과 이벤트 리스너 하나였다. 2016년이라면 브라우저 호환성 때문에 정당한 선택이었겠지만, 지금은 querySelectorclassList가 어디서나 된다.

document.querySelector('.navbar-burger').addEventListener('click', (e) => {
  e.currentTarget.classList.toggle('is-active');
});

버전을 3.7.1로 올릴까 잠깐 생각했는데, 어차피 지울 거면 올릴 이유가 없었다. 그냥 지웠다.

FontAwesome이 하던 일

FontAwesome은 킷 방식으로 들어와 있었다.

<script src="https://kit.fontawesome.com/bb01885591.js" crossorigin="anonymous"></script>

이건 JS를 받아서, 그 JS가 CSS를 받고, 그 CSS가 폰트 파일을 받는 구조다. 이렇게 해서 쓰고 있던 아이콘이 홈, 검색, 정보 세 개였다. 아이콘 3개 때문에 폰트 파일과 런타임 의존성을 통째로 지고 있었던 것이다.

Lucide의 SVG를 그냥 인라인으로 박았다. 네트워크 요청이 0이 되고, 렌더링도 더 선명하고, 색은 currentColor로 알아서 따라온다. 재사용은 컴포넌트 하나로 해결했다.

여기까지 하고 나니 외부 런타임 의존성이 전부 사라졌다. 이 상태에서 astro로 넘어갔다.

jekyll과 astro의 결정적 차이

카테고리 시스템이 없다

이걸 깨닫는 데 제일 오래 걸렸다.

jekyll에서 카테고리는 프레임워크가 주는 것이었다. _posts/dev/java/ 폴더에 글을 넣으면 알아서 카테고리가 잡히고, site.categories로 어디서든 꺼내 쓸 수 있었다. 내가 한 일은 폴더를 만든 것뿐이다.

astro에는 그런 게 없다. 카테고리도, 태그도, 시리즈도 없다. 처음엔 내가 문서를 못 찾는 줄 알고 한참 뒤졌는데, 정말 없는 게 맞았다. 대신 Content Collections와 스키마가 있다.

// src/content.config.ts
const posts = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    category: z.enum(CATEGORY_PATHS),
    tags: z.array(z.string()).default([]),
  }),
});

astro의 입장은 분류 체계는 네가 설계해라다. category라는 필드는 프레임워크가 아는 개념이 아니라 내가 스키마에 적어서 생긴 필드일 뿐이고, 그걸로 목록을 만들고 URL을 뚫는 것도 전부 내 몫이다.

처음엔 불친절하다고 느꼈다. 블로그 프레임워크가 카테고리를 안 준다니. 그런데 쓰다 보니 이쪽이 낫다는 걸 알게 됐다. z.enum(CATEGORY_PATHS)처럼 허용된 값을 못 박아두면, 오타가 빌드에서 잡히기 때문이다.

category: Invalid input: expected "dev"

jekyll 시절엔 categories: devv라고 오타를 내도 아무 일도 일어나지 않았다. 그냥 글 하나짜리 유령 카테고리가 조용히 하나 생길 뿐이었다. 실제로 이 블로그엔 글은 있는데 메뉴엔 없는 카테고리메뉴엔 있는데 글이 0개인 카테고리가 둘 다 있었고, 몇 년째 그 상태였다. 옮기면서야 발견했다.

프레임워크가 알아서 해준다는 건, 뒤집으면 내가 틀려도 알려주지 않는다는 뜻이다.

URL도 파일이 결정한다

jekyll_config.yml에 URL 규칙을 적었다.

permalink: /post/:title
paginate_path: "/list/:num/"

astro엔 이런 설정이 아예 없다. 찾지 말자. 파일을 어디에 두느냐가 곧 URL이다.

src/pages/index.astro                  →  /
src/pages/post/[slug].astro            →  /post/<slug>/
src/pages/list/[page].astro            →  /list/<n>/
src/pages/categories/[...path].astro   →  /categories/<a/b/c>/

파일명에 대괄호가 붙으면 “틀만 있고 실제 값은 코드가 정한다”는 뜻이다. [slug]는 한 칸, [...path]는 슬래시를 포함한 여러 칸을 받는다. 실제 값은 getStaticPaths()가 만들어낸다.

export const getStaticPaths = (async () => {
  const posts = await getPublishedPosts();
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
});

이 함수가 배열을 하나 뱉으면, 빌드할 때 그 개수만큼 HTML이 찍혀 나온다. 페이지를 미리 다 만들어두는 방식이라 런타임에 라우팅이랄 게 없다.

여기서 post.id가 파일명이라는 게 중요했다. 그래서 2017-09-03-java-builder-pattern.markdownjava-builder-pattern.md로 이름만 바꾸면 /post/java-builder-pattern/이 그대로 나온다. jekyll이 파일명에서 날짜를 떼고 :title을 뽑아 쓰던 것과 결과가 같다. 날짜는 front matter에 있으니 파일명에 또 있을 이유가 없다.

이 방식이 처음엔 번거로워 보였는데, 실제로는 반대였다. permalink를 바꿨을 때 어떤 URL이 나올지 머릿속으로 시뮬레이션할 필요가 없다. 파일 경로를 보면 URL이 보인다.

그럼 페이징도 직접 만드나

파일이 곧 URL이라면, 글이 늘어날 때마다 /list/4/, /list/5/를 직접 만들어야 하나 싶었다. 아니다. 대괄호 파일 하나면 된다.

export const getStaticPaths = (async ({ paginate }) => {
  const posts = await getPublishedPosts();
  return paginate(posts, { pageSize: 10 });
});

paginate()가 글 개수를 보고 페이지 수를 계산해서 경로를 알아서 뽑아준다. 반신반의해서 글을 21개에서 33개로 늘려놓고 빌드해봤는데, 정말 파일을 안 건드려도 /list/4/가 생겼다. 정확히는 글을 쓰면 페이지가 생기는 게 아니라, 빌드할 때마다 전체를 다시 계산해서 찍는 것이다.

옮기는 과정

글 옮기기

_posts 아래에 마크다운이 44개 있었다. 그런데 실제로 공개된 건 21개였다. 나머지 23개는 published: false, 즉 초안이었다. 여기에 더해 .hide/라는 폴더에 18개가 더 있었다. 앞으로 쓰겠다고 제목만 잡아둔 것들이다.

정리하면 글 21개를 위해 파일 62개를 이고 다녔다. 이것도 이번에 세어보고 알았다.

옮기면서 실제로 손댄 건 이 정도다.

마지막 둘은 결국 Liquid의 흔적이다. jekyll은 마크다운 파일을 템플릿으로도 취급해서, 본문에 {{ }}가 있으면 그걸 변수로 해석하려 든다. astro는 마크다운을 그냥 마크다운으로 읽는다.

코드 하이라이팅

rougeshiki로 바뀐다. astro에 내장이라 설정 한 줄이면 된다.

markdown: {
  shikiConfig: { theme: 'github-light' },
}

rouge는 CSS 클래스를 붙여놓고 색은 별도 스타일시트에서 입히는 방식이었는데, shiki는 빌드할 때 VS Code의 문법 분석기를 돌려서 색을 인라인 스타일로 직접 박아넣는다. 그래서 테마 CSS를 따로 관리할 필요가 없다.

대신 언어를 지정하지 않은 코드블록은 색이 안 입혀진다. 색을 박아넣을 근거가 없으니 당연한 동작이고, rouge도 마찬가지였으니 손해는 아니다. 그런데 이 사소한 차이 때문에 나중에 한참을 헤맨다. (후술)

CSS 프레임워크 갈아끼우기

Bulma도 결국 걷어냈다. 처음엔 1.0으로 올려서 계속 쓰려고 했는데, 커스텀이 필요할 때마다 !important를 붙이게 되는 게 마음에 걸렸다. 프레임워크를 이길 방법이 CSS 특이도 싸움밖에 없다면 그건 프레임워크를 쓰는 게 아니라 프레임워크와 싸우는 것이다.

tailwind로 갈아탔다. v4부터는 설정이 JS 파일이 아니라 CSS로 들어간다.

@import "tailwindcss";

@theme {
  --color-accent-500: hsl(244 55% 55%);
  --font-sans: 'Pretendard Variable', Pretendard, sans-serif;
  --radius-card: 1rem;
}

여기서 스스로 정한 원칙이 하나 있다. 되도록 프레임워크의 기능으로 해결하고, 커스텀 CSS를 새로 짜지 않는다. @theme에 토큰을 등록하면 bg-accent-500, rounded-card 같은 유틸리티가 자동으로 생긴다. 색이 필요할 때 #564DCB를 어딘가에 하드코딩하는 대신 토큰을 하나 늘리는 식이다. 이렇게 해두면 나중에 액센트 색을 바꿀 때 고칠 곳이 한 군데다.

번들은 최종적으로 37KB, gzip 7KB가 나왔다. Bulma는 CDN에서 프레임워크 전체를 받아왔으니 — 안 쓰는 컴포넌트까지 전부 — 이 정도 차이는 당연하다.

카테고리 정리

옮기면서 카테고리를 세어봤다. dev/java, dev/javascript, dev/python 식으로 잘게 나눠놨는데, 글 21개가 전부 dev/* 아래였다. Talk 같은 다른 1뎁스 카테고리는 글이 0개였다.

100%인 분류는 아무것도 분류하지 못한다. 전부 dev 하나로 합쳤다.

다만 여기서 하나 정했다. 2뎁스 카테고리는 언제든 다시 생길 수 있으니, UI와 CSS는 살려둔다. 지금 안 쓴다고 드롭다운 코드를 지워버리면, 나중에 카테고리를 하나 추가할 때 UI를 다시 만들어야 한다. 그래서 데이터 구조와 렌더링은 2뎁스를 그대로 지원하되, 지금 데이터가 1뎁스일 뿐인 상태로 뒀다.

export const CATEGORY_NAV: NavItem[] = [{ title: 'Dev', path: 'dev', use: true }];

items를 달아주면 그날로 드롭다운이 생긴다. 카테고리 목록 페이지도 [...path]라 슬래시가 몇 개든 받는다.

배포

GitHub Pagesjekyll만 자동으로 빌드해준다. 이건 Pages가 원래 jekyll 호스팅으로 시작한 서비스라서 그렇다. astroGitHub Actions로 직접 빌드해서 결과물을 올려야 한다.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: withastro/action@v3
        with:
          node-version: 22
  deploy:
    needs: build
    steps:
      - uses: actions/deploy-pages@v4

그리고 여기서 놓치기 쉬운 게 있다. 저장소 설정에서 Pages 소스를 바꿔야 한다. Settings > Pages > SourceDeploy from a branch로 되어 있으면, 워크플로가 아무리 초록불이어도 Pages는 여전히 브랜치를 보고 있다. 즉 빌드는 성공하는데 사이트는 그대로다. 실패하는 것보다 이쪽이 더 헷갈린다.

GitHub Actions로 바꾸면 된다. UI에서 눌러도 되고, CLI로도 된다.

$ gh api -X POST repos/<user>/<repo>/pages -f build_type=workflow

삽질 기록

여기서부터가 본편이다.

node 버전이 안 맞는다

첫 배포가 바로 터졌다.

Node.js v20.20.2 is not supported by Astro!
Please upgrade Node.js to a supported version: ">=22.12.0"

withastro/actionastro 팀이 만든 공식 액션인데, 기본값이 node 20이다. 그리고 astro는 22.12 이상을 요구한다. 공식 액션의 기본값이 본체의 요구사항과 안 맞는다. 로컬에선 22를 쓰고 있어서 빌드가 잘 됐고, 그래서 CI에서만 터졌다.

위 워크플로에 node-version: 22가 들어간 이유다. 이건 액션이 언젠가 고칠 문제지만, 명시해두면 액션 기본값이 또 바뀌어도 안 흔들린다.

갑자기 블로그가 어두워졌다

Bulma를 0.9.3에서 1.0.4로 올렸더니 블로그가 시커멓게 변했다. CSS는 한 줄도 안 건드렸는데.

Bulma는 1.0부터 prefers-color-scheme을 자동으로 따라간다. 0.9까지는 그런 개념이 없어서 항상 흰 바탕이었다. 즉 버그가 아니라 새 기능이고, OS를 다크모드로 쓰는 사람에게만 보인다. 나는 맥을 다크모드로 쓰고 있었으니 나한테만 시커멓게 보였던 것이다.

메이저 버전을 올린다는 건 내가 아무것도 안 해도 기본 동작이 바뀔 수 있다는 뜻이라는 걸 새삼 확인했다.

원래대로 흰 바탕을 고정하려면 이렇게 하면 된다.

<html data-theme="light">

애드센스 광고가 사라졌다

이게 제일 재미있었다.

언젠가부터 광고가 안 나오고 있었다. 당연히 코드부터 의심했다. 그런데 파보니 전부 멀쩡했다. 로더 스크립트도 붙고, <ins> 태그도 제자리에 있고, push()도 호출되고, ads.txt도 200으로 응답했다. 요청이 구글까지 갔는데 구글이 data-ad-status="unfilled", 즉 “줄 광고가 없다”고 답하고 있었다.

여기서 방향을 틀었다. 요청이 가는데 거절당한다면 문제는 코드가 아니라 계정 쪽이다. 애드센스 콘솔을 열어봤다. 사이트 목록에 등록된 도메인이 blog.seotory.com이 아니라 **seotory.com**이었다. 그리고 상태가 이랬다.

승인 상태: 주의 필요
상태 세부정보: 사이트 소유권을 확인할 수 없음, 사이트가 다운되었거나 사용할 수 없음
Ads.txt 상태: 찾을 수 없음

루트 도메인이 죽어 있었다. DNS를 확인해보니 이랬다.

$ dig +short A seotory.com
192.30.252.153      # 폐기된 옛날 GitHub Pages IP

GitHub Pages가 몇 년 전에 IP 대역을 바꿨는데, 나는 blog. 서브도메인만 옮기고 apex는 방치했던 것이다. http로는 404가 뜨고 https는 아예 연결이 안 됐다. 그동안 apex로 들어오는 사람이 없어서 몰랐다.

문제는 이게 블로그까지 끌고 들어갔다는 점이다. 애드센스는 등록된 seotory.com을 확인하러 갔다가 사이트가 죽어 있으니 소유권 확인에 실패했고, 도메인 전체(하위 blog. 포함)에 광고를 끊었다. 블로그 코드는 처음부터 아무 잘못이 없었다.

그럼 blog.seotory.com을 새로 등록하면 되지 않나 싶었는데, 안 된다.

URL은 올바른 최상위 도메인이어야 합니다.

애드센스는 apex 도메인만 등록할 수 있다. 서브도메인은 apex에 딸려가는 구조다. 그러니 seotory.com을 살리는 것 외엔 방법이 없었다.

여기서 잠깐 막혔다. apex를 살리자고 블로그를 통째로 옮길 수도 없고, 리다이렉트 하나 때문에 서버를 띄우는 것도 우스웠다. 다행히 DNS가 cloudflare에 있어서 호스팅 없이 해결됐다.

  1. apex A 레코드를 192.0.2.1로 바꾸고 프록시를 켠다. 이 IP는 문서용으로 예약돼서 실제로 라우팅되지 않는데, 프록시를 켜면 cloudflare가 앞에서 다 처리하므로 뒤가 없어도 상관없다. 요컨대 없는 서버를 가리키는 척하는 A 레코드다.
  2. Page Rulesseotory.com/*https://blog.seotory.com/$1, 301

여기서 $1이 핵심이다. 이게 없으면 모든 요청이 홈으로 가버려서 seotory.com/ads.txt가 블로그의 ads.txt에 도달하지 못한다. 그럼 애드센스는 여전히 ads.txt를 못 찾는다. ads.txt 규격은 리다이렉트를 허용하므로, 경로만 보존되면 구글이 따라와서 읽어준다.

$ curl -sI https://seotory.com/ads.txt | grep -i location
location: https://blog.seotory.com/ads.txt

이렇게 해두고 애드센스에서 Ads.txt 스니펫 방식으로 확인을 눌렀더니 **Ads.txt 상태: 승인됨**으로 바뀌었다. 결국 블로그 코드는 한 줄도 안 고쳤다.

교훈이라면, 광고가 안 나올 때 코드를 먼저 뒤지면 안 된다는 것이다. 브라우저가 요청을 보내고 응답을 받고 있다면 코드는 이미 무죄다.

Tailwind Typography가 백틱을 붙인다

마크다운 본문은 @tailwindcss/typographyprose 클래스가 처리한다. <h2>, <p>, <ul> 하나하나에 스타일을 주는 대신 부모에 prose 하나만 붙이면 되니 편한데, 기본값이 좀 이상하다.

첫째, 인라인 코드에 백틱을 붙인다.

화면에 `array func`처럼 백틱이 그대로 보이길래 마크다운이 깨진 줄 알았다. 파서를 의심하면서 HTML을 열어봤는데 멀쩡한 <code>array func</code>였다. 범인은 prose였다. CSS로 백틱을 그려넣고 있었던 것이다.

.prose :where(code)::before { content: "`" }
.prose :where(code)::after  { content: "`" }

의도는 알겠다. 인라인 코드를 배경색 없이도 구분되게 하려는 것이다. 그런데 나는 배경색을 줬으니 백틱은 중복이다. 끄면 된다.

:where(code)::before,
:where(code)::after { content: none; }

둘째, 코드블록이 어둡다고 가정한다.

prose의 기본 코드블록은 어두운 배경에 밝은 글자다. 나는 사이트가 밝으니 배경만 밝게 바꿨다. 그랬더니 언어를 지정하지 않은 코드블록만 글자가 밝은 채로 남아서, 흰 배경에 흰 글자가 됐다.

언어를 지정한 블록은 멀쩡했다. shiki가 인라인 스타일로 색을 박아넣으니 prose의 글자색이 밀렸기 때문이다. 그래서 **“이 블록만 이상하다”**는 현상이 나온다. 아까 하이라이팅에서 말한 그 차이가 여기서 터진 것이다.

배경과 글자색을 같이 바꿔주면 된다. prose가 변수로 열어놨다.

--tw-prose-pre-bg: #f6f8fa;
--tw-prose-pre-code: #24292e;

그리고 여기서 한 번 더 밟았다. 이 변수를 .prose-blog에 넣었더니 이번엔 모든 코드블록이 어두워졌다. 고치려다 더 망가진 것이다.

특이도 때문이었다. 플러그인의 규칙은 .prose(0,1,0)이고 내 .prose-blog도 (0,1,0)이다. 같은 값이면 나중에 선언된 쪽이 이기는데, 빌드 순서상 플러그인이 뒤였다. 클래스를 하나 더 붙여서 (0,2,0)으로 올리니 해결됐다.

.prose.prose-blog { ... }

유틸리티 프레임워크를 쓰면 특이도 싸움에서 해방된다고들 하는데, 플러그인이 끼면 그렇지도 않다.

리다이렉트가 카테고리를 가로챈다

카테고리를 dev 하나로 합치면서 옛 URL 19개가 사라지게 됐다. 검색엔진에 색인된 링크들이라 404로 버릴 수는 없어서, astroredirects로 넘겼다.

redirects: {
  '/categories/dev/java': '/categories/dev/',
  // ...
}

여기 함정이 있다. 나중에 dev/java다시 살리면, 리다이렉트가 실제 카테고리 페이지를 조용히 가로챈다. 경고도 에러도 없다. 실험 삼아 되살려보니 정말 글 목록 대신 리다이렉트 스텁이 나왔다. 위에서 “2뎁스는 언제든 다시 생길 수 있다”고 정해놨으니, 이건 언젠가 반드시 밟을 지뢰였다.

그래서 리다이렉트 목록을 손으로 관리하는 대신, 현재 살아있는 카테고리는 자동으로 빠지게 만들었다.

const categoryRedirects = Object.fromEntries(
  RETIRED_CATEGORY_PATHS
    .filter((p) => !CATEGORY_PATHS.includes(p))   // 부활하면 알아서 빠진다
    .map((p) => [`/categories/${p}`, '/categories/dev/']),
);

CATEGORY_PATHS는 메뉴 정의에서 뽑아낸 것이라, 카테고리를 되살리는 순간 리다이렉트에서 저절로 빠진다. 미래의 내가 이걸 기억할 거라고 믿지 않는 편이 낫다.

참고로 GitHub Pages는 정적 호스팅이라 진짜 301을 못 낸다. HTTP 헤더를 만들려면 서버가 있어야 하는데 없기 때문이다. astroredirects는 대신 meta refreshcanonical이 든 HTML을 찍어준다. 브라우저는 잘 따라가고 검색엔진도 canonical을 존중하지만, 엄밀히 301은 아니다. 제대로 된 301이 필요하면 애드센스 때처럼 cloudflare에서 처리해야 한다.

한글 폰트가 지정되어 있지 않았다

이건 jekyll 시절부터 있던 문제인데 이제야 발견했다.

Bulma의 기본 폰트 스택은 이렇다.

Inter, "SF Pro", "Segoe UI", Roboto, ... , sans-serif

한글 글꼴이 하나도 없다. 폰트 스택은 글자 하나하나마다 앞에서부터 훑어서 그 글자를 가진 첫 폰트를 쓰는 방식이다. 그러니 한글은 목록을 전부 통과해서 맨 끝 sans-serif로 떨어지고, 결국 OS가 알아서 고른 폰트로 렌더된다. 맥은 Apple SD Gothic Neo, 윈도우는 맑은 고딕.

영문은 Inter, 한글은 딴 폰트로 나오고 있었다. 그것도 보는 사람마다 다르게. 글의 90%가 한글인 블로그에서 몇 년째 이러고 있었다. 눈치채지 못한 이유는 간단하다. 한글이 안 깨지고 잘 보였기 때문이다. 폰트가 없으면 두부(□)라도 뜰 텐데, 폴백은 조용히 잘 동작한다.

_sass를 열어보니 더 웃겼다. NanumBarunGothic@font-face로 정의는 해뒀는데 어디에도 적용하지 않았다. font-family에 넣는 걸 잊은 것이다. 폰트 파일만 몇 년째 저장소에 얹고 다녔다.

Pretendard로 해결했다. 라틴과 한글을 한 폰트로 덮어서 섞일 일이 없고, 라틴 메트릭이 Inter와 호환이라 영문 느낌도 크게 안 바뀐다.

--font-sans: 'Pretendard Variable', Pretendard, -apple-system, ...;

한글 폰트는 글자 수가 많아서 파일이 무거운 게 문제인데, dynamic subset 빌드를 쓰면 유니코드 영역을 90여 개로 쪼개서 실제로 쓴 글자가 든 조각만 내려받는다.

결과

프레임워크 jekyll 4.2.2 (ruby) astro 7 (node)
CSS Bulma CDN (전체) tailwind 37KB / gzip 7KB
JS jQuery 3.1.1 없음
아이콘 FontAwesome 킷 인라인 SVG
코드 하이라이팅 rouge shiki
배포 Pages 자동 빌드 GitHub Actions
애널리틱스 UA (죽어있음) GA4
한글 폰트 미지정 Pretendard

URL은 전부 그대로 유지했다. /post/:title, /list/N/, /categories/, /feed/까지. 색인된 링크를 하나도 안 버렸다는 게 개인적으로 제일 만족스럽다.

옮기고 나서 든 생각은, astro가 불친절한 게 아니라 정직하다는 것이었다. jekyll은 카테고리도 URL도 알아서 해줬다. 편했다. 그런데 그 편함의 대가로, 오타 난 카테고리가 조용히 생겨도 몰랐고, 폰트가 적용 안 된 것도 몇 년을 몰랐고, 애널리틱스가 죽은 것도 3년을 몰랐다. 알아서 해준다는 건 틀려도 안 알려준다는 뜻이다. astro는 스키마에 적은 대로만 동작하고, 틀리면 빌드가 깨진다. 귀찮지만 이쪽이 낫다.

물론 삽질의 절반은 프레임워크와 무관했다. 죽어있던 apex 도메인이라든가, 정의만 하고 안 쓴 폰트라든가. 이건 astro가 고쳐준 게 아니라, 오랜만에 열어봤기 때문에 발견한 것이다.

아직 jekyll 블로그를 방치하고 있다면, 한 번 열어보자. 생각보다 많은 게 죽어있을 것이다.

Tags :astrojekyll

같은 카테고리의 다른 글

zsh, zplug과 oh-my-zsh 시작하기

최종 모습 zsh을 사용하게 된 계기 bash sh을 아주 잘쓰고 있었음 실수로 rm -rf 를 입력 혼돈의 카오스 닷으로 시작하는 설정 파일들이 날라감 (.bash 등등) 이렇게 된 이상 그 좋다던...

더보기

자바스크립트 화살표 함수 (array function)

array func 은 아래와 같은 간단한 형태를 보면 매우 간단하나, 복잡한 구문을 보면 매우 어렵게 느껴진다. 쉬운 array func 부터 복잡한 array func 까지 절차적으로 밟아보자. array func array func의...

더보기

java proxy pattern (프록시 패턴)

프록시는 실제로 액션을 취하는 객체를 대신해서 대리자 역할을 해준다. 프록시 패턴을 사용하게 되면 프록시 단계에서 권한을 부여할 수 있는 이점이 생기고 필요에 따라 객체를 생성시키거나 사용하기 때문에 메모리를 절약할 수...

더보기

java composite pattern (컴포지트 패턴)

컴포지트 패턴이란 클래스의 구조적 디자인 패턴으로 단일 객체와 복합 객체를 동일하게 컨트롤 할 수 있게끔 도와주는 패턴이다. 컴포지트 패턴은 아래와 같이 3가지의 요소에 의해 이루어진다. base component base component는 composition(구성자)을...

더보기