이 블로그는 원래 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.yml이 237줄 → 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라는 사실이 이 결정을 오히려 정당화한다는 점이다. 움직이지 않는 테마이기 때문에 특정 커밋에 묶어도 뒤에서 갱신돼 깨질 일이 없다. “죽은 프로젝트에 의존한다”는 리스크를, “죽었으니 절대 안 움직인다”는 안정성으로 뒤집은 셈이다. (다만 이건 보안 패치도 안 온다는 뜻이기도 하다. 원격 테마에 스크립트가 많았다면 다시 고민했을 것이다.)
포크가 아니라 최소 오버라이드
원격 테마의 몇몇 기본값은 내 상황에 안 맞아서 손을 봐야 했다. 선택지는 둘이었다.
- 테마 전체를 포크해서 내 저장소로 들여온다 → 이제 테마 전체가 내 유지보수 대상이 된다.
- 영향받는 템플릿만 오버라이드한다 → 유지보수 면적은 작지만, 나중에 핀을 올릴 때 오버라이드를 수동으로 동기화해야 한다.
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초가 없어서 이 글이 태어났다는 게, 조금 웃기면서도 이 블로그의 컨셉에 가장 잘 맞는 결말인 것 같다.