정말 감사하게도 2025 오픈소스 컨트리뷰션 참여형 프로젝트 fedify의 멘티로 참여하게 되었다. 참여가 결정된 날, 멘토님으로부터 fedify에 대한 설명이 담긴 메일을 받았다. 첨부된 링크에는 fedify에 대한 설명과 함께 fedify를 활용하여 마이크로블로그를 만드는 튜토리얼을 볼 수 있었다.
🔗 https://hackers.pub/@hongminhee/2025/fedify-tutorial-ko
fedify에 대한 사전지식이 많지 않았기 때문에 잘 따라갈 수 있을까 걱정했지만 설명이 매우 자세하게 써져있었기 때문에 어렵지 않게 따라갈 수 있었다. 튜토리얼을 진행하며 fedify와 연합우주 그리고 개발에 필요한 내용들을 알 수 있게 되었고, 이 지식들은 앞으로 오픈소스 컨트리뷰션을 할 때 많은 도움이 될 것 같다.
튜토리얼을 진행하며 새롭게 알게 된 내용들이 많은데, 그 중 일부를 짧게 정리해보았다.
1. 웹 프레임워크 Hono
튜토리얼을 진행할 때 웹 프레임워크 Hono를 활용했는데, 제너릭 타입을 선언할 때 Hono의 FC를 활용했다.
import type { FC } from "hono/jsx";
export interface FollowerListProps {
followers: Actor[];
}
export const FollowerList: FC<FollowerListProps> = ({ followers }) => (
<>
<h2>Followers</h2>
<ul>
{followers.map((follower) => (
<li key={follower.id}>
<ActorLink actor={follower} />
</li>
))}
</ul>
</>
);
위와 같은 방식으로 타입 인자들을 FC를 활용하여 설정할 수 있다.
아마 FC를 쓰지 않았다면, 함수 인자의 props 타입을 직접 지정했을 것이다.
→ ( { followers }: FollowerListProps) )
웹 프레임워크 Hono를 사용하면 좋은 점이 무엇인가 찾아보니, 다음과 같은 장점이 있다고 한다.
- 초경량, 초고속 런타임 지원 → Cloudflare Workers, Deno, Bun, Node.js 등 다양한 환경에서 거의 동일한 코드를 실행 가능
- 서버리스/엣지 친화적 설계 → 서버리스 환경에 최적화되어 배포, 실행, 스케일링이 매우 쉽고 빠름
- 미들웨어와 라우팅 → Express 스타일의 미들웨어와 라우팅을 매우 가볍고 빠르게 구현 가능
- TypeScript 친화적
2. sqlite3
지금까지 맥북을 사용하면서 sqlite3가 맥북에 내장되어 있는 줄은 몰랐는데, 설치를 하지 않아도 sqlite3를 사용할 수 있었다!
스키마 선언은 src/schema.sql에서 진행했다.
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
username TEXT NOT NULL UNIQUE CHECK (trim(lower(username)) = username
AND username <> ''
AND length(username) <= 50)
);
위 스키마에서 AND username <> '' 은 username 값이 빈 문자열이 아니어야 한다는 조건이다.
(<> → 같지 않다.)
그리고 터미널에서 ‘sqlite3 microblog.sqlite3 < src/schema.sql’ 명령어를 통하여 데이터베이스 파일을 생성했다.
이 때 ‘< src/schema.sql’은
리다이렉션 연산자(<)를 사용해, src/schema.sql 파일에 저장된 SQL 명령문들을 표준 입력으로 전달 → schema.sql 파일 안의 모든 SQL 명령(예: 테이블 생성, 인덱스 생성 등)이 차례로 실행된다는 뜻이다.
3. db.pragma
src/db.ts에서 db.pragma 함수를 활용한다. pragma는 SQLite에서만 지원하는 특별한 명령어로, 데이터베이스의 동작 방식이나 내부 설정을 조회/변경할 때 사용한다고 한다.
import Database from "better-sqlite3";
const db = new Database("microblog.sqlite3");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
export default db;
튜토리얼에서는 다음과 같이
‘journal_mode = WAL’은 로그 선행 기입 모드(데이터베이스의 변경사항을 먼저 로그 파일에 기록한 뒤, 나중에 실제 데이터 파일에 반영하는 방식)를 채택할 때 활용하고,
‘foreign_keys = ON’은 외래 키 제약 조건을 검사할 때 활용한다고 한다.
외래 키 제약 조건이 무엇인지 잘 몰라서 더 찾아보았다.
→ 한 테이블의 특정 컬럼(외래 키)이 다른 테이블의 기본 키(또는 유니크 키)를 참조하도록 하여, 존재하지 않는 값을 참조하는 것을 방지
예시(by perplexity)
CREATE TABLE artist (
artistid INTEGER PRIMARY KEY,
artistname TEXT
);
CREATE TABLE track (
trackid INTEGER,
trackname TEXT,
trackartist INTEGER,
FOREIGN KEY(trackartist) REFERENCES artist(artistid)
);
track 테이블의 trackartist 칼럼은 artist 테이블의 artistId를 참조
→ track 테이블에 값을 넣을 때, 해당 trackartist 값이 반드시 artist 테이블에 존재해야
4. db.prepare
db.prepare("INSERT INTO users (username) VALUES (?)").run(username);
튜토리얼의 많은 부분에서 db.parpare()를 사용하는데, .prepare()가 무슨 역할을 하는지 궁금했다.
db.prepare()는 better-sqlite3 패키지에서 제공하는 메서드인데,
.prepqre(sql)은 주어진 SQL 쿼리를 준비된 문장 객체로 컴파일하고, 이 객체는 .get(), .run(), .all(), .iterate() 같은 메서드를 통해 실행된다.
참고로
.get() → 하나의 행 조회,
.all() → 여러 행을 배열로 조회,
.run() → INSERT, UPDATE, DELETE 등 쓰기,
.iterate() → 반복자로 결과 처리를 수행한다.
prepare()는 SQL을 한 번만 파싱하고 컴파일해두어서 다음 실행 시 더 빠르게 수행이 된다.
그리고 파라미터 바인딩을 사용하여 SQL Injection을 방지할 수 있다. 실제로 튜토리얼의 많은 부분에서 SQL에 ? 표시가 있길래 무슨 뜻인지 궁금했는데, 파라미터 바인딩을 사용한 것이다.
5. 파라미터 바인딩
users 테이블에서 username 컬럼의 값이 특정 username과 일치하는 행을 조회하는 SQL문을 작성한다고 가정해보자.
파라미터 바인딩을 쓰지 않는다면,
const username = "'; DROP TABLE users; --";
const sql = "SELECT * FROM users WHERE username = '" + username + "'";
// username 값이 그대로 SQL문에 들어가 위험함 (SQL Injection 공격 위험)
파라미터 바인딩을 쓴다면,
const sql = "SELECT * FROM users WHERE username = ?";
db.prepare(sql).get(username);
// ? 위치에 username 값을 안전하게 "끼워넣음"
? 자리가 파라미터 바인딩이고, 값은 SQL문과 분리해서 DB 엔진이 따로 처리한다.
즉, 파라미터 바인딩은 쿼리문에서 값이 들어갈 자리를 미리 정해놓고, 실행할 때 값을 따로 넘기는 방법이다.
사용자가 입력한 값으로 인해 SQL 코드로 오동작되는 걸 막을 수 있고, DB가 쿼리와 값을 분리해서 처리하므로 쿼리 구조를 망치지 않기 때문에 효율적이다.
6. 아무리 핸들을 검색해도 결과가 뜨지 않았다
액터를 만들고 ‘fedify tunnel 8000’ 명령어를 통해 인터넷에 로컬 서버를 노출하여 테스트할 수 있다. 그리고 ‘터미널에 나오는 URL/users/아이디’로 들어가면 연합우주 핸들이 나오는데, 그 핸들을 복사해서 마스토돈에 검색하면 액터가 보여야된다.
하지만 보이지 않았다. 명령어를 강제 종료하고 다시 실행하기를 몇번을 반복해도 결과는 다르지 않았다. 무엇이 문제일까. 고민을 거듭하다 디스코드에 질문을 올렸다.
질문을 올리고 얼마 지나지 않아 멘토님께서 문제 해결을 위해 와주셨다. 대화가 몇 번 오간 뒤, 마스토돈에 생성한 핸들이 검색이 된는데 혹시 마스토돈 회원가입을 하지 않았는지 여쭤보셨다.
허무하게도 내가 마스토돈에 로그인을 하지 않은채 검색을 했기 때문에 검색 결과가 뜨지 않았던 것이었다. 팝업이라도 하나 띄어져 있었다면 좋았을텐데! 그래도 해결되어서 다행이었다.
7. 튜토리얼을 끝마치고
차근차근 튜토리얼을 보면서 코드를 작성한 결과 Fedify를 활용한 간단한 마이크로블로그를 만들어낼 수 있었다.
🔗 https://github.com/crohasang/fedify-tutorial
튜토리얼을 진행하며 만들어낸 마이크로블로그의 플로우를 AI를 활용하여 요약해보았다.
1. 프로젝트 시작 및 기반 환경
- 서버 구동: package.json 스크립트 실행 → src/index.ts에서 Hono 서버 시작.
- DB 연결: src/db.ts에서 microblog.sqlite3 (DB 파일) 연결 및 초기화.
- 스키마 정의: src/schema.sql로 DB 테이블(users, actors, posts, follows, keys) 구조화. src/schema.ts로 타입스크립트 인터페이스 제공.
- 로깅: src/logging.ts에서 애플리케이션 로그 설정.
2. 사용자 및 웹 인터페이스
- 초기 설정: 사용자 없을 시 /setup으로 리다이렉트 → src/views.tsx의 SetupForm 렌더링.
- 계정 생성 (POST /setup): src/app.tsx 처리 → 사용자 이름 검증 → users, actors 테이블에 정보 저장.
- src/federation.ts를 통해 ActivityPub 액터 URI 및 암호화 키 쌍(keys 테이블) 생성/저장.
- 메인 피드 (GET /): src/app.tsx에서 사용자 정보, 팔로잉 게시물 조회 → src/views.tsx의 Home 렌더링.
- 프로필/목록 조회: src/app.tsx의 라우트(/users/:username, /users/:username/followers, /users/:username/following, /users/:username/posts/:id) 처리 → DB 조회 → src/views.tsx의 관련 컴포넌트(Profile, FollowerList, FollowingList, PostPage, PostList) 렌더링.
3. 게시물 작성 및 Fediverse 전파
- 게시물 작성 (POST /users/:username/posts): src/app.tsx 처리 → 내용 검증 → DB posts 테이블에 저장.
- ActivityPub Create: src/app.tsx에서 src/federation.ts의 fedi 인스턴스 사용 → 게시물(Note)에 대한 Create 액티비티 생성 및 팔로워들에게 전송.
- 외부 서버 수신: 외부 Fediverse 서버들이 Create 액티비티를 받아 게시물을 피드에 반영.
4. 팔로우/언팔로우 및 Fediverse 연동
- 팔로우 요청 (POST /users/:username/following): src/app.tsx 처리 → 입력된 액터 조회 → src/federation.ts의 fedi 인스턴스 사용 → Follow 액티비티 생성 및 대상 액터에게 전송.
- 외부 Follow 수신: src/federation.ts 인박스 리스너 → Follow 액티비티 수신 → DB actors, follows 테이블에 팔로워 정보 및 관계 저장 → Accept 액티비티로 응답.
- Undo 수신: src/federation.ts 인박스 리스너 → Undo 액티비티 수신 → follows 테이블에서 관계 제거.
- Accept 수신: src/federation.ts 인박스 리스너 → Accept 액티비티 수신 → follows 테이블에 관계 기록 (팔로우 수락).
- 외부 Create 수신: src/federation.ts 인박스 리스너 → Create 액티비티 수신 (게시물 Note 객체) → DB posts 테이블에 게시물 저장.
이번 튜토리얼을 통해 ActivityPub, Fediverse, Fedify가 어떻게 동작하는지 간단하게나마 파악할 수 있었다. Fedify 프레임워크를 통해 ActivityPub 연동을 하면 간단하게 Fediverse의 일원이 될 수 있다. 튜토리얼을 따라하며 Actor를 생성하고, Dispatcher를 통해 Actor의 프로필을 외부에 제공하고, 다른 Fediverse actor의 팔로우 요청이나 게시물 활동 등을 수신하고 처리하는 흐름을 익힐 수 있었다.
이제 Fedify 기여를 시작할 차례다. 오픈소스에 기여하는 것은 처음이라 낯설지만, 아무 준비 없이 들이받는게 아니라 OSSCA라는 프로그램 안에서 진행하므로 크게 어렵진 않을 것 같다. 시작해보자.
'Fedify' 카테고리의 다른 글
[Fedify] 첫 오픈소스 기여를 해보자 (0) | 2025.07.20 |
---|