나의 티스토리 글 목록을 보면 알다싶이, 고3때 대학 입시를 위해 딥러닝을 짧게 공부했었다. 이후 대학에 와서 개발에 관심을 가진 이후로는 기록의 중요성을 느끼고 블로그를 다시 쓰게 되었다
velog는 home에 추천 게시물이 있고, 최신 트렌드 및 취준 인사이트를 쉽게 얻을 수 있어서 참 좋다
또한 따로 초기 세팅 없이 기본 테마가 적용되어서 심플하고, 마크다운을 지원하기 때문에 코드 입력도 편하다
그래서 ‘글을 쓰는 것’에만 집중할 수 있는게 가장 큰 장점인 거 같다
덕분에 약 2년동안 나름 열심히 기록을 하며 벌써 9개의 시리즈를 썼다는 걸 처음 알았다
옮기는 이유
그러나 고3 때 블로그를 운영하던 당시, 방문자 통계를 보던 재미가 있었는데, velog는 방문자의 경로를 볼 수는 없고 방문자 수만 볼 수 있다는게 아쉬웠다 (근데 본인까지 카운트 됨..!)
또한 velog는 html 기반인지라 구글 서치에 등록이 안되기 때문에 블로그 노출도 되지 않는다
또한 티스토리는 테마를 마음껏 커스터마이징 할 수 있기 때문에 올해부터 천천히 마음이 기울었다
그치만 그동안 써운 게시물을 수작업으로 바꾸는 건 너무 시간이 오래 걸릴 거 같아서 고민하다가
자동화할 순 없을까?
라는 생각을 하고, 해당 글을 작성하게 되었다
전체 그림부터 그리기
자동화 코드를 작성하기 전에, 문제를 세 단계로 나눠봤다
- velog에서 내 글을 마크다운으로 뽑아온 뒤,
- 본문 속 이미지를 어딘가에 다시 올리고 링크를 바꾸고,
- 뽑아온 마크다운을 tistory에 글로 올린다
결론적으로는 여기서 가장 먼저 막힌 게 3번이었다. 티스토리 Open API가 2024년 2월에 완전히 종료됐기 때문에 글을 프로그래밍으로 발행하는 공식 경로가 사라진 셈이다. 그래서 발행은 어쩔 수 없이 Playwright를 사용했고, 사람이 에디터를 조작하는 걸 흉내 내는 방식으로 가기로 했다.
아래는 최종 계획이다
- 추출: velog GraphQL API → 글 본문(마크다운)을 그대로 가져올 수 있었다
- 이미지: velog 이미지를 내려받아 GitHub에 올리고 jsDelivr(CDN)로 서빙
- 발행: Playwright로 tistory 에디터를 열어 제목&본문 입력 후 발행
문제1. 마크다운 모드로 전환이 안 됨
첫 테스트를 해보니 제목까지는 잘 입력되는데, 본문 에디터를 마크다운 모드로 바꾸는 데서 에러가 났다

strict mode violation: locator(".CodeMirror textarea") resolved to 2 elements
에러메세지를 보면 CodeMirror textarea 요소가 2개라고 뜬다
Playwright는 셀렉터가 정확히 하나의 요소를 가리킬 때만 동작하기 때문에 오류가 발생하는 것이었다
처음엔 CodeMirror ID를 명시적으로 박아서 해결하려 했는데, 계속 타임아웃이 났다
그래서 Playwright Inspector를 띄워서 --debug 상태에서 브라우저가 멈춰 있을 때, Inspector의 "Pick locator" 버튼으로 화면 요소를 직접 클릭하면 정확한 셀렉터를 찾았다

요소 검사보다 훨씬 빨랐고 덕분에 진짜 원인을 알게되었다
본문 편집기는 처음 생각했던 것과 구조가 달랐다
(기본 모드는 iframe 기반 리치 텍스트고, 마크다운 모드는 CodeMirror였다.) 엉뚱한 셀렉터를 찾고 있었으니 될 리가 없었다
문제2. 한 번에 터진 네 가지 문제
셀렉터를 맞추고 나니 첫 글이 발행됐다! 그런데 결과물이 엉망이었다

#,*같은 마크다운 문법이 적용되지 않고 글자 그대로 노출됨- 이미지가 마크다운 문법(
![]()) 텍스트 그대로 보임 - 글 발행 순서가 최신순이 아닌 알파벳순으로 정렬
- 이상하게 본문 전체에 취소선이 그어진 채 발행됨
처음엔 네 개가 다 다른 문제인 줄 알았지만 파고들수록, 12는 하나의 원인에서 나온 거였다

바로 마크다운 모드로 전환이 실제로는 안 됐던 것인데 .. 마크다운 버튼을 누르면 작성 모드를 변경하시겠습니까?라는 확인창이 떴다가 0.1초 만에 사라지고, 모드는 그대로였던 거 였다
이 확인창은 브라우저 네이티브 confirm() 다이얼로그라서, 일반 DOM 버튼처럼 클릭할 수 없었다
그리고 Playwright는 다이얼로그 핸들러가 없으면 그걸 자동으로 취소(dismiss)해버리기 때문에 매번 전환이 안됐던 것이다
# 네이티브 confirm 다이얼로그는 이렇게 수락해야 함
page.once("dialog", lambda d: d.accept())
위를 스크립트에 추가하니 마크다운 모드가 제대로 켜졌고, 12가 해결됐다 (3 발행 순서는 별개 문제라, 파일명순 정렬을 글 작성일(date) 기준 오름차순으로 바꿔서 오래된 글부터 발행되게 고쳤다)
다만 4번 취소선 같은 문제는 아직 해결이 안돼서 수동으로 취소해주고 있다 ... 짐작가는 원인은 캐시 문제인 거 같은데 좀 더 시도해봐야겟다
문제3. 에디터에선 멀쩡한데, 발행하면 빈 본문


마크다운도 잘 적용되었고, 이제 모두 발행하려하니 발행하거나 미리보기를 누르면 본문이 텅 비어 있었다
에디터 화면에는 본문 1807자가 멀쩡히 들어가 있는데, 다시 원인을 파악해보았다 ..
원인은 CodeMirror.setValue()로 값을 넣으면 에디터 화면(표시 레이어)에는 텍스트가 들어가지만, 티스토리가 발행 시 실제로 읽어가는 저장 필드에는 동기화가 안 걸린다 프로그래밍으로 값을 주입한 거라, 티스토리 내부의 ‘사용자가 뭔가 입력했다’는 변경 감지가 작동하지 않았던 것이다
해결은 실제 사용자가 타이핑한 것처럼 만드는 것이었다
에디터를 클릭해 포커스를 준 뒤 키보드 입력으로 본문을 넣으니, 변경 감지가 정상적으로 걸려 저장 필드까지 동기화됐다
이 과정에서 ui 상태와 저장 상태는 별개라는 걸 다시 상기했다
화면에 보이는 것과 시스템이 저장하는 것은 다르다 😅
문제4.이미지 서빙 안됨
마지막 관문은 이미지였다
분명 깃헙에 잘 올렸는데, jsDelivr URL을 열면 404가 떴다
오류 메세지:
Couldn't find the requested file /assets/....png in yooncandooit/velog2tistory.
저장소는 찾았는데 그 파일은 못 찾는다는 메시지가 뜨길래, 확인해보니 파일은 로컬에도 있고, git ls-files에도 잡히고, 원격에도 push돼 있었다
GitHub 웹에서는 이미지가 멀쩡히 보이기까지 했다..
단서는 GitHub blob 페이지의 raw 링크가 @main이 아니라 커밋 해시로 뜬다는 점이었다.
.../blob/7c2971372864a8ce.../assets/... ← @main 이 아니라 커밋 해시
이건 저장소가 Private일 때 나타나는 특징이다.
jsDelivr나 raw.githubusercontent.com은 Private 저장소를 서빙하지 않는다. 그렇기 때문에 인증이 없는 CDN 입장에선 그 파일에 접근할 수 없으니 404였던 것이다
(알고보니 내가 로그인돼 있어서 깃헙 웹에서만 보였던 거)
이미지 호스팅 용도이니 저장소를 Public으로 전환했고, 그제야 CDN이 이미지를 정상적으로 서빙한 걸 확인 할 수 있었다
🥳 이전 성공
네 개의 문제를 하나씩 넘고 나니, 드디어 모든 글이 이미지까지 온전히 렌더링된 채로 옮겨졌다
오래된 글부터 최신 글 순서로, 마크다운 서식과 코드블록, 이미지까지 그대로 이전되었다!
배운 점
setValue로 화면은 채워도 저장 필드는 비어 있을 수 있다
프레임워크가 값을 언제 읽는지를 이해해야 하는 디버깅 과정이었다- 네이티브 다이얼로그는 DOM이 아니다
confirm()은 버튼 클릭이 아니라 dialog 이벤트로 처리해야 하고, 핸들러가 없으면 자동 취소된다 - 여러 증상이 한 원인에서 나올 수 있다. 마크다운 미적용, 이미지 노출이 전부 모드 전환 실패 하나였다
원인을 찾을 때 공통 원인일 수도 있다는 걸 배웠다 - 원인은 이분법으로 좁힌다 → raw와 jsDelivr를 나눠 테스트해서 캐시 문제인지 경로 문제인지 갈랐고, 커밋 해시라는 작은 단서가 private 저장소를 짚어줬다
- 에러 메시지를 정확히 읽자. "resolved to 2 elements", "저장소는 찾았으나 파일 없음"등등 메시지가 이미 원인을 절반쯤 알려주고 있어서 빠른 문제해결에 큰 도움이 되었다
- 마지막으로, 혼자하는 프로젝트에서 issue/PR 생성은 오버 엔지인듯 해서 issue 칸을 트러블 슈팅 기록 칸으로 썼다, 바로바로 이슈를 기록하는 습관을 기르기에 편한 방법인 듯하여 앞으로 개인 프로젝트 시 해당 방식으로 기록해도 좋을 거 같다

🌱 자동화 코드 오픈소스로 공개 (링크!)
지금 만든 스크립트는 주석, 임시 코드와 여러 번 고친 흔적이 많아서,
조만간 불필요한 부분을 정리하고, 남이 바로 따라 할 수 있는 형태로 다듬어 오픈소스로 공개하려 합니다.
취소선 이슈 or 혹시 쓰다가 오류가 나는 부분이 있으면 이슈로 남겨주시면 감사하겠습니다.
cf. 이 방식은 브라우저 자동화라 티스토리 에디터 UI가 바뀌면 셀렉터가 깨질 수 있고, 어디까지나 본인 계정에서 본인 글을 옮기는 개인 백업/이전 용도입니다. 각 플랫폼의 이용약관은 사용자 책임 하에 확인하시길 바랍니다.