[프로젝트] Notion2WordPress 개발 회고 1

1. 배경

가. Tistory의 한계

Tistory에 개발 블로그를 운영하고 있다.

4년 정도 운영한 Tistory 개발 블로그

4년 정도 블로그를 운영하면서 기록하는 습관이 생겼다.

이제 개발 블로그뿐만 아니라 다양한 주제로 블로그를 확장하고 싶었다.

기존에 사용하던 Tistory를 사용할 수도 있지만 그렇게 하지 않았다.

평소 Tistory를 사용하면서 아쉬운 점이 몇 개 있다.

우선 Tistory는 포스팅을 자동화하기 어렵다.

Tistory에서 제공하는 API로는 자동화하는 것에 한계가 있었다.

특히 이미지를 업로드하는 것에 많은 시간이 낭비되고 있었다.

또한 가장 큰 문제점은 소중한 데이터들을 다른 사람에게 맡겨뒀다는 불안함이다.

문제는 수익성 부족으로 인한 티스토리 정책의 변화, 다음과 카카오의 분리 등 Tistory의 불확실성이 증가하고 있다.

늦었지만 중요한 데이터를 직접 소유하고 관리한다고 결심했다.


나. WordPress

블로그를 처음 시작할 때와는 달리 개발 지식도 많이 쌓였다.

무엇보다도 이젠 직접 홈서버를 운영하고 있다.

별도의 비용 없이 블로그를 호스팅할 수 있다.

때문에 직접 호스팅할 수 있는 블로그 애플리케이션을 찾아보았고 WordPress가 레이더에 포착되었다.

직접 블로그를 개발하는 것도 고려했지만 유지보수의 부담을 줄이고 싶어 사람들이 많이 사용하는 CMS인 WordPress를 선택했다.

앞으로 새로운 블로그는 워드프레스에서 시작하고, 기존의 개발 블로그는 워드프레스로 옮길 예정이다.


다. Notion과 WordPress

구텐베르크 에디터의 완성도가 부족함
구텐베르크 에디터

워드프레스의 기본 에디터인 구텐베르크 에디터는 불편했다.

커서와 입력 위치가 따로 노는 등 아직 완성도가 많이 부족해 보였다.

지금껏 Notion에서 작성하고 워드프레스에 포스팅하고 있었고, 별일 없다면 앞으로도 그럴 예정이다.

WordPress로 옮기고 나서부터는 최대한 자동화해 생산성을 높일 예정이다.

Notion에서 작성된 글을 자동으로 WordPress에 업로드하는 것이 목표다.

처음에는 유명한 자동화 도구인 n8n을 사용해서 자동화를 시도했지만 실패했다.

요구사항과 정확히 일치하는 커뮤니티 노드는 방치된 지 오래였다.

n8n에서 제공하는 직접 Javascript를 작성할 수 있는 노드를 제공한다.

결국 일정 수준 이상의 작업을 처리하고 싶다면 직접 코딩해야 하고 이것은 직접 개발하는 것과 크게 다를 바 없다.

몇몇 WordPress Plugin을 사용할 수도 있었지만, 추가적인 비용을 지불하기 싫었고, 플러그인을 신뢰할 수 없었다.

결국 장기적으로 생산성을 끌어올리기 위해선 직접 개발하는 것이 옳다고 판단해서 직접 개발하기로 마음먹었다.


2. 프로젝트 소개

An automated synchronization system that syncs Notion pages to WordPress blog as draft posts. Simply write your content in Notion and automatically publish it as a WordPress draft.

Notion2Wordpress는 Notion에서 작성한 글을 WordPress로 자동으로 동기화하는 시스템이다.

제공하는 핵심 기능은 다음과 같다.

  • Automatic Synchronization: Notion 페이지를 자동으로 WordPress 임시 글로 변환
  • Image Handling: Notion에서 이미지를 다운로드하여 WordPress 미디어 라이브러리에 업로드
  • Scheduling: Cron 기반 주기적 동기화(기본값: 5분마다)
  • Manual Execution: CLI를 통한 수동 동기화 지원
  • Error Handling: 실패 시 자동 재시도(기본 3회) 및 롤백 지원
  • Notifications: 동기화 성공/실패에 대한 Telegram 알림
  • Tracking: SQLite 기반 동기화 기록 관리

이외에도 쉽게 배포할 수 있도록 Docker Image와 docker-compose.yml을 제공한다.

구체적인 스펙은 spec.md를 참고하세요.


가. 동작

Notion Database 설정
Status Property ValuesDescription
writing작성 중. 동기화 대상에서 제외.
adding동기화 대상
done동기화 완료
error동기화 실패
  1. 가장 먼저 Notion에 위 예시를 따라서 데이터베이스를 생성한다.
  2. 새 페이지를 생성한다.
  3. Notion 페이지의 status 속성을 adding으로 설정한다.
  4. 시스템이 해당 페이지를 자동으로 감지하여 WordPress에 임시 글로 동기화한다.
  5. 동기화가 성공하면 Notion의 statusdone으로 업데이트한다. 실패 시 error로 업데이트한다.
  6. Telegram을 통해 동기화 결과 알림을 받는다.
  7. WordPress 관리자 패널에서 임시 글을 검토한 후 수동으로 발행한다.

나. 기술 스택

목적기술버전
RuntimeNode.js20.x
RuntimeTypeScript5.9.3
Notion API@notionhq/client5.4.0
WordPress APIaxios1.13.2
Content Conversionnotion-to-md3.1.9
Content Conversionmarked17.0.0
Schedulernode-cron4.2.1
Databasebetter-sqlite312.4.1
NotificationsTelegraf4.16.3
TestingVitest4.0.8
DeploymentDocker

Notion Page를 마크다운으로 변환하기 위해서 많이 사용하는 notion-to-md라는 라이브러리를 사용한다.

마크다운을 다시 HTML로 변환하기 위해서 marked라는 라이브러리를 사용한다.

해당 라이브러리들을 사용하기 위해서 런타임은 typescript를 사용하게 되었다.

SQLite로도 충분할 것 같아 MySQL, PostgreSQL 같은 무거운 DB 대신에 비교적 가벼운 SQLite를 선택했다.

처음에는 sqlite3를 사용하려 했으나 더 이상 유지보수가 되지 않아서 better-sqlite3를 사용했다.


3. 설계

소프트웨어를 설계하는 과정에서 고민했던 것들을 몇 가지 소개하겠다.


가. 유지보수 부담 줄이기

유지보수의 부담을 최소화하기 위해서 라이브러리를 적극적으로 사용했다.

단, 보안을 위해서 공식 라이브러리와 비교적 검증된 메이저 라이브러리만 사용하고, 꼭 필요한 경우에만 라이브러리를 사용했다.

Notion Block의 구조가 변경될 경우를 가장 염려했다.

Notion Block에 수정 사항이 있을 때마다 코드를 수정하고 싶진 않았다.

다행히 Github에서 Notion Page를 마크다운으로 변환하기 위해서 많이 사용하는 notion-to-md라는 라이브러리를 찾을 수 있었다.

WordPress는 HTML을 업로드할 수 있기 때문에 마크다운을 HTML로 변환하기 위해서 marked라는 라이브러리를 추가로 사용했다.

해당 라이브러리들을 사용하기 위해서 런타임은 Typescript를 사용하기로 했다.

물론 Javascript의 weak typing이 유지보수에 불리하다는 점을 고려해서 Typescript를 사용한 것도 있다.

Parameter와 Return의 type을 엄격하게 관리할 수 있어서 좋았다.

덕분에 Javascript지만 Java 같은 느낌으로 코딩할 수 있었다.


나. 시퀀스 다이어그램

이 프로젝트는 처음에는 github의 Spec-kit을 사용했다.

Spec-kit이 생성하는 개발 문서의 수준은 굉장히 높아서 만족했다.

단 한 가지 아쉬웠던 점이 있다면 시퀀스 다이어그램이 없다는 점이었다.

처음에는 Agent가 전체 프로세스를 설계하지 않은 상태에서 DB를 모델링했다.

그렇다 보니 table의 생성 및 삭제 순서와 table 간의 관계를 전혀 고려하지 않았다.

그럴듯해 보이지만 자세히 들여다보면 논리적인 오류가 있다는 사실을 알 수 있었다.

그래서 전체 프로세스를 이해시키기 위해서 DB 모델링에 앞서 시퀀스 다이어그램을 작성하도록 했다.

몇 번의 수정을 거쳐서 시퀀스 다이어그램을 완성할 수 있었다.

간략하게 설명하자면…

  1. Notion에 요청하여 동기화할 페이지들을 찾는다.
  2. 해당 페이지들의 Notion Block을 다운로드 받는다.
  3. 그 중 이미지 URL을 따로 수집한다.
  4. Notion Block을 Markdown으로 변환하고 이를 다시 HTML로 변환한다.
  5. 이미지를 WordPress에 업로드한다.
  6. HTML에 있는 Notion image URL을 WordPress image URL로 교체한다.
  7. WordPress에 새 글을 업로드한다.
  8. 앞선 단계에서 문제가 발생하면 즉시 롤백을 실행해서 트랜잭션의 원자성을 보장한다.
  9. 실행 결과를 Telegram 메시지로 알린다.

성공 및 실패 시퀀스 다이어그램을 작성한 다음 이것을 참고하여 DB 모델링을 요청했다.

완벽하지 않아서 직접 손보긴 했지만, 꽤나 괜찮게 만들어 주었다.

해당 시퀀스 다이어그램은 본격적인 구현 단계에서도 Agent가 참고하는 중요한 Context였다.


다. 증분 스캔

글이 많이 쌓이면 동기화할 Notion 페이지를 찾는 것이 힘들어진다.

그래서 가장 최근에 동기화한 시간 이후에 수정된 페이지들에 대해서만 스캔하도록 설계했다.


라. 이미지 URL

이미지를 자동으로 업로드하는 것은 해당 프로젝트의 핵심 요구사항 중 하나다.

그리고 가장 복잡한 부분이었다.

Notion에서 제공하는 URL은 1시간 후에 만료된다.

따라서 직접 WordPress에 업로드하고 해당 URL로 교체해야 했다.

우선 Notion Page에서 이미지를 찾아서 다운로드 받는다.

다운받은 이미지를 WordPress에 업로드하고 URL을 반환받는다.

본문의 Notion image URL을 WordPress image URL로 교체해야 한다.

초기 설계 단계에서는 HTML로 변환을 마친 후 Notion image URL을 HTML에서 찾아서 교체하려고 했다.

2부에서 나중에 언급하겠지만 이미지 URL과 관련해서 많은 버그가 발생했다.


마. Webhook이 아닌 Cron을 선택한 이유

Webhook을 사용하면 Notion Database의 변경 사항이 있을 때만 동작을 실행시킬 수 있어 더 합리적이다.

그럼에도 Webhook보다는 Cron을 선택한 이유가 있다.

Port를 열고 싶지 않았다.

모르는 IP가 정말 미친 듯이 두드린다
그만해라 진짜

홈서버를 직접 운영하면서 Port를 열지 않는 것이 최고의 보안 조치라는 것을 뼈저리게 느끼고 있다.

따라서 다소 비효율적이지만 Port를 열지 않아도 된다는 점이 더 중요해서 Webhook이 아닌 Cron을 선택했다.


바. MVP

분명 있으면 좋지만, MVP에는 포함하지 않은 기능들이 있다.

  • No update sync: 새로운 페이지만 처리되며, 기존 페이지는 수정해도 업데이트되지 않는다
  • No auto-publish: 모든 게시물은 WordPress 관리자 패널에서 수동 승인이 필요하다
  • No deletion sync: Notion에서 삭제되어도 WordPress 게시물은 그대로 유지된다
  • No category/tag sync: WordPress 기본 설정이 사용된다
  • No duplicate image check: 중복된 이미지를 감지하거나 업로드를 방지하는 기능이 없다

아쉽지만 MVP는 정말 최소한의 핵심 기능을 성공적으로 구현하는 것에 집중했다.

특히 두 가지 기능은 끝까지 MVP에 포함할지 고민했다.

  • Duplicate image check(이미지 중복 업로드를 방지하는 기능)

고민 끝에 포함하지 않은 이유는 이 기능이 DB에 의존하고 있다는 것이었다.

기존에 운영 중인 WordPress에 연결할 경우 WordPress의 미디어 라이브러리를 읽어서 DB에 저장해야 한다.

이 과정이 MVP에 포함하기에 부적합하다고 판단해서 이미지 중복 업로드를 막는 기능은 MVP에서 제외하게 되었다.

  • Update sync(기존 페이지는 수정해도 업데이트되는 기능)

해당 기능의 경우 Notion → WordPress로 수정할 수 있는 기능을 제공한다.

다만 WordPress에서 바로 수정한 경우를 고려하면 복잡해진다.

수정한 부분이 WordPress → Notion으로의 역방향 동기화가 진행되지 않는다면 일관성이 깨진다.

결국 Update sync 기능을 구현하기 위해서는 양방향 동기화 기능을 우선적으로 구현해야 한다.

이 또한 MVP의 범주를 벗어난다고 생각해서 제외하게 되었다.


4. 구현

가. 디렉터리 구조

src/
├── index.ts                 # Main entry point
├── cli/                     # CLI commands
├── config/                  # Environment configuration
├── db/                      # Database management
├── enums/                   # Type definitions
├── lib/                     # Utility functions
├── orchestrator/            # Sync orchestrator
└── services/                # External service integrations
    ├── notionService.ts     # Notion API
    ├── wpService.ts         # WordPress API
    └── telegramService.ts   # Telegram API
Code language: PHP (php)
image.png
프로젝트 구조

Cron과 Manual로 실행하는 두 가지 옵션을 제공한다.

모든 절차는 syncOrchestrator.ts에서 통제한다.

외부 API는 /services 아래에 *Service.ts라는 파일에서 다룬다.

MVP에선 syncOrchestrator가 모든 작업을 처리하고 있다.

장차 유지보수를 위해서 syncOrchestrator를 2가지 layer로 나눌 필요가 있어 보인다.

Task를 정의하는 layer와 실제로 task를 처리하는 layer로. (약간 SpringBoot의 ControllerService 같은 느낌으로)


나. 전체 일정

혼자서 1달이라는 시간 동안 기획, 설계, 구현, 배포를 모두 진행했다.

  • 요구사항 정의 및 설계 : 3일
  • Agent를 활용한 구현 : 1일
  • 버그 수정 및 리팩토링 : 2주
  • 배포 및 문서화 : 2주

모든 과정에서 AI를 적극적으로 활용했다.

덕분에 혼자서 개발했지만 꽤나 완성도 있는 결과물을 얻을 수 있었다.


다. Agentic Coding 그리고 끝없는 리팩터링

이번 프로젝트는 정말 특이하게 진행됐다.

구현에 1일 리팩터링에 2주가 걸렸다.

AI 덕분이기도 하고 AI 때문이기도 하다.

Agent는 정말 빠른 속도로 동작하는 MVP를 만들어냈다.

기쁜 마음에 꿀잠을 자고 아침에 일어나서 코드를 리뷰했다.

angry pepe meme

그리고 분노했다.

대표적인 문제점 몇가지만 나열하자면…

  • 리터럴 남발
  • 일관성 부족
  • 애써 구현한 함수 미사용
  • 읽기도 힘든 엄청나게 긴 함수
  • 코드 중복

덕분에 모든 코드를 처음부터 끝까지 리뷰하고 리팩터링을 할 수 밖에 없었다.

덕분에 구현에 1일, 리팩터링에 2주가 소모되는 기이한 경험을 했다.

그래도 AI가 발전하면서 좋은 점도 많다.

실제로 Typescript는 처음인데 AI와 함께라면 만들면서 배울 수 있다!

또 테스트 코드 추가, 주석 추가, 문서작성과 코드리뷰에 강하다.


2부에선 버그, 성능 개선, 배포, 실사용 그리고 느낀점에 대하여 다루겠다.

댓글 남기기