0523

회고

Tistory에서 개인 블로그(kwangilkim.com)로 이전하게 된 이야기

| 서론

안녕하세요, 팡일입니다.

어느덧 기술 블로그를 운영해온 지 약 3년이 되어갑니다. 23년도에 처음으로 velog를 시작했고, 24년도에 velog를 본격적으로 운영해왔다가, 25년도에 tistory로 넘어오게 됐죠. velog에서 tistory로 이전하게 됐던 이야기는 아래의 포스팅을 참고해주세요!

https://buly.kr/EI6T32C

tistory의 장점들을 정말 많이 이용해왔지만, 약 20개월 정도 사용하고 운영해오다가 결국에는 개인 블로그를 만드는 방향으로 나아가게 됐습니다. 오늘은 왜 개인 블로그를 만들게 되었는지, 그리고 어떻게 만들고 이전했는지를 공유해보려고 합니다.

그리고 이 글을 통해서 기존 pangil-log라는 이름으로 운영된 tistory에 많은 발걸음과 관심을 가져주신 분들께 감사의 말을 전해 드립니다.

| 근본적인 이유

LinkedIn, Github 등을 보면 참 다양한 개발자들이 본인의 기술 블로그를 운영하는 것을 볼 수 있습니다. velog, tistory, 네이버 블로그 등 다양한 플랫폼을 사용하고 있는데요. 이미 인프라가 구성되어 있는 플랫폼을 통해서 계정 생성만 하면 포스팅을 바로 작성할 수 있다는 점, 작성된 글이 SEO에 최적화되어 검색 시 유입률을 높일 수 있다는 점, 구현된 통계 기능을 통해서 보다 운영을 용이하게 할 수 있다는 장점들이 결국 플랫폼 블로그를 사용하게 해주는 것 같습니다. 저도 그 장점들을 많이 이용해오고 누려왔었죠. 그러면서 문득 그런 생각이 들더군요.

만약 해당 플랫폼이 중지하게 된다면, 나는 어떤 플랫폼을 써야 하지?

지난 4월 28일에 충격적인 일이 있었죠. 바로 국내 대표 알고리즘 학습 플랫폼인 백준이 서버를 종료하게 된 소식입니다. 프로그래머스, 비코, Codecademy와 같은 플랫폼들이 있지만, 그래도 백준은 이 중에서 제일 큰 입지를 가지고 있는 플랫폼이기에 영원히 갈 거라 생각했습니다만, 지난 4월 말 서버 종료라는 소식을 전하게 됐습니다.

그런데 곰곰이 생각해보면, 이건 백준만의 이야기가 아닙니다. 블로그 세계에서는 이미 여러 번 반복된 역사거든요.

  • 다음 블로그 종료 (2022년 9월) — 15년 넘게 운영되던 다음 블로그가 문을 닫으면서, 수많은 블로거들이 티스토리로 "이전당해야" 했습니다. 본인의 의지와 무관하게요.

  • 이글루스 종료 (2023년 6월) — 무려 20년 가까이 운영된 플랫폼이었습니다. 백업 기간 안에 글을 내려받지 못한 블로거들의 기록은 그대로 사라졌습니다.

  • 카카오 데이터센터 화재 (2022년 10월) — 서비스 종료까지는 아니었지만, 티스토리를 포함한 카카오 서비스 전체가 며칠간 마비되면서 "내 글이 내 손에 없다"는 사실을 많은 사람들이 체감한 사건이었죠.

  • Tistory Open API 종료 (2024년 2월) — 외부 도구로 티스토리를 자동화해서 운영하던 사람들의 워크플로우가 하루아침에 막혔습니다.

그리고 사실, 저는 이걸 남의 일로만 배운 게 아닙니다. 이번 이관 작업 중에 발견한 건데, velog에서 tistory로 넘어올 때 옮긴 글 65편 안에, 이미지 618개가 여전히 velog CDN 주소를 바라보고 있었습니다. 글은 옮겼지만 이미지는 velog 서버에 얹혀살고 있었던 거죠. velog가 사라지는 날, 제 티스토리 글 65편의 이미지가 함께 깨질 운명이었습니다. "플랫폼 종속"이라는 게 이렇게 눈에 안 보이는 곳까지 스며들어 있더군요.

이런 사건들을 하나씩 되짚어보면서 "내가 쓰고 있는 서비스가 언젠가는 종료될 수 있겠네?"라는 생각이 막연한 불안이 아니라 꽤 현실적인 시나리오라는 걸 인정하게 됐습니다. 그리고 예전부터 FE 개발자로서 본인의 기술 블로그를 직접 개발하고 운영하고 싶었던 마음이 서서히 올라오곤 했습니다.

4월에 취업을 하게 되면서 기술 블로그를 이전만큼 운영 및 관리하지 못하게 된 상황도 있었고, tistory를 사용하면서 개인적으로 느꼈던 아쉬운 점들이 쌓이면서, 이제는 개인 블로그화를 해야겠다는 생각이 점점 커지게 됐습니다.

결국 핵심은 하나였습니다. 내 글의 생명 주기를 플랫폼이 아니라 내가 결정하고 싶다는 것이었죠. 3년 동안 쌓인 1,000개가 넘는 글은 이제 단순한 기록이 아니라 저의 자산인데, 그 자산의 열쇠를 남이 쥐고 있다는 게 점점 불편해졌습니다.

| Tistory를 쓰면서 아쉬웠던 점들

tistory 자체는 좋은 플랫폼입니다. 다만 20개월을 운영하면서 개인적으로 아쉬웠던 지점들이 있었습니다.

첫째, 데이터의 소유권이 온전히 제게 있지 않다는 점입니다. 백업 기능이 있긴 하지만, 결국 글이 살아있는 곳은 카카오의 서버입니다. 그리고 나중에 이관을 하면서 알게 된 사실인데, 그 백업조차 완전하지 않았습니다. 티스토리 내보내기는 이모지 같은 4바이트 문자를 ?로 깨뜨려서 내보냅니다. 카테고리 이름에 붙여둔 이모지들이 전부 물음표가 되어 돌아왔죠. 내 글의 온전한 사본조차 내 마음대로 받을 수 없다는 것을 그때 실감했습니다.

둘째, 커스터마이징의 한계입니다. 스킨 편집으로 어느 정도 손을 댈 수는 있지만, 결국 tistory가 허용하는 범위 안에서의 커스터마이징입니다. FE 개발자로서 "이 부분은 이렇게 만들고 싶은데"라는 순간마다 벽에 부딪히는 느낌이 있었습니다.

셋째, 글쓰기 경험을 제 방식대로 다듬을 수 없다는 점입니다. 에디터, 코드 블록 하이라이팅, 이미지 처리 방식 등 매일 쓰는 도구인데도 제가 개선할 수 있는 것이 없었습니다. 예를 들어 티스토리 에디터의 코드 언어 자동 감지는 SQL 코드를 n1ql이나 routeros 같은 엉뚱한 언어로 판정하곤 했는데, 불편함을 발견해도 할 수 있는 건 "적응"뿐이었죠.

넷째, 하나의 계정으로 여러 성격의 글을 담기 어려웠습니다. 저는 기술 글뿐 아니라 매일의 묵상(QT), 주일 설교, 찬양 묵상까지 꾸준히 기록해왔는데, 성격이 다른 영역들을 한 블로그 안에서 자연스럽게 분리해서 운영하기가 애매했습니다. 실제로 작년에는 묵상 글을 두 번째 티스토리 블로그로 분리하기까지 했었죠. 기술 글을 보러 온 독자와 묵상 글을 보러 온 독자는 서로 다른 사람들인데, 플랫폼 안에서는 이 구조를 제대로 표현할 방법이 없었습니다.

이런 아쉬움들은 개발자가 아니었다면 그냥 감수하고 넘어갔을 부분들입니다. 하지만 "내가 직접 만들 수 있는데?"라는 생각이 드는 순간부터는, 감수할 이유가 점점 사라지더군요.

| 개인 블로그, 어떻게 만들었나

kwangilkim.com은 단순한 블로그 하나가 아니라, 하나의 Next.js 앱이 세 개의 얼굴을 갖는 구조입니다.

  • 프로필 허브 (루트 도메인) — 저라는 사람을 소개하는 메인 공간

  • 개발 블로그 (dev 서브도메인) — 기존 pangil-log를 잇는 기술 블로그

  • 신앙 블로그 (faith 서브도메인) — 기존 pangpang-log를 잇는 묵상 블로그

미들웨어에서 요청의 호스트를 보고 세 공간 중 하나로 라우팅하는 방식이라, 앱은 하나지만 방문자에게는 완전히 분리된 세 개의 사이트로 보입니다. 티스토리 시절 블로그를 두 개로 쪼개면서까지 해결하려 했던 "성격이 다른 글의 분리"를, 이번에는 구조 차원에서 해결한 셈입니다.

기술 스택

  • Next.js (App Router) + TypeScript — 서버 컴포넌트 우선. 공개 페이지의 클라이언트 코드는 코드 복사 버튼, TOC 하이라이트, 다크모드 토글 같은 꼭 필요한 인터랙션으로만 제한했습니다.

  • Supabase + Prisma — 포스트는 단일 posts 테이블에 JSONB 콘텐츠로 저장하되, 저장·발행·렌더링 세 지점 모두 Zod 스키마 검증을 통과해야 합니다. 타입 없는 JSON이 코드 여기저기로 흘러다니는 걸 원천 차단했습니다.

  • Tiptap 기반 자체 에디터 — tistory의 고정 툴바 글쓰기 경험이 익숙했기에, Tiptap 위에 그 경험을 직접 구현했습니다. 코드 하이라이팅은 Shiki로 서버에서 처리합니다.

  • pg_trgm 기반 전문 검색 — 블로그 내 검색도 Postgres 확장으로 직접 구현했습니다.

  • Vercel + GitHub Actions — 배포와 자동화.

이 블로그만의 장치들

만들다 보니 "플랫폼이었다면 못 했을 것들"이 하나씩 생겼는데, 몇 가지만 소개하면:

  • 청구기호 — 모든 글은 발행 시 타입별 통산 번호를 받습니다. QT-1043, SR-0104(설교), PR-0388(찬양) 같은 식이죠. 3년 치 기록을 도서관 서가처럼 정리하고 싶다는 생각에서 나온 시스템인데, 한번 부여된 번호는 불변이라 번호 자체가 이 기록물의 연대기가 됩니다.

  • 로컬 우선 설교 에디터 — 주일 설교 묵상은 예배 중에 실시간으로 받아 적는 글입니다. 네트워크가 불안정한 환경에서 글이 날아가는 것이 최악의 시나리오라, 설교 에디터만큼은 로컬(localStorage)이 진실의 원천이고 서버는 백그라운드 백업으로 설계했습니다. 네트워크가 끊겨도 "로컬 저장됨 · 동기화 대기" 상태로 계속 쓸 수 있고, 복구가 필요하면 로컬 값과 서버 값 중 무엇을 남길지 제게 묻습니다.

  • 묵상 크롤러와 데드맨 스위치 — 매일의 QT 초안은 GitHub Actions 크롤러가 자동으로 만들어 줍니다. 그런데 "자동화가 조용히 죽어 있는 것"이 자동화의 가장 무서운 실패라서, 앱 쪽에 워치독을 따로 뒀습니다. 크롤러가 오늘 흔적을 남기지 않았으면 Slack으로 알려주죠. 재미있는 건 GitHub Actions의 스케줄 실행은 레포가 60일간 조용하면 자동으로 꺼진다는 점인데, 이런 함정까지 감지 대상입니다. 플랫폼이 멈추는 게 무서워서 만든 블로그인데, 정작 저 자신이 방치했을 때도 스스로 알려주도록 만든 겁니다.

  • 직접 만든 통계 — 방문 통계도 자체 구현했습니다. Origin 검증으로 남의 페이지에서 날아오는 가짜 비콘을 걸러내고, 매주 요약을 Slack으로 받아봅니다.

주요 아키텍처 결정들은 ADR(Architecture Decision Record)로 남겨두었는데, 이 부분은 추후 별도의 포스팅으로 자세히 다루게 된다면 공유드리겠습니다.

| 데이터 이관 — 759편의 대이동

개인 블로그를 만들기로 결심했을 때 가장 큰 산은 사실 개발이 아니라 이관이었습니다. 옮겨야 할 백업 파일이 759편이었거든요. 이 정도 규모면 "손으로 확인하면 되겠지"가 통하지 않습니다. 이관 자체가 하나의 파이프라인 개발 프로젝트가 됐습니다.

백업은 두 벌, 그리고 ID 충돌

시작부터 변수가 있었습니다. 작년에 묵상 글을 두 번째 티스토리 블로그로 분리했던 이력 때문에 백업이 두 벌이었는데, 두 백업의 원본 글 ID가 각각 1번부터 시작하더군요. 첫 백업의 134번과 두 번째 백업의 134번은 완전히 다른 글인데, ID를 이관의 멱등 키로 쓰려던 계획이 그대로 충돌했습니다. 결국 백업마다 오프셋을 부여해서 해결했는데, 실제로 dry-run에서 첫 백업의 글 판정이 두 번째 백업의 동번호 글에 잘못 씌워지는 걸 목격하고 나서야 이 문제의 심각성을 체감했습니다.

티스토리 백업 HTML의 실체

티스토리 백업은 구조화된 데이터가 아니라 글별로 렌더링된 HTML입니다. 파싱해보니 온갖 현실이 튀어나왔습니다.

  • 거의 모든 태그에 data-ke-* 속성과 인라인 스타일이 붙어 있고, 문단 안에 블록 요소가 들어가 있는 등 정리가 필요한 마크업이 가득했습니다.

  • 카테고리 이름의 이모지는 전부 ?로 깨져 있었습니다(내보내기가 4바이트 문자를 버립니다). 카테고리로 글 타입을 판별해야 했기에, 한글·영문만 남기고 비교하는 정규화 로직이 필요했습니다.

  • 코드 블록의 언어 정보가 두 갈래였습니다. 제가 직접 고른 값과 티스토리가 자동 감지한 값인데, 자동 감지는 SQL을 n1ql로 적어두는 수준이라 우선순위 규칙을 따로 세워야 했죠.

변환 방식에서 중요한 결정이 하나 있었는데, HTML을 에디터 라이브러리의 자동 변환에 맡기지 않고 직접 순회하며 변환했습니다. 자동 변환은 모르는 요소를 조용히 버리는데, 759편을 옮기면서 무엇을 잃었는지 모르는 것이 이 작업의 가장 큰 위험이었기 때문입니다. 못 옮긴 것은 전부 기록으로 남겨 리포트에서 집계되도록 했습니다.

사소해 보이지만 뜻이 있는 디테일도 있었습니다. 티스토리는 빈 문단을 줄 간격 용도로 남발해서 기본적으로는 버려야 하는데, 찬양 가사에서는 빈 줄이 절 구분입니다. 그래서 찬양 글만 빈 문단을 보존하는 옵션을 따로 뒀습니다. 3년 치 기록에는 이런 결이 곳곳에 숨어 있더군요.

velog에서 온 시한폭탄

앞서 말한 velog CDN 이미지 618개도 이 단계에서 처리했습니다. dry-run 단계에서 외부 이미지를 전부 실제로 내려받아 확보 가능한지 먼저 검증했는데 — "옮길 수 있다"는 것을 적재 당일이 아니라 지금 알아야 하기 때문입니다. 그날 velog가 죽어 있으면 그 65편은 그냥 깨지니까요. 확보된 이미지는 전부 자체 스토리지로 옮기고 본문의 경로를 치환했습니다. 이번 이관의 원칙 중 하나가 "외부 CDN을 바라보는 이미지 0개"였습니다.

사람의 판단이 필요했던 32편

카테고리가 없거나 애매한 32편은 규칙으로 판정할 수 없었습니다. 실제로 열어보니 글쓰기 서식과 진짜 글이 섞여 있었죠. 이건 하나씩 직접 보고 판정했는데, 그 판단 결과를 머릿속이 아니라 코드의 예외 표로 남겼습니다. 이관은 한 번에 끝나지 않고 여러 번 다시 돌리게 되는데, 그때마다 같은 판단을 손으로 반복하면 매번 조금씩 달라지기 때문입니다.

dry-run이 기본, 그리고 사후 검증

파이프라인 전체는 dry-run이 기본이었습니다. DB를 건드리지 않고 "무엇이 어디로 가는지, 무엇이 사람 손을 필요로 하는지"만 보여주는 리포트를 먼저 돌리고, 리포트와 실제 적재가 같은 변환 함수를 쓰도록 강제했습니다. dry-run에서 본 결과와 실제로 들어가는 것이 다르면 dry-run은 아무 의미가 없으니까요. 각 타입별 변환기는 실제 백업 HTML을 fixture로 삼은 단위 테스트로 고정했습니다.

적재 시에는 청구기호를 원본 작성일 순서로 소급 부여했습니다. 번호가 곧 이 기록물의 연대기이므로, 옮기는 순서가 아니라 쓴 순서를 따라야 했죠. 발행 기준을 넘지 못한 글은 버리지 않고 초안으로 들여보냈고, 적재가 끝난 뒤에는 별도의 검증 스크립트로 DB에 들어간 결과를 다시 확인했습니다. 750여 번의 쓰기 중 하나가 조용히 실패했는지는 DB에 물어봐야 아는 거니까요. 이관의 완료 조건은 명확했습니다: 전량 이관, 원본 작성일 보존, 깨진 이미지 0.

이 과정을 거치면서 느낀 점은, 글은 자산이고 이관은 자산 실사라는 것입니다. 플랫폼에 얹혀 있을 때는 보이지 않던 데이터의 상태 — 깨진 이모지, 남의 CDN에 있는 이미지, 서식과 뒤섞인 글 — 가 직접 옮기려고 보니 전부 드러나더군요.

| SEO 최적화

개인 블로그의 가장 큰 리스크는 SEO입니다. tistory는 다음/카카오 생태계와 검색 엔진에 이미 최적화된 인프라를 갖추고 있지만, 개인 도메인은 말 그대로 맨땅에서 시작해야 하니까요. 그래서 처음부터 SEO를 필수 요건으로 두고 다음 작업들을 진행했습니다.

메타데이터와 렌더링

Next.js App Router의 Metadata API로 글마다 title, description, Open Graph를 동적으로 생성하고, 본문은 서버에서 렌더링되기 때문에 크롤러가 완성된 HTML을 그대로 읽습니다. SPA 기반 개인 블로그가 흔히 겪는 "검색엔진이 빈 페이지를 본다" 문제를 구조적으로 피한 셈입니다. OG 카드 이미지도 글마다 동적으로 생성합니다 — 링크가 공유되는 순간이 곧 브랜딩이라고 생각해서, 청구기호가 박힌 카드가 나가도록 했습니다.

sitemap, RSS, 그리고 검색엔진에 알리기

sitemap과 RSS는 세 공간별로 동적으로 생성합니다. 그런데 "새 글을 검색엔진에 알리는" 문제를 파보니 생각보다 지형이 복잡하더군요. 구글은 2023년에 sitemap ping 엔드포인트를 폐지해서 서치콘솔에 sitemap을 등록해두는 것 외에 자동화할 것이 없고, 네이버의 수집 요청 API는 제휴 승인이 필요해서 개인 블로그는 키를 받을 수 없습니다. 다행히 네이버가 2023년 7월부터 IndexNow를 지원하기 시작해서, 이 규격 하나로 네이버·Bing 등을 함께 커버하도록 했습니다. 글이 발행되면 IndexNow로 자동 제출됩니다.

구조화된 데이터

각 포스트에 JSON-LD를 심었습니다. 개인 블로그이므로 발행 주체를 조직이 아니라 사람(Person)으로 잡았는데, 검색 결과에 글쓴이와 날짜가 제대로 붙는 것을 노린 작업입니다.

기존 유입 지키기

사실 SEO에서 제일 두려웠던 건 새 도메인의 랭킹이 아니라, tistory로 들어오던 기존 유입을 잃는 것이었습니다. tistory는 커스텀 리다이렉트를 지원하지 않기 때문에 완벽한 301 이전은 어렵지만, 유입이 많은 주요 글들에는 새 블로그로의 안내를 남겨 이전 링크를 타고 온 분들이 길을 잃지 않도록 했습니다.

새 도메인이 검색 랭킹을 회복하는 데는 시간이 걸린다는 걸 알고 시작했습니다. 단기적으로는 유입이 줄겠지만, 장기적으로는 도메인 자체가 제 자산이 되기에 감수할 만한 트레이드오프라고 판단했습니다.

| 앞으로의 계획

이전은 끝이 아니라 시작입니다. 사실 이 프로젝트에는 성공 판정 기준을 하나 정해뒀습니다. "이주 후 90일간 티스토리로 회귀하지 않는 것." 만드는 것보다 계속 쓰는 것이 어렵다는 걸 아니까요. 그 90일을 위해 다음과 같은 것들을 계획하고 있습니다.

  • 블로그를 만들면서 내렸던 기술적 의사결정들(아키텍처, 에디터 구현, 검색, 이관 파이프라인 등)을 시리즈로 포스팅

  • 통계/분석 기능 고도화 — 플랫폼이 주던 통계를 이제 제가 직접 만들어야 하니까요

  • 글쓰기 경험 개선 — 직접 만든 에디터를 쓰면서 불편한 점이 보일 때마다 바로 고칠 수 있다는 게 개인 블로그의 가장 큰 재미인 것 같습니다

| 마무리

velog에서 tistory로, 그리고 tistory에서 kwangilkim.com으로. 3년 동안 두 번의 이사를 했지만, 이번 이사는 성격이 다릅니다. 더 이상 남의 집을 옮겨 다니는 게 아니라, 제 집을 지은 것이니까요. 다음 블로그가 문을 닫아도, 이글루스가 사라져도, velog의 CDN이 멈춰도 — kwangilkim.com의 스위치는 제 손에 있습니다.

플랫폼 블로그가 나쁘다는 이야기가 아닙니다. 오히려 글쓰기를 시작하는 단계라면 플랫폼을 적극 추천합니다. 다만 글이 자산이 될 만큼 쌓였고, 그 자산을 직접 지키고 가꾸고 싶어졌다면 — 그리고 마침 그걸 만들 수 있는 개발자라면 — 개인 블로그는 충분히 도전해볼 만한 프로젝트라고 생각합니다.

pangil-log를 찾아주셨던 모든 분들께 다시 한번 감사드리며, 앞으로는 kwangilkim.com에서 뵙겠습니다.

읽어주셔서 감사합니다. 🙇