블로그에 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로 直列化(직렬화)하면 各各(각각) nameMapsummaryMapcontentMap으로 表現(표현)된다。言語別(언어별) 값을 處理(처리)하지 않는 具顯(구현)()namesummarycontent에는 基本(기본) 言語(언어)의 값도 함께 넣었다。英文版(영문판)이 있으면 英語(영어)를、없으면 國漢文混用(국한문 혼용) 朝鮮語(조선어)基本(기본)값으로 삼았다。言語別(언어별) 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가 소셜 네트워크를 새로 만드는 데에만 쓰이는 것이 아니라、이미 存在(존재)하는 웹사이트가 自己(자기) 모습을 維持(유지)한 채 聯合宇宙(연합우주)合流(합류)하는 데에도 쓰일 수 있다는 것을 確認(확인)할 수 있었다。

이 글은 聯合宇宙(fediverse)에서 https://writings.hongminhee.org/ap/articles/2026/07/fedified-blog로 檢索해서 찾을 수 있습니다.