0. 오픈소스 기여했던 프레임워크를 실제로 사용해보자
오픈소스 컨트리뷰션 아카데미의 Fedify 팀에 참여해 오픈소스 기여를 진행했었다. 컨트리뷰션 아카데미를 수료한 후, 내가 기여한 Fedify 프레임워크를 제대로 사용해본적이 없는 거 같다는 생각을 하게 되었다. 이 프레임워크를 어떻게 활용해볼까 생각해보다가, 내 개인 사이트에 Fedify를 도입해서 연합우주 계정을 생성하고 다른 연합우주 SNS와 소통을 해보기로 했다.
1. ActivityPub, 연합우주(Fediverse)와 Fedify
ActivityPub은 World Wide Web Consortium(W3C)에서 표준화한 개방형 분산 소셜 네트워크 프로토콜이다.
(자세한 설명: https://hackernoon.com/lang/ko/액티비티펍에-대한-간략한-소개-소셜-네트워크의-미래)
ActivityPub 프로토콜을 기반으로 만들어진 SNS들은 '연합우주(Fediverse)'에 속해 있다고 표현한다. 이 프로토콜 덕분에 연합우주 내의 서로 다른 SNS 사용자들끼리도 계정을 팔로우하거나 댓글을 남기는 등 자유롭게 상호작용할 수 있다. 그리고 'Fedify' 프레임워크를 활용하면 ActivityPub 기반 SNS를 더욱 쉽고 편리하게 개발할 수 있다.
2. 데이터베이스 구조
개인 웹 사이트에 ActivityPub 프로토콜을 구현하기 위해 아래와 같이 ERD를 구성했다.

actors - 사용자 정보
연합우주의 모든 사용자를 나타내는 테이블이다. 각 Actor는 고유 식별자(id)와 사용자명(username), 화면에 표시될 이름(display_name)을 가진다. inbox_url은 이 사용자가 다른 서버로부터 메시지를 받을 주소이며, shared_inbox_url은 서버 전체가 공유하는 inbox 주소이다. is_local 필드로 우리 서버의 사용자인지 다른 서버 사용자인지를 구분할 수 있다.
key_pairs - 암호화 키
분산 네트워크에서 메시지의 진위를 검증하기 위한 암호화 키 쌍을 저장한다. private_key로 메시지에 서명하고, public_key로 다른 사람이 그 서명을 검증한다. 이를 통해 메시지가 정말 해당 사용자로부터 온 것인지 확인할 수 있다.
micro_posts - 게시물
사용자가 작성한 게시물을 저장한다. content에는 원본 텍스트가, content_html에는 HTML로 변환된 내용이 담긴다. visibility 필드로 게시물의 공개 범위를 설정할 수 있는데, public은 모두에게 공개, followers는 팔로워에게만 공개하는 방식이다.
follows - 팔로우 관계
follower_id는 팔로우하는 사람, following_id는 팔로우 받는 사람을 나타낸다. status 필드로 팔로우 요청이 아직 승인 대기(pending) 상태인지, 이미 승인(accepted)되었는지 추적한다. 이는 비공개 계정의 팔로우 승인 기능에 사용된다.
inbox_activities - 받은 활동 기록
다른 서버로부터 받은 모든 활동을 기록한다. 누군가 나를 팔로우하거나 내 게시물에 좋아요를 누르면 해당 활동이 ActivityPub 프로토콜을 통해 전달되어 이 테이블에 저장된다. raw_data에는 원본 JSON 데이터가, processed 필드에는 처리 여부가 기록되어 중복 처리를 방지한다.
3. Nest.js를 활용하여 백엔드 구축하기
Nest.js를 활용하여 백엔드를 구축했다. 구조는 다음과 같다.

fedify.service.ts
ActivityPub 프로토콜의 중심이다. Federation 인스턴스를 주입받아(@fedify/nestjs 활용) Actor Dispatcher, Object Dispatcher, Inbox Listeners, Outbox를 초기화한다.
Actor Dispatcher는 사용자 정보를 제공하고, Object Dispatcher는 게시물을 제공한다.
inbox.service.ts
다른 연합우주 서버로부터 받은 활동을 처리한다.
Follow 요청을 받으면 원격 사용자 정보를 저장하고 팔로우 관계를 데이터베이스에 기록한 후, Accept 응답을 보낸다. Undo 활동이 오면 언팔로우를 처리한다.
outbox.service.ts
우리 서버에서 다른 서버로 활동을 전송한다.
Accept 활동으로 팔로우 요청을 수락하고, Create 활동으로 새 게시물을 팔로워들에게 전달한다. Federation의 ‘sendActivity’ 메서드를 사용해 HTTP 서명된 요청으로 전송한다.
actor.service.ts
사용자 정보를 관리한다. 로컬 사용자는 ‘createLocalActor’로 생성하며, 원격 사용자는 ‘createOrUpdateRemoteActor’로 다른 서버에서 받은 정보를 저장한다. 모든 사용자는 고유한 ActivityPub ID(URI)를 가진다.
keypair.service.ts
각 사용자의 암호화 키 쌍을 관리한다. RSA와 Ed25519 두 가지 타입의 키를 생성하고 JWK 형식으로 저장한다. 이 키들은 ActivityPub 메시지에 HTTP 서명을 하여 메시지의 신뢰성을 보장한다.
following.service.ts
팔로우 로직을 처리한다. WebFinger 프로토콜로 원격 사용자를 찾고, Follow 활동을 전송한다. 또한 팔로잉 중인 사용자들의 최신 활동을 수집해 타임라인을 생성한다.
following.controller.ts
팔로우 요청을 받고, 팔로워 목록을 조회하며, 타임라인을 제공하는 API를 생성했다.
micro-posts.service.ts
게시물 작성을 담당한다. 새 게시물을 ActivityPub의 Note 객체로 변환하고, Create 활동으로 감싸서 모든 팔로워에게 전송한다.
4. Nginx 리버스 프록시와 도메인 구성
프로덕션 환경에서는 프론트엔드와 백엔드를 별도 도메인으로 분리했다. 메인 도메인은 Next.js 프론트엔드를 서빙하고, API 서브도메인은 NestJS 백엔드를 제공한다.
백엔드 서버는 EC2에서 포트 3001로 실행되며, Nginx가 API 서브도메인의 모든 요청을 localhost:3001로 프록시한다. 환경변수는 FEDIVERSE_BACKEND_URL=https://another.domain.com로 설정했다. (이 글에서는 보안을 위해 API 서브도메인을 another.domain.com으로 표기한다.)
Nginx 설정
ec2 ssh에서 /etc/nginx/conf.d/another.domain.com.conf 파일에 다음과 같이 설정했다.
server {
server_name another.domain.com;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/another.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/another.domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = another.domain.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name another.domain.com;
return 404;
}
Let's Encrypt를 통해 SSL 인증서를 발급받아 HTTPS를 지원하며, 모든 HTTP 요청은 자동으로 HTTPS로 리다이렉트된다.
ActivityPub을 위한 프록시 설정
ActivityPub 프로토콜이 제대로 작동하려면 특정 경로들이 백엔드로 라우팅되어야 한다. Next.js의 rewrites 기능을 활용하여 /.well-known/, /users/, /inbox 같은 경로를 API 서브도메인으로 프록시한다.
가장 중요한 것은 WebFinger 프로토콜이다. 다른 연합우주 서버가 @username@mydomain.com 형식으로 사용자를 찾을 때, ‘/.well-known/webfinger?resource=acct:username@mydomain.com’ 경로로 요청을 보낸다. 이 요청은 Next.js rewrites를 통해 https://another.domain.com/.well-known/webfinger로 전달되고, Nginx는 이를 다시 백엔드(포트 3001)로 프록시한다. 백엔드는 Fedify가 자동으로 생성한 WebFinger 응답을 반환하여 해당 사용자의 ActivityPub 프로필 주소를 알려준다.
이러한 프록시 체인 덕분에 연합우주 핸들이 정상적으로 작동하며, HackersPub이나 Mastodon 같은 다른 연합우주 SNS에서 이 주소로 검색하면 내 계정을 찾고 팔로우할 수 있다.
5. 구현 후 테스트
구현을 완료했으니 이제 실제로 연합우주에서 작동하는지 테스트해보자. 기존에 연합우주 플랫폼인 Hackers.pub에 가입되어 있었기 때문에, 이를 활용해 상호작용을 테스트했다.
계정 검색 테스트
먼저 해커스펍에서 내가 만든 계정인 @crohasang@crohasang.com을 검색해보았다.

계정 검색이 정상적으로 되는 것을 확인할 수 있었다. 이는 WebFinger 프로토콜이 제대로 작동하고 있으며, ActivityPub 프로필이 연합우주에 올바르게 등록되었다는 뜻이다.
팔로우 관계 테스트
이제 실제로 팔로우 기능이 작동하는지 확인해보았다. 내 웹 사이트 계정(@crohasang@crohasang.com)과 해커스펍 계정(@crohasang@hackers.pub)을 서로 맞팔로우했다. 그리고 내 웹 사이트에서 팔로워와 팔로잉 목록이 제대로 표시되는지 확인해보았다.

팔로잉과 팔로워가 모두 정상적으로 표시되는 것을 확인할 수 있었다. 이는 팔로우 요청을 보낼 때 outbox.service.ts가 Follow 액티비티를 전송하고, 상대방 서버로부터 받은 Accept 응답을 inbox.service.ts가 처리하여 데이터베이스에 저장했다는 뜻이다.
타임라인 연동 테스트
마지막으로 타임라인 기능을 테스트했다. 해커스펍 계정으로 글을 작성한 후, 내 웹 사이트의 타임라인에 해당 글이 나타나는지 확인해보았다.

해커스펍에서 작성한 글이 웹 사이트 타임라인에서 정상적으로 표시되는 것을 확인할 수 있었다! 이는 following.service.ts가 팔로잉 중인 계정들의 최신 액티비티를 수집하고, 이를 통합 타임라인으로 제공하는 기능이 제대로 작동한다는 뜻이다.
이렇게 Fedify와 NestJS를 활용해 개인 사이트를 연합우주에 연결했다.
이번 작업을 통해 ActivityPub 프로토콜의 핵심 개념들을 이해할 수 있었고, Fedify 프레임워크를 실제 프로젝트에 적용하는 경험을 할 수 있었다. 또한 NestJS에서 기능별로 모듈과 서비스를 나누고, TypeORM으로 데이터베이스 마이그레이션을 관리하는 방법도 익힐 수 있었다. 아직 좋아요 같은 구현하지 못한 기능들이 남아있지만, 앞으로 천천히 추가해볼 예정이다.
'Project > crohasang_page' 카테고리의 다른 글
| NestJS 백엔드와 EC2 인스턴스로 개인 사이트에 RDS 연결하기 (0) | 2025.11.15 |
|---|---|
| Next.js + MySQL을 활용해서 개인 웹 사이트에 글 포스팅하기 (0) | 2025.10.26 |