이 블로그는 원래 Chirpy 테마로 시작했다. 사이드바, 다크모드, PWA, 검색, 카테고리·태그 아카이브까지 웬만한 건 다 들어 있는 잘 만든 테마다. 그런데 얼마 못 가 Type Theme라는, 사이드바도 없는 단일 컬럼 미니멀 테마로 갈아탔다.

교체 커밋과 CLAUDE.md 변경 이력에 나는 이유를 이렇게 적어뒀다.

Chirpy 저장소는 archived 상태라 유지보수 대신 교체 선택.

이 글은 그 한 줄이 틀렸다는 걸 나중에 알게 된 이야기다. 그 착오 덕분에 “왜 옮겼는가”를 정직하게 다시 쓰면 무엇이 남는지를 짚어본 이야기이기도 하다.

적어둔 이유가 사실이 아니었다

테마를 옮기고 한참 뒤, 이 결정을 글로 정리하려고 저장소 상태를 다시 확인했다. 그런데 방향이 뒤집혀 있었다.

  • Chirpy는 archived가 아니었다. 이 글을 쓰는 2026년 7월 시점에도 릴리스가 계속 나오고 이슈가 활발하게 오가는, 여전히 살아 있는 프로젝트였다.
  • 정작 archived인 쪽은 도착지 Type Theme였다. 저장소 상단에 “This repository was archived by the owner on Jul 26, 2025. It is now read-only.”라고 붙어 있다.

즉 나는 “소스가 죽었으니 옮긴다”고 적어놓고, 실제로는 살아 있는 테마를 떠나 멈춘 테마로 이사했다. 교체의 근거였던 “archived”라는 사실은 떠나온 곳이 아니라 도착한 곳에 붙는 라벨이었다.

이게 이 글에서 제일 하고 싶은 이야기다. 결정 자체가 틀렸다는 게 아니라 — 뒤에서 보겠지만 옮긴 건 여러모로 잘한 일이었다 — 결정을 정당화하려고 갖다 붙인 “검증 가능한 사실”을 검증하지 않고 적었다는 것. changelog에 “저장소가 archived다” 같은 문장을 쓸 때, 그건 취향이나 판단이 아니라 30초면 확인되는 팩트다. 그 30초를 건너뛰면, 미래의 내가 그 기록을 근거로 삼았다가 이렇게 뒤집힌다.

역설적이게도 이 교훈을 가장 확실하게 가르쳐준 건 이 글을 쓰기 위한 리서치 그 자체였다.

그럼 진짜 왜 옮겼나

가짜 이유를 걷어내고 나면 진짜 동인이 남는다. 그건 “archived”보다 훨씬 정직하고, 훨씬 설득력 있다.

이 블로그의 컨셉은 인사이트다. 절차를 나열하는 튜토리얼이 아니라 “왜 그렇게 판단했는지, 무엇을 배웠는지”를 쓰는 공간. 이 컨셉은 테마를 옮기기 전부터 이미 정해져 있었다. 그런 글에 Chirpy가 제공하는 기능 대부분은 그냥 안 쓰는 무게였다.

교체 커밋 하나의 순증감이 이걸 그대로 보여준다.

+136 / -706 lines

무엇이 사라졌는지가 더 흥미롭다.

  • _config.yml237줄 → 72줄로 줄었다. PWA, comments, analytics, webmaster verification, collections/defaults 같은 Chirpy 전용 블록이 통째로 빠졌다.
  • .devcontainer/, .vscode/, tools/run.sh, tools/test.sh 같은 스캐폴딩이 사라졌다.
  • 정적 자산을 끌어오던 git submodule(chirpy-static-assets)도 제거했다.

여기서 배운 건 조금 반직관적이다. 기능이 많은 걸 버리는 것 자체가 이득이었다. Chirpy가 나빠서가 아니다. 인사이트 블로그라는 목적에는 그 기능들이 대부분 불필요했고, 안 쓰는 기능은 유지보수 면적으로만 남는다. “더 많은 걸 주는 도구”가 항상 더 좋은 선택은 아니라는, 알면서도 자주 잊는 사실을 700줄 삭제로 다시 확인했다.

“테마 교체”인 줄 알았는데 “배포 모델 교체”였다

가장 아팠던 지점은 여기다. 나는 이 작업을 “파일 몇 개 바꾸는 테마 교체”로 생각했는데, 실제로는 배포 파이프라인을 바꾸는 일이었다.

Chirpy는 GitHub Actions로 사이트를 직접 빌드해서 배포했다. 워크플로가 jekyll build_site를 만들고, htmlproofer로 검사한 뒤, 그 결과물을 Pages에 올리는 구조다. Type Theme로 옮기면서 나는 이 방식을 버리고 GitHub Pages 네이티브 빌드(Pages가 알아서 Jekyll을 돌리는 방식)로 전환했다. Actions로 직접 빌드할 이유가 없어졌으니까.

문제는 저장소에 .nojekyll 파일이 그대로 남아 있었다는 것이다.

같은 파일인데 두 배포 모델에서 의미가 정반대다.

  • Actions로 직접 빌드할 때: 워크플로가 이미 _site를 완성해서 넘긴다. 그러니 Pages한테 “여기서 Jekyll 또 돌리지 말고 준 대로 서빙해”라고 말하는 .nojekyll맞는 신호다.
  • 네이티브 빌드로 바뀐 뒤: 이제 Jekyll을 돌려줄 주체가 Pages 자신이다. 그런데 .nojekyll이 “Jekyll 돌리지 마”라고 막아버리니, 소스가 처리되지 않은 채 그대로 노출됐다. 사이트가 깨졌다.

결국 별도 hotfix 커밋으로 정리했다.

fix: remove .nojekyll so GitHub Pages legacy build runs Jekyll

교훈은 파일 한 줄보다 크다. 배포 모델을 바꾸면, 이전 모델에서는 옳았던 설정이 새 모델에서는 지뢰가 된다. .nojekyll은 버그가 아니라 “예전 문맥에서는 정답이었던 것”이다. 파이프라인을 갈아탈 때 위험한 건 새로 추가하는 것보다 이전 파이프라인이 남기고 간 전제들이다. 그것들은 조용히 남아 있다가, 문맥이 바뀐 순간 반대로 작동한다.

멈춘 테마를 신뢰하는 법

앞에서 “archived 테마로 이사했다”는 게 실수처럼 들렸을 수 있다. 하지만 archived라는 사실을 알고 나면, 오히려 그걸 안전하게 다루는 방법이 명확해진다.

원격 테마(remote_theme)를 브랜치 이름으로 추적하면, 그쪽이 언제 바뀌든 내 사이트가 따라 흔들린다. 그래서 재현 가능한 빌드를 위해 세 겹으로 고정했다.

# _config.yml — 테마를 커밋 SHA로 못 박음 (브랜치 추적 X)
remote_theme: rohanchandra/type-theme@c6ec5a69ff7dfe2df193be08515193c72bd4a55d
# Gemfile — github-pages gem 버전 고정
gem "github-pages", "232", group: :jekyll_plugins

여기에 더해 Gemfile.lock.gitignore에서 빼고 커밋에 포함시켰다. 테마 SHA, gem 버전, lockfile — 세 개를 동시에 잠근 것이다.

흥미로운 건, archived라는 사실이 이 결정을 오히려 정당화한다는 점이다. 움직이지 않는 테마이기 때문에 특정 커밋에 묶어도 뒤에서 갱신돼 깨질 일이 없다. “죽은 프로젝트에 의존한다”는 리스크를, “죽었으니 절대 안 움직인다”는 안정성으로 뒤집은 셈이다. (다만 이건 보안 패치도 안 온다는 뜻이기도 하다. 원격 테마에 스크립트가 많았다면 다시 고민했을 것이다.)

포크가 아니라 최소 오버라이드

원격 테마의 몇몇 기본값은 내 상황에 안 맞아서 손을 봐야 했다. 선택지는 둘이었다.

  1. 테마 전체를 포크해서 내 저장소로 들여온다 → 이제 테마 전체가 내 유지보수 대상이 된다.
  2. 영향받는 템플릿만 오버라이드한다 → 유지보수 면적은 작지만, 나중에 핀을 올릴 때 오버라이드를 수동으로 동기화해야 한다.

2번을 택했다. 실제로 오버라이드한 건 head.html, header.html, default.html, post.html, page.html, main.scss 정도다. 그리고 이 트레이드오프의 대가(동기화 부담)를 잊지 않으려고 커밋 메시지에 지시문으로 박아뒀다.

Keep theme overrides synchronized if the pinned Type Theme commit is intentionally upgraded.

정답이 있는 선택은 아니었다. 다만 “작은 유지보수 면적 + 명시된 동기화 리스크”가, “포크로 얻는 자유 + 무한 유지보수 면적”보다 이 블로그 규모엔 맞다고 판단했다. 중요한 건 트레이드오프를 고른 것보다, 고른 뒤에 그 대가를 기록으로 남긴 것이다.

/blog라는 접두어가 만든 버그들

이 블로그는 user page(user.github.io)가 아니라 project page(user.github.io/blog)다. 즉 baseurl/blog다. 그런데 원본 테마의 URL 처리 기본값들은 대체로 user page(baseurl 없음)를 가정하고 있었다. 그 간극에서 같은 종류의 버그가 세 번 터졌다. 전부 “/blog가 한 번 더 붙는” 문제였다.

1. 페이지네이션 경로 중복

# baseurl이 이미 /blog인데 경로에 blog/를 또 넣어 /blog/blog/page2/가 됨
paginate_path: "blog/page:num"   # 문제
paginate_path: "/page:num/"      # 수정

2. 검색 결과 URL의 이중 슬래시

<!-- baseurl + 앞에 /가 붙은 post.url = /blog//... -->
"{{ site.baseurl }}/{{ post.url }}"   <!-- 문제 -->
"{{ post.url | relative_url }}"       <!-- 수정 -->

3. 빈 feature image가 홈페이지를 배경으로 요청

feature 이미지가 없는 글에서, 원본 테마는 background-image: url('/blog/')를 깔았다. 빈 값에 baseurl만 남아 사이트 홈페이지 자체를 배경 이미지로 불러오는 요청이 나간 것이다. page.feature-img가 있을 때만 배경을 지정하도록 오버라이드해서 막았다.

세 버그의 해법이 전부 같은 방향이라는 게 핵심이다. baseurl을 문자열로 손수 이어 붙이지 말고, relative_url 같은 Jekyll 필터나 명시적 조건에 맡기는 것. 문자열 연결로 URL을 만들면 “붙는다/안 붙는다”의 경계에서 반드시 사고가 난다. 이 버그 클래스는 user page에서는 절대 안 보인다. baseurl이 빈 문자열이라 이중으로 붙어도 티가 안 나니까. 테마를 가져다 쓸 때 그 테마가 어떤 배포 형태를 가정하는지는, 문서가 아니라 이렇게 URL이 깨지고 나서야 드러난다.

발행 전에, 신뢰성부터

여기서 한 판단이 스스로도 의외였다. 글을 한 편도 쓰기 전에 계약 테스트와 CI부터 깔았다.

포스트 6개짜리 픽스처를 만들어 실제로 jekyll build를 돌리고, 생성된 결과물을 단언으로 검증하는 테스트다. 위에서 고친 버그들이 회귀하지 않는지를 실제 빌드 산출물에 대고 확인한다.

# test/site_output_test.rb — 생성된 HTML을 직접 단언
refute_match(%r{"url": "/blog//}, search)          # 이중 슬래시 재발 방지
refute_includes about, "background-image: url('/blog/')"  # 홈 배경 요청 방지
assert_includes search, 'integrity="sha384-'       # 검색 CDN 스크립트 SRI
assert_includes search, 'aria-label="검색"'         # 접근성

그리고 이걸 CI로 강제했다. push/PR마다 Ruby 3.4 환경에서 이 테스트가 돈다. 여기서 GitHub Actions의 역할이 바뀌었다는 게 재밌다. 예전엔 Actions가 “배포”를 담당했는데, 이제 배포는 Pages 네이티브가 맡고 Actions는 “빌드 계약을 먼저 검증”하는 게이트가 됐다. 같은 도구, 다른 임무.

콘텐츠가 0개인데 테스트부터 까는 게 과해 보일 수 있다. 하지만 인사이트 블로그의 신뢰는 글 내용에서만 오는 게 아니다. 독자가 링크를 눌렀을 때 사이트가 안 깨지는 것, 페이지네이션이 정상인 것, 검색이 되는 것 — 이 기반이 무너지면 글이 아무리 좋아도 신뢰가 안 쌓인다. .nojekyll 하나로 사이트가 통째로 깨졌던 경험이 이 판단을 밀어줬다.

곁다리로 하나 더 드러났다. 테마를 교체하며 빌드 exclude 목록을 정리하다가, CLAUDE.md가 정적 파일로 그대로 서빙되고 있었다는 걸 발견했다. 프론트매터 없는 최상위 .md는 Jekyll이 그냥 복사해서 공개 페이지로 노출한다. exclude에 추가해서 막았지만, 교훈은 그대로다. 빌드 방식이 바뀌면 “무엇이 공개되는가”의 규칙도 같이 바뀐다. 테마 교체는 렌더링만 바꾸는 게 아니라, 노출 경계도 다시 그린다.

그래서 무엇을 배웠나

테마 하나 옮긴 작업치고 배운 게 많았는데, 정리하면 결국 하나로 모인다.

“왜”를 기록할 때, 그 안에 섞인 “검증 가능한 사실”은 그 자리에서 검증하라. “단일 컬럼이 컨셉에 맞아서”는 판단이라 틀릴 수 없지만, “저장소가 archived라서”는 팩트라서 틀릴 수 있다. 나는 후자를 확인 없이 적었고, 그 한 줄이 이 글 전체를 다시 쓰게 만들었다.

나머지 교훈들도 결이 같다.

  • 테마 교체는 대개 배포 모델 교체다. 이전 모델이 남긴 전제(.nojekyll)가 새 문맥에서 반대로 작동한다.
  • 멈춘 의존성은 핀으로 고정하면 오히려 안정적이다. archived가 리스크이자 동시에 안정성의 근거가 된다.
  • URL은 문자열로 잇지 말고 필터에 맡겨라. project page의 /blog 접두어가 그걸 세 번 가르쳐줬다.

다시 한다면 딱 하나를 바꾸겠다. 결정을 changelog에 적는 순간, “이 문장은 판단인가 사실인가”를 한 번 자문하는 것. 사실이라면, 적기 전에 확인하는 30초. 그 30초가 없어서 이 글이 태어났다는 게, 조금 웃기면서도 이 블로그의 컨셉에 가장 잘 맞는 결말인 것 같다.