블로그에 ActivityPub 연동을 붙였다

직접 만든 정적 사이트 생성기인 Jikji로 이 블로그를 만들고 나서 5년 가까이 흘렀다. 그때의 나는 아직 TypeScript나 최신 웹 기술에 익숙하지 않았고, ActivityPub 구현도 해 본 적이 없었다. 하지만 이제는 TypeScript와 최신 웹 기술에 비교적 익숙해졌고, ActivityPub은 내게 중요한 기술이 되었다. 게다가 명색이 Fedify 메인테이너인데 정작 블로그가 연합이 안 되어 있다는 게 늘 마음에 걸리기도 했다. 그래서 블로그에 ActivityPub 연동을 붙이게 되었다.

기존 스택: Jikji + PHP

이 블로그는 원래 Jikji라는 내가 직접 Deno로 만든 정적 사이트 생성기 기반이었다. 엄밀하게는 정적 사이트라고 말하기는 어려웠는데, 옛날 Movable Type이 그러했던 것처럼, HTML만을 생성하는 게 아니라 PHP도 일부 생성했기 때문이다. PHP는 주로 HTTP 내용 협상을 위해서 쓰였다. 브라우저의 Accept-Language 헤더를 보고, 국한문 혼용 조선어, 한글 전용 한국어, 영어, 일본어 중에 적절한 언어로 표시했다. 단지 그뿐이지만.

이미 PHP를 쓰고 있으니, PHP로 얇게 ActivityPub 구현을 붙이는 것을 먼저 고려했다. 하지만 PHP를 쓰고 있다곤 해도 직접 코딩하는 게 아니라 Jikji가 생성해줬기 때문에 쓴 것이지, PHP를 손으로 직접 코딩하고 싶지 않다는 생각이 들었다. 새 글이 올라오면 자동으로 Create(Article) 액티비티를 팔로워들에게 전달해야 하는데 그러자면 어차피 메시지 큐 같은 구조가 필요해진다. PHP에 메시지 큐까지 붙여서 사용하게 되면, 사견으로는 PHP에 적합한 규모와 복잡도를 벗어난다는 느낌도 들었다. 그리고 무엇보다, Fedify가 있는데 굳이 ActivityPub 구현을 바닥부터 하고 싶지 않다는 생각도 했다.

그래서 PHP를 아예 걷어내고, Fedify를 붙이기로 마음먹었다.

새 스택: Astro + Netlify

일단 첫 선택으로, Jikji와 PHP를 버리고 정적 콘텐츠 중심의 웹 사이트를 만드는 데에 특화되어 있는 JavaScript 웹 프레임워크 Astro를 쓰기로 했다. 이미 @fedify/astro 연동 패키지가 있다는 것도 Astro를 고른 큰 이유였다.

단, 기존에 쓰던 CSS나 HTML 템플릿은 최대한 재활용했다. 현재 웹 디자인에 만족하고 있기도 했고, 웹 디자인 개편까지 하면 범위가 너무 커질 것 같았다. 퍼머링크도 완전히 그대로 보존했다. 전반적으로 방문자가 뭐가 딱히 바뀌었는지 눈치 못 챌 정도로 표면을 유지하면서 기술 스택을 이전하는 게 목표였다.

배포는 Cloudflare Workers와 Netlify 중에서 고민하다가, Fedify를 아직 Netlify에 올려 본 적이 없는 것 같아서 Fedify의 Netlify 지원을 이참에 추가하는 계기로 삼기로 했다. 여태까지 정적 사이트 호스팅을 위해 Netlify를 여러 차례 써 왔지만, 에지 함수를 함께 쓰는 건 처음이었다. 기본적으로는 정적인 애셋들로 이뤄져 있지만 일부 기능만 동적으로 동작한다는 개념이, 마치 90년대 말 웹 사이트를 만들 때 CGI 스크립트만 /cgi-bin/ 디렉터리 안에 두던 방식을 떠오르게 했다.

기존에는 블로그 글을 Markdown 파일로 저장하고 Git에 체크인한 뒤, 푸시하면 GitHub Actions로 정적 사이트를 빌드하고 나서 SFTP를 이용해 배포하는 흐름이었는데, 이제는 GitHub Actions를 빌드 파이프라인에서 뺄 수 있었다. Netlify가 알아서 빌드를 해주기 때문이다. 결과적으로 더 단순해졌다.

Astro는 대체로 만족스러웠고, 이전은 비교적 수월했다. 5년 전에 만든 뒤 거의 업데이트를 안 했던 Jikji보다 나은 건 물론이었다. Jikji는 더 이상 관리할 이유가 없어졌기 때문에 저장소를 아카이브 처리했다.

Astro에 Fedify 붙이기

@fedify/astro 최신화

그런데 막상 Astro에 Fedify를 붙이려니, @fedify/astro가 최신 버전인 Astro 7을 지원하지 않았다. 내부에서 사용하는 Astro API는 크게 달라지지 않았지만, 패키지에 명시된 호환 범위와 테스트는 Astro 5까지만 다루고 있었다. 결국 블로그에 Fedify를 붙이기 전에 먼저 @fedify/astro부터 고쳐야 했다.

단순히 패키지의 버전 범위만 넓히지는 않았다. 기존 테스트는 가짜 Astro 컨텍스트를 만들어 미들웨어를 직접 호출했기 때문에, Vite의 SSR 설정이나 어댑터 사이의 호환성, 빌드된 서버의 요청 라우팅 같은 문제는 잡아낼 수 없었다. 그래서 Fedify 패키지를 실제로 패킹해 작은 Astro 애플리케이션에 설치하고, 서버를 빌드하고 실행한 다음 HTTP 요청까지 보내는 호환성 테스트를 새로 만들었다.

이 테스트는 Astro 5, 6, 7에서 HTML 요청은 Astro 페이지로 넘어가는지, ActivityPub과 WebFinger 요청은 Fedify가 처리하는지, 그밖의 경로에서는 Astro의 404 Not Found 응답이 유지되는지 등을 확인한다. Astro 7은 Node.js뿐 아니라 Deno와 Bun으로도 빌드해 보도록 했다.

이 작업은 이미 업스트림에 머지되었고, Fedify 2.4.0에 포함될 예정이다.

정적 페이지와 동적 엔드포인트

Astro 프로젝트 전체는 서버 아웃풋으로 빌드하되, 기존 블로그 페이지는 이전처럼 프리렌더링하도록 했다. 반면 WebFinger와 액터, 인박스, 아웃박스, 팔로워 컬렉션, ActivityPub 객체 경로는 요청을 받을 때 Fedify가 동적으로 처리한다. @fedify/astro가 제공하는 미들웨어는 요청의 URL과 Accept 헤더를 살펴보고 Fedify가 처리할 요청만 가로챈다. 같은 URL이라도 HTML을 요청하면 기존 Astro 페이지를 보여주고, ActivityPub 표현을 요청하면 Fedify가 만든 객체를 돌려줄 수도 있다.

결과적으로 방문자가 보는 블로그는 여전히 정적 사이트에 가깝다. 새로 생긴 동적 부분은 대부분 페디버스의 다른 서버가 접근하는 곳에만 있다. 앞에서 CGI를 떠올렸던 것도 이런 구성 때문이었다.

PersonArticle

ActivityPub을 붙일 때에는 이 블로그에서 무엇이 액터이고 무엇이 객체인지도 정해야 했다. 블로그의 액터에는 Person 타입을 골랐다. 실제 발행 작업은 프로그램이 자동으로 하지만, 액터가 나타내는 것은 블로그 소프트웨어나 서비스가 아니라 이 글들을 쓰는 나 자신이기 때문이다. 그래서 핸들은 @hongminhee@writings.hongminhee.org이고, 액터의 웹 URL은 이 블로그를 가리킨다.

각 블로그 글에는 Article 타입을 골랐다. 제목과 본문이 있고, 독립적인 퍼머링크를 가진 장문 문서이므로 Note보다 의미가 잘 맞는다고 생각했다. 다행히 Mastodon 등 최근의 주요 ActivityPub 구현들도 Article을 대부분 지원한다. 사람이 읽는 기존 퍼머링크는 그대로 두고, ActivityPub 객체에는 /ap/articles/{year}/{month}/{slug} 형태의 별도 URI를 붙였다. Articleurl은 다시 기존 퍼머링크를 가리킨다. ActivityPub 객체의 URI와 사람이 읽을 웹 페이지의 퍼머링크를 구분한 것이다.

다국어 표현은 조금 더 고민할 부분이 있었다. 언어별 페이지를 서로 다른 Article 객체로 표현하면 같은 글에 대한 반응과 공유가 여러 객체로 흩어진다. 그래서 같은 퍼머링크에 속한 국한문 혼용 조선어(ko-Kore), 한글 전용 한국어(ko-Hang-KR), 영어(en), 일본어(ja) 버전을 하나의 Article로 묶었다. 제목과 요약, 본문에는 각각 언어 태그가 붙은 값을 모두 넣었다. JSON-LD로 직렬화하면 각각 nameMap, summaryMap, contentMap으로 표현된다. 언어별 값을 처리하지 않는 구현을 위해 name, summary, content에는 기본 언어의 값도 함께 넣었다. 영문판이 있으면 영어를, 없으면 국한문 혼용 조선어를 기본값으로 삼았다. 언어별 HTML 페이지는 Articleurl에도 hreflang 속성이 붙은 Link 객체로 덧붙였다.

이렇게 하면 수신 서버가 다국어 값을 이해할 경우 이용자의 언어에 맞는 제목과 본문을 고를 수 있고, 그렇지 않더라도 기본값은 표시할 수 있다. 사실, 내가 아는 거의 모든 ActivityPub 구현은 아직 이런 다국어 자연어 값을 제대로 표시하지 못한다. Mastodon 이슈 트래커에 이를 위한 이슈가 올라와 있고 Hackers' Pub 이슈 트래커에도 비슷한 제안이 올라와 있지만, 언제 구현될지는 기약이 없다. 아마 UI 디자인 측면에서도 고민이 필요할 것이다.

Fedify를 Netlify에서 돌리기

정적 파일만 배포할 때와 달리 ActivityPub 서버에는 배포가 끝난 뒤에도 남아 있어야 하는 상태가 있다. 우선 액터의 서명 키가 배포할 때마다 바뀌어서는 안 된다. 팔로워 목록도 다음 배포에서 사라지면 안 된다. 이 두 가지는 Netlify Database에 저장했다.

인박스에서 받은 액티비티와 원격 서버에 보낼 액티비티는 Async Workloads로 만든 메시지 큐에서 처리한다. 액티비티 전송은 상대 서버의 상태에 따라 느려지거나 실패할 수 있으므로, HTTP 요청을 받은 함수 안에서 모두 끝내려고 해서는 안 된다. 큐에 넣어 두면 요청과 전달을 분리할 수 있고, 실패한 작업도 나중에 재시도할 수 있다. 다행히 Fedify는 이러한 작업을 추상화해 두었고, 백엔드 어댑터도 확장할 수 있다. 다만 아직 Netlify의 Async Workloads를 위한 어댑터가 없었기 때문에, Fedify가 Async Workloads를 메시지 큐로 사용하면서 Netlify Database에 전달 순서 상태를 보존할 수 있도록 @fedify/netlify 패키지도 만들었다.

새 글을 연합우주(fediverse)에 알리는 일은 조금 다른 문제였다. 정적 사이트의 빌드가 끝났다고 해서 실행 중인 ActivityPub 서버가 어떤 글이 달라졌는지 저절로 알 수는 없다. 그래서 프로덕션 배포가 성공하면 이전 배포와 새 배포의 글 목록을 비교한다. 새로 생긴 글에는 Create(Article), 내용이나 수정 시각이 달라진 글에는 Update(Article), 사라진 글에는 Delete(Article) 액티비티를 만들어 팔로워에게 보낸다. 재시도하더라도 같은 변경에 같은 액티비티 ID를 쓰고, 더 오래된 배포가 나중에 동기화되어 최신 상태를 되돌리지 못하도록 배포 순서도 확인한다.

Netlify에는 배포 미리보기(deploy preview)와 브랜치 배포(branch deploy) 기능이 있는데, 그 경우에는 연합 기능을 아예 끄도록 했다. 미리보기마다 같은 블로그를 자처하는 액터가 생기거나, 시험 배포가 실제 팔로워에게 액티비티를 보내면 곤란하기 때문이다. 로컬에서는 메모리 저장소와 큐를 써서 개발할 수 있고, 프로덕션에서만 영속적인 데이터베이스와 큐를 사용한다.

아무튼 덕분에 Fedify는 이제 Deno Deploy 및 Cloudflare Workers와 함께 Netlify Functions도 지원하게 되었다. 물론, Node.js, Deno, Bun에서도 돌아가는 것은 기본이다.

마치며

이 작업으로 블로그에 타임라인이나 답글 작성 화면 같은 소셜 기능이 생긴 것은 아니다. 글을 쓰고 읽는 방식도, 기존 퍼머링크와 디자인도 거의 그대로다. 다만 이제 이 블로그와 글에는 연합우주에서 통용되는 이름과 주소가 생겼다. 사람들은 @hongminhee@writings.hongminhee.org를 팔로해 새 글을 받아 볼 수 있고, 각 글의 ActivityPub 객체 URI를 검색해 원문을 찾을 수 있다.

Fedify를 유지보수하면서 다른 개발자에게 ActivityPub을 구현하는 방법을 제공해 왔고, HolloHackers' Pub 등을 만들면서 개밥 먹기도 꽤나 했지만, 이미 운영 중인 웹 사이트, 그것도 정적 페이지로 되어 있던 블로그에 Fedify를 붙이는 건 처음이었다. 덕분에 Astro 통합의 호환성 테스트와 Netlify 지원을 추가했고, 문서나 단위 테스트만 봐서는 알기 어려운 배포와 운영상의 문제도 겪었다. Fedify가 소셜 네트워크를 새로 만드는 데에만 쓰이는 것이 아니라, 이미 존재하는 웹사이트가 자기 모습을 유지한 채 연합우주에 합류하는 데에도 쓰일 수 있다는 것을 확인할 수 있었다.

이 글은 연합우주(페디버스)에서 https://writings.hongminhee.org/ap/articles/2026/07/fedified-blog로 검색해서 찾을 수 있습니다.