洪 民憙 (Hong Minhee)'s avatar

洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · 923 following · 1194 followers

An intersectionalist, feminist, and socialist guy living in Seoul (UTC+09:00). @tokolovesme's spouse. Who's behind @fedify, @hollo, and @botkit. Write some free software in , , , & . They/them.

서울에 사는 交叉女性主義者이자 社會主義者. 金剛兔(@tokolovesme)의 配偶者. @fedify, @hollo, @botkit 메인테이너. , , , 等으로 自由 소프트웨어 만듦.

()

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

Hello, I'm an open source software engineer in my late 30s living in , , and an avid advocate of and the .

I'm the creator of @fedify, an server framework in , @hollo, an ActivityPub-enabled microblogging software for single users, and @botkit, a simple ActivityPub bot framework.

I'm also very interested in East Asian languages (so-called ) and . Feel free to talk to me in , (), or (), or even in Literary Chinese (, )!

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post

安寧(안녕)하세요, 저는 서울에 살고 있는 30() 後半(후반) 오픈 소스 소프트웨어 엔지니어이며, 自由(자유)·오픈 소스 소프트웨어와 聯合宇宙(연합우주)(fediverse)의 熱烈(열렬)支持者(지지자)입니다.

저는 TypeScript() ActivityPub 서버 프레임워크인 @fedify 프로젝트와 싱글 유저() ActivityPub 마이크로블로그인 @hollo 프로젝트와 ActivityPub 봇 프레임워크인 @botkit 프로젝트의 製作者(제작자)이기도 합니다.

저는 ()아시아 言語(언어)(이른바 )와 유니코드에도 關心(관심)이 많습니다. 聯合宇宙(연합우주)에서는 國漢文混用體(국한문 혼용체)를 쓰고 있어요! 제게 韓國語(한국어)英語(영어), 日本語(일본어)로 말을 걸어주세요. (아니면, 漢文(한문)으로도!)

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post

こんにちは、私はソウルに住んでいる30代後半のオープンソースソフトウェアエンジニアで、自由・オープンソースソフトウェアとフェディバースの熱烈な支持者です。名前は洪 民憙ホン・ミンヒです。

私はTypeScript用のActivityPubサーバーフレームワークである「@fedify」と、ActivityPubをサポートする1人用マイクロブログである 「@hollo」と、ActivityPubのボットを作成する為のシンプルなフレームワークである「@botkit」の作者でもあります。

私は東アジア言語(いわゆるCJK)とUnicodeにも興味が多いです。日本語、英語、韓国語で話しかけてください。(または、漢文でも!)

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to Jaeyeol Lee (a.k.a. kodingwarrior) :vim:'s post

@kodingwarrior 아… 마이그레이션 도구라도 만들어 놓든가 하겠습니다. 😭

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to Jaeyeol Lee (a.k.a. kodingwarrior) :vim:'s post

@kodingwarrior 어떤가요? 효험은 좀 있는 것 같나요?

ayo's avatar
ayo

@ayo@ayco.io

The new fedidb is being built with @nuxt :)

github.com/fedidb/fedidb-nuxt

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

Hackers' Pub、ユーザー名変更の柔軟性を導入:フェディバースの常識を打ち破る

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


Hackers' Pub is a community-focused platform where programmers and technology enthusiasts share knowledge and experiences. As an ActivityPub-enabled social network, it allows users to connect with others across the broader fediverse ecosystem, bringing technical discussions and insights directly to followers' feeds.

In the fediverse landscape, your username is typically set in stone once chosen. Most ActivityPub-powered platforms like Mastodon, Pleroma, and others enforce this permanence as a fundamental design principle. However, Hackers' Pub is charting a different course with a more flexible approach to digital identity.

One-Time Username Change: Freedom with Responsibility

Unlike most fediverse platforms, Hackers' Pub now allows users to change their username (the part before the @ in your Fediverse handle) exactly once during the lifetime of their account. This policy acknowledges that people grow, interests evolve, and the username that seemed perfect when you joined might not represent who you are today.

This one-time change limit strikes a careful balance—offering flexibility while maintaining the stability and reliability that's essential for a federated network.

Username Recycling: New Opportunities

When you change your username on Hackers' Pub, your previous username becomes available for other users to claim. This recycling mechanism creates new opportunities for meaningful usernames to find their most fitting owners, rather than remaining permanently locked to accounts that no longer use them.

For newcomers to the platform, this means a wider selection of desirable usernames might become available over time—something virtually unheard of in the traditional fediverse ecosystem.

Worried about broken links after changing your username? Hackers' Pub has implemented a thoughtful solution. All permalinks containing your original username will continue to function until someone else claims that username. This approach helps preserve the web of connections and conversations that make the fediverse valuable.

This temporary preservation period gives your connections time to adjust to your new identity while preventing immediate link rot across the federation.

The Technical Foundation: ActivityPub Actor URIs

What enables Hackers' Pub to offer username changes while other fediverse platforms can't? The answer lies in how actor identities are implemented at the protocol level.

Hackers' Pub uses UUID-based actor URIs that don't contain the username. For example, a user with handle @hongminhee has an underlying ActivityPub actor URI that looks like https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c. Since the username isn't part of this permanent identifier, it can be changed without breaking federation connections.

This contrasts sharply with platforms like Mastodon, where a user @hongminhee has an actor URI of https://mastodon.social/users/hongminhee. With the username embedded directly in the URI, changing it would break all federation connections, which is why these platforms don't allow username changes.

This architectural choice gives Hackers' Pub the technical flexibility to implement username changes while maintaining account continuity across the fediverse.

GitHub-Inspired Approach

Those familiar with GitHub might recognize this model—Hackers' Pub has adapted GitHub's username change policy for the fediverse context. This approach brings the best of both worlds: the option for identity evolution from centralized platforms and the federation benefits of the fediverse.

What This Means for Users

For Hackers' Pub users, this policy offers a significant advantage over other fediverse instances:

  • You can correct an unfortunate username choice
  • Your online identity can evolve as you do
  • Your content history remains intact during the transition
  • You maintain your social connections despite the change

The Future of Fediverse Identity

Hackers' Pub's username policy represents an interesting experiment in the fediverse—testing whether more flexible identity management can coexist with the stability needed for federation. If successful, we might see other platforms adopt similar approaches, creating a more adaptable yet still interconnected social web.

For now, users should consider this policy a compelling reason to choose Hackers' Pub as their fediverse home, especially if username flexibility matters to their online experience.


Hackers' Pub is currently in invitation-only beta. If you're interested in trying out the platform and its unique username policy, please leave your email address in the comments below. We'll add you to the allowlist, enabling you to sign up directly on the website. Note that this doesn't involve sending invitation emails—your address will simply be approved for registration when you visit the signup page.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

BlueskyはXの優れた代替となりうるが、フェディバースの代替ではない

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


최근 X(구 Twitter)를 떠나는 사람들이 늘면서 Bluesky에 대한 관심이 뜨겁습니다. Bluesky는 깔끔한 인터페이스와 과거 Twitter와 유사한 사용자 경험을 제공하며, 신뢰할 수 있는 이탈(credible exit)이라는 매력적인 개념을 내세워 X의 유력한 대안으로 떠오르고 있습니다. 하지만 Bluesky와 그 기반 프로토콜인 AT Protocol을 연합우주(fediverse)의 대안으로 보기에는 근본적인 차이가 존재합니다. 이 글에서는 Christine Lemmer-Webber 씨(@cwebber)의 날카로운 분석(〈Bluesky는 실제로 얼마나 탈중앙화 되어 있나〉 및 〈답장: 답장: Bluesky와 탈중앙화〉)을 바탕으로, Bryan Newbold 씨(@bnewbold)의 반론(〈Bluesky와 탈중앙화에 대한 답변〉)을 충분히 고려하면서 Bluesky가 어째서 X의 대안은 될 수 있어도 연합우주의 대안은 될 수 없는지 이야기를 풀어볼까 합니다.

메시지 전달 對 공유 힙: 근본적인 설계 차이

Bluesky와 연합우주의 가장 큰 차이점 중 하나는 설계입니다. 연합우주는 이메일이나 XMPP와 유사한 메시지 전달(message passing) 방식을 채택하고 있습니다. 이는 특정 수신자에게 메시지를 직접 전달하는 방식으로, 효율성이 높습니다. 예를 들어, 수많은 서버 중 단 몇 곳의 사용자만 특정 메시지에 관심을 있다면 해당 서버에만 메시지를 전달하면 됩니다. 비유하자면, 철수가 영희에게 편지를 보내려면 직접 영희의 집으로 편지를 보내고, 영희가 회신하고 싶으면 직접 철수에게 회신하는 것과 같은 방식입니다.

반면, Bluesky는 공유 힙(shared heap) 방식을 사용합니다. 이는 메시지를 특정 수신자에게 직접 보내는 대신, 모든 메시지를 중앙의 “릴레이”라는 곳에 저장하고, 관심 있는 사용자가 릴레이에서 자신에게 필요한 정보를 필터링하는 방식입니다. 이는 마치 모든 편지가 하나의 거대한 우체국(릴레이)에 쌓이고, 각자가 이 우체국에 방문하여 자신에게 관련된 편지를 직접 찾아야 하는 것과 같습니다. 이런 방식에서는 메시지가 직접 전달되지 않기 때문에, 답글이 어떤 메시지에 대한 것인지 파악하려면 모든 가능한 메시지를 알고 있어야 합니다.

이 설계는 데이터와 색인을 분리하여 유연성을 제공한다는 주장도 있지만, 필연적으로 대규모 중앙 집권화된 릴레이에 의존하게 되어 탈중앙화의 이상과는 거리가 멀어진다는 한계가 있습니다.

결국 Bluesky가 공유 힙 방식을 채택하고 중앙 집권화된 릴레이에 의존하게 되는 데에는 운영 비용이라는 현실적인 이유가 크게 작용합니다. Christine Lemmer-Webber 씨의 분석에 따르면, Bluesky에서 전체 네트워크 기록을 저장하는 릴레이를 운영하는 데에는 상당한 스토리지를 요구하며, 이는 빠르게 증가하고 있습니다. 2024년 7월에는 약 1TB의 저장 공간이 필요했지만, 불과 4개월 후인 11월에는 약 5TB로 증가했습니다. 상업용 호스팅 서비스 기준으로 이는 연간 수만 달러(약 $55,000)에 달하는 비용이 발생할 수 있습니다.

반면, 연합우주에서는 개인이나 소규모 단체가 Raspberry Pi와 같은 저렴한 장비로도 GoToSocial과 같은 소프트웨어를 실행하여 독립적인 노드를 운영할 수 있습니다. 물론 대규모 연합우주 인스턴스는 더 많은 비용이 들겠지만, Bluesky의 전체 릴레이 운영 비용과는 비교하기 어려울 정도로 저렴합니다. 이처럼 운영 비용의 현격한 차이는 Bluesky가 분산된 구조를 채택하기 어렵게 만들고, 결국 중앙 집권화된 릴레이에 의존하게 만드는 주요 원인이라고 볼 수 있습니다.

전역 뷰에 대한 집착과 중앙 집권화의 심화

Bluesky는 댓글 누락과 같은 문제를 피하기 위해 네트워크 전체의 일관된 전역 뷰를 유지하는 데 집중하는 것으로 보입니다. 이러한 목표는 사용자 경험 측면에서 긍정적일 수 있지만, 필연적으로 중앙 집권화를 야기합니다. 대표적인 예가 차단 목록의 전체 공개입니다. 네트워크 전체의 일관성을 유지하기 위해 누가 누구를 차단했는지 모든 앱뷰가 알아야 하므로, 차단 정보가 공개되는 것입니다.

이는 개인 정보 보호 측면에서 심각한 우려를 낳을 수 있습니다. 단순히 누군가의 게시물을 보고 차단된 사람을 추측하는 것과, 네트워크에 “J. K. Rowling[1]을 차단한 모든 사람”을 직접 질의할 수 있는 것 사이에는 큰 차이가 있습니다. 실제로 ActivityPub 개발 과정에서는 이런 문제를 고려하여 서버 간에 차단 활동을 전달하지 않도록 명시적으로 설계했습니다. 이는 차단한 사람이 차단당한 사람의 보복을 받을 위험을 줄이기 위함입니다.

반면 연합우주에서는 각 서버가 독립적으로 차단 정책을 시행하며, 사용자에게 더 많은 자율성을 제공합니다.

AT Protocol과 개방형 표준으로서의 ActivityPub

연합우주의 핵심 프로토콜인 ActivityPub은 W3C의 채택 권고안으로, 개방형 표준입니다. 이는 누구나 자유롭게 구현하고 사용할 수 있으며, 다양한 소프트웨어 간의 상호 운용성을 보장합니다. 현재 페디버스 커뮤니티는 FEP를 중심으로 활발하게 프로토콜을 개선하고 발전시켜 나가고 있습니다. 반면, Bluesky의 AT Protocol은 아직 특정 사기업에 의해 주도되고 있으며, 개방형 표준으로서의 지위는 아직 확립되지 않았습니다. 이는 페디버스가 가진 확장성과 지속 가능성 측면에서 중요한 차이점이라고 할 수 있습니다.

DM의 중앙화

Bluesky는 콘텐츠 주소 지정이나 이동 가능한 아이덴티티와 같은 탈중앙화 요소를 도입했지만, DM은 완전히 중앙화되어 있습니다. 사용자가 어떤 PDS를 사용하든, 어떤 릴레이를 사용하든 상관없이 모든 DM은 Bluesky 회사를 통해 전송됩니다.

이는 Bluesky가 아직 기능적으로 완전한 Twitter 대체품이 되기 위해 속도를 우선시했다는 증거입니다. Bluesky는 이 DM 시스템이 장기적인 솔루션이 아니라고 밝혔지만, 대부분의 사용자들은 이 사실을 인지하지 못하고 있으며 DM도 AT Protocol의 다른 기능처럼 작동한다고 가정합니다.

이러한 중앙화된 DM 구현은 “신뢰할 수 있는 이탈”이라는 Bluesky의 핵심 가치와도 모순됩니다. 만약 Bluesky社가 적대적인 인수나 정책 변경을 겪게 된다면, 사용자들의 개인 대화는 완전히 회사의 통제 하에 남게 됩니다.

이동 가능한 아이덴티티와 DID: Bluesky 방식의 한계

Bluesky는 이동 가능한 아이덴티티(portable identity)를 핵심적인 장점 중 하나로 내세우며, 이를 위해 DIDs, 즉 분산 식별자를 활용합니다. 이는 사용자가 자신의 계정과 데이터를 다른 플랫폼으로 쉽게 이동할 수 있도록 하는 중요한 기능입니다. 하지만 Christine Lemmer-Webber는 AT Protocol이 채택한 did:webdid:plc 방식이 여전히 DNS와 Bluesky社가 관리하는 중앙 집권화된 PLC 레지스트리에 의존하고 있어 완전한 사용자 통제하의 독립적인 아이덴티티를 제공하는지 의문을 제기합니다.

더 놀라운 점은 Bluesky社가 초기에 모든 계정에 대해 동일한 rotationKeys를 사용했다는 사실입니다. 이는 클라우드 HSM 제품이 키별로 비용을 청구해서 각 사용자에게 고유한 키를 제공하는 것이 금전적으로 비용이 많이 들었기 때문이라고 합니다. 이러한 접근 방식은 DIDs 시스템을 구축하는 근본적인 목표와 모순되는 것으로 보입니다.

중요한 점은 DIDs 기술 자체가 탈중앙화된 아이덴티티를 위한 잠재력을 가지고 있음에도, Bluesky와 AT Protocol이 채택한 특정 방식이 중앙 집권화된 요소에 의존한다는 것입니다. 블록체인 기반의 DIDs와 같은 진정으로 탈중앙화된 방식도 존재하지만, AT Protocol은 비교적 구현이 쉬운 did:webdid:plc를 선택했습니다. 따라서 사용자가 Bluesky 생태계를 벗어나 자신의 아이덴티티를 완전히 독립적으로 관리하고자 할 때 제약이 발생할 수 있습니다.

또한 현재 시스템에서는 Bluesky社가 사용자의 키를 대신 관리하고 있어, 사용자가 현재는 Bluesky社를 신뢰하더라도 미래에 신뢰하지 않게 된 경우에도 여전히 회사에 의존해야 합니다. Bluesky社가 사용자를 대신하여 이동을 수행하도록 신뢰해야 하며, 심지어 Bluesky社가 사용자에게 향후 신원 정보를 제어할 권한을 위임하더라도 Bluesky社는 항상 해당 사용자의 키를 통제할 것입니다.

한편, 연합우주에서는 이미 노마딕 아이덴티티(nomadic identity)라는 개념을 통해 이동 가능한 아이덴티티에 대한 논의와 연구가 활발하게 진행되어 왔습니다. 이는 단순히 계정을 이전하는 것을 넘어, 사용자의 데이터와 관계, 심지어 평판까지도 자유롭게 이동할 수 있도록 하는 더 포괄적인 개념입니다. 《We Distribute》에 실린 기사 〈오, Zot! ActivityPub에 노마딕 아이덴티티가 도입된다〉에 소개된 Zot 프로토콜과 같은 기술은 이미 연합우주 안에서 이러한 노마딕 아이덴티티를 구현하기 위한 메커니즘을 제공하고 있습니다. 또한, FEP-ef61와 같은 제안을 통해 ActivityPub 자체를 개선하여 더 나은 이동 가능한 아이덴티티 기능을 추가하려는 노력도 진행 중입니다.

그래서, 결론은?

결론적으로, Bluesky는 사용자 친화적인 인터페이스와 신뢰할 수 있는 이탈 기능을 통해 X의 훌륭한 대안이 될 수 있습니다. Bluesky는 콘텐츠 주소 지정 방식을 통해 노드가 다운되더라도 콘텐츠가 살아남을 수 있게 하는 등 연합우주가 아직 충분히 활용하지 못하는 몇 가지 강점도 가지고 있습니다.

하지만 중앙 집권화된 설계, 전역 뷰에 대한 집착으로 인한 부작용, 개방형 표준으로서의 한계, DM의 중앙화, 그리고 이동 가능한 아이덴티티 구현의 제한점 등 여러 측면에서 연합우주의 대안으로 보기는 어렵습니다. 연합우주는 메시지 전달 방식의 분산된 아키텍처, 낮은 참여 장벽, 개방형 표준 기반의 활발한 커뮤니티 개발, 그리고 사용자에게 더 많은 자율성과 통제권을 제공하는 철학을 바탕으로 구축된, 근본적으로 다른 종류의 탈중앙화 소셜 네트워크입니다.

또한, Bluesky社가 벤처 캐피털 자금을 확보함에 따라 “조직은 미래의 적이다”라는 그들의 자체 인식에도 불구하고, 투자자 수익과 플랫폼 성장이라는 상업적 압력이 진정한 탈중앙화 추구보다 우선시될 위험이 있습니다. 특히 유료 계정과 광고가 도입되면서 이러한 우려는 더욱 커질 수 있습니다.

따라서 Bluesky는 X를 대체할 수 있을지 모르지만, 연합우주가 제공하는 탈중앙화된 가치와 경험을 대체하기는 어려울 것이라고 생각합니다. 두 시스템은 근본적으로 다른 목표와 설계 철학을 가지고 있으며, 이상적으로는 서로를 보완하는 방향으로 발전해 나갈 수 있을 것입니다.


  1. 판타지 소설 시리즈 《해리 포터》의 작가. ↩︎

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

DIYドラマを捨てよう:なぜActivityPubを一から構築するのではなくFedifyを使うべきなのか?

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


So, you're captivated by the fediverse—the decentralized social web powered by protocols like ActivityPub. Maybe you're dreaming of building the next great federated app, a unique space connected to Mastodon, Lemmy, Pixelfed, and more. The temptation to dive deep and implement ActivityPub yourself, from the ground up, is strong. Total control, right? Understanding every byte? Sounds cool!

But hold on a sec. Before you embark on that epic quest, let's talk reality. Implementing ActivityPub correctly isn't just one task; it's like juggling several complex standards while riding a unicycle… blindfolded. It’s hard.

That's where Fedify comes in. It's a TypeScript framework designed to handle the gnarliest parts of ActivityPub development, letting you focus on what makes your app special, not reinventing the federation wheel.

This post will break down the common headaches of DIY ActivityPub implementation and show how Fedify acts as the super-powered pain reliever, starting with the very foundation of how data is represented.

Challenge #1: Data Modeling—Speaking ActivityStreams & JSON-LD Fluently

At its core, ActivityPub relies on the ActivityStreams 2.0 vocabulary to describe actions and objects, and it uses JSON-LD as the syntax to encode this vocabulary. While powerful, this combination introduces significant complexity right from the start.

First, understanding and correctly using the vast ActivityStreams vocabulary itself is a hurdle. You need to model everything—posts (Note, Article), profiles (Person, Organization), actions (Create, Follow, Like, Announce)—using the precise terms and properties defined in the specification. Manual JSON construction is tedious and prone to errors.

Second, JSON-LD, the encoding layer, has specific rules that make direct JSON manipulation surprisingly tricky:

  • Missing vs. Empty Array: In JSON-LD, a property being absent is often semantically identical to it being present with an empty array. Your application logic needs to treat these cases equally when checking for values. For example, these two Note objects mean the same thing regarding the name property:
    // No name property
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ""
    }
    // Equivalent to:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "name": [],
      "content": ""
    }
  • Single Value vs. Array: Similarly, a property holding a single value directly is often equivalent to it holding a single-element array containing that value. Your code must anticipate both representations for the same meaning, like for the content property here:
    // Single value
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": "Hello"
    }
    // Equivalent to:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ["Hello"]
    }
  • Object Reference vs. Embedded Object: Properties can contain either the full JSON-LD object embedded directly or just a URI string referencing that object. Your application needs to be prepared to fetch the object's data if only a URI is given (a process called dereferencing). These two Announce activities are semantically equivalent (assuming the URIs resolve correctly):
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // Embedded objects:
      "actor": {
        "type": "Person",
        "id": "http://sally.example.org/",
        "name": "Sally"
      },
      "object": {
        "type": "Arrive",
        "id": "https://sally.example.com/arrive",
        /* ... */
      }
    }
    // Equivalent to:
    {
      "@context":
      "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // URI references:
      "actor": "http://sally.example.org/",
      "object": "https://sally.example.com/arrive"
    }

Attempting to manually handle all these vocabulary rules and JSON-LD variations consistently across your application inevitably leads to verbose, complex, and fragile code, ripe for subtle bugs that break federation.

Fedify tackles this entire data modeling challenge with its comprehensive, type-safe Activity Vocabulary API. It provides TypeScript classes for ActivityStreams types and common extensions, giving you autocompletion and compile-time safety. Crucially, these classes internally manage all the tricky JSON-LD nuances. Fedify's property accessors present a consistent interface—non-functional properties (like tags) always return arrays, functional properties (like content) always return single values or null. It handles object references versus embedded objects seamlessly through dereferencing accessors (like activity.getActor()) which automatically fetch remote objects via URI when needed—a feature known as property hydration. With Fedify, you work with a clean, predictable TypeScript API, letting the framework handle the messy details of AS vocabulary and JSON-LD encoding.

Challenge #2: Discovery & Identity—Finding Your Actors

Once you can model data, you need to make your actors discoverable. This primarily involves the WebFinger protocol (RFC 7033). You'd need to build a server endpoint at /.well-known/webfinger capable of parsing resource queries (like acct: URIs), validating the requested domain against your server, and responding with a precisely formatted JSON Resource Descriptor (JRD). This JRD must include specific links, like a self link pointing to the actor's ActivityPub ID using the correct media type. Getting any part of this wrong can make your actors invisible.

Fedify simplifies this significantly. It automatically handles WebFinger requests based on the actor information you provide through its setActorDispatcher() method. Fedify generates the correct JRD response. If you need more advanced control, like mapping user-facing handles to internal identifiers, you can easily register mapHandle() or mapAlias() callbacks. You focus on defining your actors; Fedify handles making them discoverable.

// Example: Define how to find actors
federation.setActorDispatcher(
  "/users/{username}",
  async (ctx, username) => { /* ... */ }
);
// Now GET /.well-known/webfinger?resource=acct:username@your.domain just works!

Challenge #3: Core ActivityPub Mechanics—Handling Requests and Collections

Serving actor profiles requires careful content negotiation. A request for an actor's ID needs JSON-LD for machine clients (Accept: application/activity+json) but HTML for browsers (Accept: text/html). Handling incoming activities at the inbox endpoint involves validating POST requests, verifying cryptographic signatures, parsing the payload, preventing duplicates (idempotency), and routing based on activity type. Implementing collections (outbox, followers, etc.) with correct pagination adds another layer.

Fedify streamlines all of this. Its core request handler (via Federation.fetch() or framework adapters like @fedify/express) manages content negotiation. You define actors with setActorDispatcher() and web pages with your framework (Hono, Express, SvelteKit, etc.)—Fedify routes appropriately. For the inbox, setInboxListeners() lets you define handlers per activity type (e.g., .on(Follow, ...)), while Fedify automatically handles validation, signature verification, parsing, and idempotency checks using its KV Store. Collection implementation is simplified via dispatchers (e.g., setFollowersDispatcher()); you provide logic to fetch a page of data, and Fedify constructs the correct Collection or CollectionPage with pagination.

// Define inbox handlers
federation.setInboxListeners("/inbox", "/users/{handle}/inbox")
  .on(Follow, async (ctx, follow) => { /* Handle follow */ })
  .on(Undo, async (ctx, undo) => { /* Handle undo */ });

// Define followers collection logic
federation.setFollowersDispatcher(
  "/users/{handle}/followers",
  async (ctx, handle, cursor) => { /* ... */ }
);

Challenge #4: Reliable Delivery & Asynchronous Processing—Sending Activities Robustly

Sending an activity requires more than a simple POST. Networks fail, servers go down. You need robust failure handling and retry logic (ideally with backoff). Processing incoming activities synchronously can block your server. Efficiently broadcasting to many followers (fan-out) requires background processing and using shared inboxes where possible.

Fedify addresses reliability and scalability using its MessageQueue abstraction. When configured (highly recommended), Context.sendActivity() enqueues delivery tasks. Background workers handle sending with automatic retries based on configurable policies (like outboxRetryPolicy). Fedify supports various queue backends (Deno KV, Redis, PostgreSQL, AMQP). For high-traffic fan-out, Fedify uses an optimized two-stage mechanism to distribute the load efficiently.

// Configure Fedify with a persistent queue (e.g., Deno KV)
const federation = createFederation({
  queue: new DenoKvMessageQueue(/* ... */),
  // ...
});
// Sending is now reliable and non-blocking
await ctx.sendActivity({ handle: "myUser" }, recipient, someActivity);

Challenge #5: Security—Avoiding Common Pitfalls

Securing an ActivityPub server is critical. You need to implement HTTP Signatures (draft-cavage-http-signatures-12) for server-to-server authentication—a complex process. You might also need Linked Data Signatures (LDS) or Object Integrity Proofs (OIP) based on FEP-8b32 for data integrity and compatibility. Managing cryptographic keys securely is essential. Lastly, fetching remote resources risks Server-Side Request Forgery (SSRF) if not validated properly.

Fedify is designed with security in mind. It automatically handles the creation and verification of HTTP Signatures, LDS, and OIP, provided you supply keys via setKeyPairsDispatcher(). It includes key management utilities. Crucially, Fedify's default document loader includes built-in SSRF protection, blocking requests to private IPs unless explicitly allowed.

Challenge #6: Interoperability & Maintenance—Playing Nicely with Others

The fediverse is diverse. Different servers have quirks. Ensuring compatibility requires testing and adaptation. Standards evolve with new Federation Enhancement Proposals (FEPs). You also need protocols like NodeInfo to advertise server capabilities.

Fedify aims for broad interoperability and is actively maintained. It includes features like ActivityTransformers to smooth over implementation differences. NodeInfo support is built-in via setNodeInfoDispatcher().

Challenge #7: Developer Experience—Actually Building Your App

Beyond the protocol, building any server involves setup, testing, and debugging. With federation, debugging becomes harder—was the message malformed? Was the signature wrong? Is the remote server down? Is it a compatibility quirk? Good tooling is essential.

Fedify enhances the developer experience significantly. Being built with TypeScript, it offers excellent type safety and editor auto-completion. The fedify CLI is a powerful companion designed to streamline common development tasks.

You can quickly scaffold a new project tailored to your chosen runtime and web framework using fedify init.

For debugging interactions and verifying data, fedify lookup is invaluable. It lets you inspect how any remote actor or object appears from the outside by performing WebFinger discovery and fetching the object's data. Fedify then displays the parsed object structure and properties directly in your terminal. For example, running:

$ fedify lookup @fedify-example@fedify-blog.deno.dev

Will first show progress messages and then output the structured representation of the actor, similar to this:

// Output of fedify lookup command (shows parsed object structure)
Person {
  id: URL "https://fedify-blog.deno.dev/users/fedify-example",
  name: "Fedify Example Blog",
  published: 2024-03-03T13:18:11.857Z, // Simplified timestamp
  summary: "This blog is powered by Fedify, a fediverse server framework.",
  url: URL "https://fedify-blog.deno.dev/",
  preferredUsername: "fedify-example",
  publicKey: CryptographicKey {
    id: URL "https://fedify-blog.deno.dev/users/fedify-example#main-key",
    owner: URL "https://fedify-blog.deno.dev/users/fedify-example",
    publicKey: CryptoKey { /* ... CryptoKey details ... */ }
  },
  // ... other properties like inbox, outbox, followers, endpoints ...
}

This allows you to easily check how data is structured or troubleshoot why an interaction might be failing by seeing the actual properties Fedify parsed.

Testing outgoing activities from your application during development is made much easier with fedify inbox. Running the command starts a temporary local server that acts as a publicly accessible inbox, displaying key information about the temporary actor it creates for receiving messages:

$ fedify inbox
✔ The ephemeral ActivityPub server is up and running: https://<unique_id>.lhr.life/
✔ Sent follow request to @<some_test_account>@activitypub.academy.
╭───────────────┬─────────────────────────────────────────╮
│ Actor handle: │ i@<unique_id>.lhr.life                  │
├───────────────┼─────────────────────────────────────────┤
│   Actor URI:  │ https://<unique_id>.lhr.life/i          │
├───────────────┼─────────────────────────────────────────┤
│  Actor inbox: │ https://<unique_id>.lhr.life/i/inbox    │
├───────────────┼─────────────────────────────────────────┤
│ Shared inbox: │ https://<unique_id>.lhr.life/inbox      │
╰───────────────┴─────────────────────────────────────────╯

Web interface available at: http://localhost:8000/

You then configure your developing application to send an activity to the Actor inbox or Shared inbox URI provided. When an activity arrives, fedify inbox only prints a summary table to your console indicating that a request was received:

╭────────────────┬─────────────────────────────────────╮
│     Request #: │ 2                                   │
├────────────────┼─────────────────────────────────────┤
│ Activity type: │ Follow                              │
├────────────────┼─────────────────────────────────────┤
│  HTTP request: │ POST /i/inbox                       │
├────────────────┼─────────────────────────────────────┤
│ HTTP response: │ 202                                 │
├────────────────┼─────────────────────────────────────┤
│       Details  │ https://<unique_id>.lhr.life/r/2    │
╰────────────────┴─────────────────────────────────────╯

Crucially, the detailed information about the received request—including the full headers (like Signature), the request body (the Activity JSON), and the signature verification status—is only available in the web interface provided by fedify inbox. This web UI allows you to thoroughly inspect incoming activities during development.

Screenshot of the Fedify Inbox web interface showing received activities and their details.
The Fedify Inbox web UI is where you view detailed activity information.

When you need to test interactions with the live fediverse from your local machine beyond just sending, fedify tunnel can securely expose your entire local development server temporarily. This suite of tools significantly eases the process of building and debugging federated applications.

Conclusion: Build Features, Not Plumbing

Implementing the ActivityPub suite of protocols from scratch is an incredibly complex and time-consuming undertaking. It involves deep dives into multiple technical specifications, cryptographic signing, security hardening, and navigating the nuances of a diverse ecosystem. While educational, it dramatically slows down the process of building the actual, unique features of your federated application.

Fedify offers a well-architected, secure, and type-safe foundation, handling the intricacies of federation for you—data modeling, discovery, core mechanics, delivery, security, and interoperability. It lets you focus on your application's unique value and user experience. Stop wrestling with low-level protocol details and start building your vision for the fediverse faster and more reliably. Give Fedify a try!

Getting started is straightforward. First, install the Fedify CLI using your preferred method. Once installed, create a new project template by running fedify init your-project-name.

Check out the Fedify tutorials and Fedify manual to learn more. Happy federating!

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

アプリケーション開発の観点から見たDrizzle ORMとKyselyの比較」(日本語)

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


TypeScript로 백엔드 서버를 개발하면서 적절한 ORM 선택은 항상 중요한 결정 중 하나입니다. 최근 제 프로젝트에서 Drizzle ORM과 Kysely를 모두 사용해 볼 기회가 있었는데, 개인적으로는 Drizzle ORM이 더 편리하고 생산성이 높았던 경험을 공유하고자 합니다.

두 ORM에 대한 간략한 소개

Drizzle ORM은 TypeScript용 ORM으로, 타입 안전성과 직관적인 API를 강점으로 내세우고 있습니다. 스키마 정의부터 마이그레이션, 쿼리 빌더까지 풀스택 개발 경험을 제공합니다.

Kysely는 “타입 안전한 SQL 쿼리 빌더”로 자신을 소개하며, 타입스크립트의 타입 시스템을 활용해 쿼리 작성 시 타입 안전성을 보장합니다.

두 도구 모두 훌륭하지만, 제 개발 경험에 비추어 볼 때 Drizzle ORM이 몇 가지 측면에서 더 편리했습니다.

Drizzle ORM을 선호하게 된 이유

스키마 정의의 직관성

Drizzle ORM의 스키마 정의 방식은 매우 직관적이고 선언적입니다:

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').unique().notNull(),
  age: integer('age')
});

Drizzle ORM은 이 스키마 정의로부터 자동으로 CREATE TABLE SQL을 생성할 수 있어, 스키마와 코드가 항상 동기화되어 있습니다.

반면 Kysely는 타입 정의에 더 중점을 두고 있어 스키마와 타입 정의가 분리되는 경향이 있습니다:

interface Database {
  users: {
    id: Generated<number>;
    name: string;
    email: string;
    age: number | null;
  };
}

이 타입 정의는 TypeScript 코드에서 타입 안전성을 제공하지만, 이 타입 정의만으로는 CREATE TABLE SQL을 생성할 수 없다는 것이 결정적인 단점입니다. 실제로 테이블을 생성하려면 별도의 SQL 스크립트나 마이그레이션 코드를 작성해야 합니다. 이는 타입과 실제 데이터베이스 스키마 간의 불일치 가능성을 높입니다.

Drizzle의 접근 방식이 데이터베이스 스키마와 TypeScript 타입을 더 긴밀하게 연결해주어 개발 과정에서 혼란을 줄여주었습니다.

마이그레이션 경험

Drizzle ORM의 마이그레이션 도구(drizzle-kit)는 정말 인상적이었습니다. 스키마 변경사항을 자동으로 감지하고 SQL 마이그레이션 파일을 생성해주는 기능이 개발 워크플로우를 크게 개선했습니다:

npx drizzle-kit generate:pg

이 명령어 하나로 스키마 변경사항에 대한 마이그레이션 파일이 생성되며, 이를 검토하고 적용하는 과정이 매우 간단했습니다.

반면 Kysely의 마이그레이션은 본질적으로 수동적입니다. 개발자가 직접 마이그레이션 파일을 작성해야 하며, 스키마 변경사항을 자동으로 감지하거나 SQL을 생성해주는 기능이 없습니다:

// Kysely의 마이그레이션 예시
async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('name', 'text', (col) => col.notNull())
    .addColumn('email', 'text', (col) => col.unique().notNull())
    .addColumn('age', 'integer')
    .execute();
}

async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

이러한 수동 방식은 복잡한 스키마 변경에서 실수할 가능성이 높아지고, 특히 큰 프로젝트에서는 작업량이 상당히 증가할 수 있었습니다.

하지만 Kysely의 마이그레이션에도 두 가지 중요한 장점이 있습니다:

  1. TypeScript 기반 마이그레이션: Kysely의 마이그레이션 스크립트는 TypeScript로 작성되기 때문에, 마이그레이션 로직에 애플리케이션 로직을 통합할 수 있습니다. 예를 들어, S3와 같은 오브젝트 스토리지의 데이터도 함께 마이그레이트하는 복잡한 시나리오를 구현할 수 있습니다. 반면 Drizzle ORM은 SQL 기반 마이그레이션이므로 이러한 통합이 불가능합니다.

  2. 양방향 마이그레이션: Kysely는 updown 함수를 모두 정의하여 업그레이드와 다운그레이드를 모두 지원합니다. 이는 특히 팀 협업 환경에서 중요한데, 다른 개발자의 변경사항과 충돌이 발생할 경우 롤백이 필요할 수 있기 때문입니다. Drizzle ORM은 현재 업그레이드만 지원하며, 다운그레이드 기능이 없어 협업 시 불편할 수 있습니다.

참고로, Python 생태계의 SQLAlchemy 마이그레이션 도구인 Alembic은 훨씬 더 발전된 형태의 마이그레이션을 제공합니다. Alembic은 비선형적인 마이그레이션 경로(브랜치포인트 생성 가능)를 지원하여 복잡한 팀 개발 환경에서도 유연하게 대응할 수 있습니다. 이상적으로는 JavaScript/TypeScript 생태계의 ORM도 이러한 수준의 마이그레이션 도구를 제공하는 것이 바람직합니다.

관계 설정의 용이성

Drizzle ORM에서 테이블 간 관계 설정이 매우 직관적이었습니다:

import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ one, many }) => ({
  profile: one(profiles, {
    fields: [users.id],
    references: [profiles.userId],
  }),
  posts: many(posts)
}));

이 방식은 데이터베이스 설계의 본질적인, 관계적인 측면을 명확하게 표현해주었습니다.

쿼리 작성의 편의성과 동일 이름 칼럼 문제 처리

두 ORM 모두 쿼리 작성을 위한 API를 제공하지만, Drizzle의 접근 방식이 더 직관적이고 관계형 모델을 활용하기 쉬웠습니다:

// Drizzle ORM - db.query 방식으로 관계 활용
const result = await db.query.posts.findMany({
  where: eq(posts.published, true),
  with: {
    user: true  // 게시물 작성자 정보를 함께 조회
  }
});

// 결과 접근이 직관적이고 타입 안전함
console.log(result[0].title);       // 게시물 제목
console.log(result[0].user.name);   // 작성자 이름 - 객체 구조로 명확하게 구분됨
console.log(result[0].user.id);     // 작성자 ID - 게시물 ID와 이름이 같아도 문제 없음

// Kysely
const result = await db
  .selectFrom('posts')
  .where('posts.published', '=', true)
  .leftJoin('users', 'posts.userId', 'users.id')
  .selectAll();

// 결과 접근 시 칼럼 이름 충돌 문제
console.log(result[0].id) // 오류: posts.id와 users.id 중 어떤 것인지 모호함
console.log(result[0].name) // 오류: 둘 다 name 칼럼이 있다면 모호함

Drizzle의 접근 방식이 테이블과 컬럼을 참조할 때 타입 안전성을 더 강력하게 보장하고, 관계를 활용한 쿼리 작성이 더 직관적이었습니다.

특히 여러 테이블 조인 시 동일한 이름의 칼럼 처리 부분에서 Drizzle ORM이 훨씬 더 편리했습니다. 이는 제 개발 경험에서 가장 중요한 차이점 중 하나였습니다.

// Drizzle ORM - 동일 이름 칼럼 처리
const result = await db.query.posts.findMany({
  with: {
    user: true  // posts.id와 users.id가 모두 있지만 자동으로 구분됨
  }
});

// 결과에 자연스럽게 접근 가능
console.log(result[0].id);        // 게시물 ID
console.log(result[0].user.id);   // 사용자 ID - 명확하게 구분됨
console.log(result[0].user.name); // 사용자 이름

// Kysely - 동일 이름 칼럼 처리를 위해 별칭 필요
const result = await db
  .selectFrom('posts')
  .leftJoin('users', 'posts.userId', 'users.id')
  .select([
    'posts.id as postId',       // 별칭 필수
    'posts.title',
    'posts.content',
    'users.id as userId',       // 별칭 필수
    'users.name as userName',   // 칼럼 이름이 같을 수 있으므로 별칭 필수
    'users.email as userEmail'  // 일관성을 위해 모든 사용자 관련 칼럼에 접두어 필요
  ]);

// 별칭을 통한 접근
console.log(result[0].postId);    // 게시물 ID
console.log(result[0].userId);    // 사용자 ID
console.log(result[0].userName);  // 사용자 이름

Drizzle ORM은 테이블과 칼럼을 객체로 참조하기 때문에, 동일한 이름의 칼럼이 있어도 자연스럽게 계층 구조로 처리되며 타입 추론도 정확하게 작동합니다. 반면 Kysely에서는 문자열 기반 접근 방식 때문에 별칭을 수동으로 지정해야 하는 경우가 많았고, 복잡한 조인에서 이런 작업이 번거로워졌습니다. 특히 여러 테이블에 같은 이름의 칼럼이 많을수록 모든 칼럼에 명시적인 별칭을 지정해야 하는 불편함이 있었습니다.

또한 Drizzle ORM은 결과 타입을 자동으로 정확하게 추론해주어 별도의 타입 지정 없이도 안전하게 결과를 사용할 수 있었습니다.

Kysely의 장점

물론 Kysely도 여러 강점이 있습니다:

  1. 더 가벼운 구조: 필요한 기능만 포함할 수 있는 모듈화된 구조
  2. SQL에 더 가까운 접근: SQL 구문에 매우 충실한 API 설계
  3. 유연성: 복잡한 쿼리에서 때로 더 유연한 작성이 가능

또한 앞서 언급했듯이, Kysely의 TypeScript 기반 마이그레이션과 양방향(up/down) 마이그레이션 지원은 특정 상황에서 Drizzle ORM보다 우위에 있는 기능입니다.

SQLAlchemy와의 비교 및 앞으로의 기대

JavaScript/TypeScript 생태계의 ORM을 이야기하기 전에, 여러 언어 중에서도 Python의 SQLAlchemy는 특별한 위치를 차지합니다. 개인적으로 여태 사용해본 다양한 언어의 ORM 중에서 SQLAlchemy가 가장 기능이 풍부하고 강력하다고 느꼈습니다. 복잡한 쿼리 구성, 고급 관계 매핑, 트랜잭션 관리, 이벤트 시스템 등 SQLAlchemy의 기능은 정말 방대합니다.

Drizzle ORM은 JavaScript 생태계에서 매우 인상적인 발전을 이루었지만, 아직 SQLAlchemy의 경지에는 이르지 못했다고 생각합니다. 특히 다음과 같은 부분에서 SQLAlchemy의 성숙도와 기능 풍부함이 돋보입니다:

  • 복잡한 서브쿼리와 윈도우 함수 지원
  • 다양한 이벤트 리스너와 훅
  • 다양한 상속 전략
  • 복잡한 트랜잭션 관리와 세션 관리
  • 대규모 프로젝트에서 검증된 안정성
  • Alembic을 통한 비선형적 마이그레이션 지원
  • 놀라울 정도로 방대하고 상세한 문서화

결론

두 ORM 모두 훌륭한 도구이지만, 제 개발 스타일과 프로젝트 요구사항에는 Drizzle ORM이 더 잘 맞았습니다. 특히 스키마 정의의 직관성, 강력한 마이그레이션 도구, 그리고 전반적인 개발자 경험 측면에서 Drizzle ORM이 더 생산적인 개발을 가능하게 해주었습니다.

동일 이름 칼럼 처리와 같은 실질적인 문제에서 Drizzle ORM의 객체 기반 접근 방식이 가져다주는 편리함은 실제 프로젝트에서 큰 차이를 만들었습니다.

ORM 선택은 결국 프로젝트 특성과 개인 선호도에 크게 좌우됩니다. 새로운 프로젝트를 시작한다면 두 도구 모두 간단히 테스트해보고 자신의 워크플로우에 더 적합한 것을 선택하는 것이 좋겠지만, 제 경우에는 Drizzle ORM이 명확한 승자였습니다.

앞으로 Drizzle ORM이 더욱 발전하여 SQLAlchemy 수준의 풍부한 기능과 유연성을 제공하게 되길 바랍니다. JavaScript/TypeScript 생태계에도 그런 수준의 강력한 ORM이 있으면 좋겠습니다. 다행히도 Drizzle ORM은 계속해서 발전하고 있으며, 그 발전 속도를 보면 기대가 큽니다.

여러분의 경험은 어떤가요? 다른 ORM 도구나 언어를 사용해보셨다면 의견을 공유해주세요!

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

Hackers' PubにLLMを活用した記事の自動翻訳機能が追加されました。これにより、Hackers' Pubに投稿された英語や韓国語の記事を日本語で閲覧できるだけでなく、日本語で書いた記事を海外の読者に紹介する事も可能に成りました。

なお、Hackers' Pubは招待制で運営されています。ご興味のある方は、DMでメールアドレスを教えていただければ、招待いたします。

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


LLM 기반의 게시글 번역 기능이 추가되었습니다. 우선, 자신이 쓴 게시글이 LLM을 이용해 번역되는 것을 허용하려면, 게시글 공개 설정에서 “LLM 기반 자동 번역 허용” 옵션을 켜 주셔야 합니다. 기존 게시글은 모두 이 옵션이 꺼져 있습니다만, 새로 쓰는 게시글의 경우 기본적으로 켜져 있습니다.

한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션

위와 같이 옵션을 켜 준 게시글은 위쪽에 다음과 같이 “다른 언어로 읽기” 메뉴가 표시되게 됩니다. 이 메뉴에 나오는 언어 목록은 언어 설정에서 정할 수 있습니다.

게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)

이 중에서 이미 번역이 완료된 언어는 바로 표시되지만, 아직 번역이 완료되지 않은 언어의 경우, 아래와 같이 기다리라는 메시지가 뜨게 됩니다. 게시글의 분량에 따라 번역 시간은 차이가 나지만, 짧으면 30초에서 길면 5분 정도 걸립니다.

게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”

번역이 완료되면, 아래와 같이 메시지가 바뀝니다.

게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”

번역 기능은 제가 Hackers' Pub을 맨 처음 구상할 때부터 핵심 기능으로 고려하고 있던 것이었습니다. 소프트웨어 프로그래머로서 일정 수준 이상 성장하기 위해서는 반드시 영어를 배워야만 하는 불합리함이나 그리고 일본어나 중국어 등 영어가 아닌 언어로 쓰인 다양한 자료에 대부분의 외국인은 접근하지 못한다는 아쉬움을 오래 전부터 느꼈기 때문입니다. 다행히 얼마 전부터 LLM의 번역 품질이 아주 좋아졌고, 이를 활용하여 꽤 괜찮은 품질의 번역 기능을 Hackers' Pub 같은 작은 웹사이트에서도 구현할 수 있게 되었네요.

참고로 현재 번역에 쓰이는 모델은 Claude Sonnet 3.7입니다. 저렴하다고는 할 수 없는 모델인데요. 시범적으로 운영해 보고, 비용이 너무 부담된다고 여겨지면 Gemini 2.5 Flash 같은 다른 모델로 전환하는 것도 고려하고 있습니다.

아무튼, 모처럼 추가한 번역 기능이니 많은 분들이 유용함을 누리셨으면 좋겠습니다.

아래는 제가 샘플로 미리 만들어 둔 번역본들입니다:

甘瀬ここあ's avatar
甘瀬ここあ

@cocoa@hackers.pub

print("Hello World")
ここあにゃん :surprised_karin_chibi:'s avatar
ここあにゃん :surprised_karin_chibi:

@AmaseCocoa@s.amase.cc

招待してもらったのでhackers.pubに出たり入ったりした→@cocoa@hackers.pub

大文字小文字混ぜると問題起こる気がしたのでわざと
@cocoaにした (そもそもこっちが正式ではなくもない)

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to ここあにゃん :surprised_karin_chibi:'s post

@AmaseCocoa もしHackers' Pubのアカウントが必要でしたら、招待できますよ。

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

나도 大部分(대부분)의 술은 그 特有(특유)의 알코올 냄새 때문에 마실 수 있는 液體(액체)로 느껴지지 않는다. 조금이나마 마실 수 있는 술은 低度酒(저도주)屑糖(설탕) 잔뜩 넣은 것들인데, 알코올 냄새가 그나마 적게 나기 때문에. 꾸준히 마시다 보면 알코올 냄새에 ()拒否感(거부감)이 줄면서 다른 냄새와 맛을 느끼게 된다고 하는데, ()로 그렇게까지 해서 알코올을 마시고 싶지는 않아서 40()를 눈앞에 두고도 술을 못 마신다.

https://fed.brid.gy/r/https://bsky.app/profile/did:plc:ppk763j7o2wkinvzuqx4orrb/post/3lobcxxoybc2c

잇창명 EatChangmyeong💕's avatar
잇창명 EatChangmyeong💕

@eatch.dev@bsky.brid.gy

(돌맞을 발언 계속) 사실 저는 "술이 맛있다" ← 이것부터 벌써 동의를 못하겠고 특정 감정을 조절하려고 술을 마시는 것도 잘 이해가 안돼요 그냥... 맨정신으로 느끼면 되는 거 아닌가...?

ここあにゃん :surprised_karin_chibi:'s avatar
ここあにゃん :surprised_karin_chibi:

@AmaseCocoa@s.amase.cc

hackers.pub,少し気になってる

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


LLM 기반의 게시글 번역 기능이 추가되었습니다. 우선, 자신이 쓴 게시글이 LLM을 이용해 번역되는 것을 허용하려면, 게시글 공개 설정에서 “LLM 기반 자동 번역 허용” 옵션을 켜 주셔야 합니다. 기존 게시글은 모두 이 옵션이 꺼져 있습니다만, 새로 쓰는 게시글의 경우 기본적으로 켜져 있습니다.

한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션

위와 같이 옵션을 켜 준 게시글은 위쪽에 다음과 같이 “다른 언어로 읽기” 메뉴가 표시되게 됩니다. 이 메뉴에 나오는 언어 목록은 언어 설정에서 정할 수 있습니다.

게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)

이 중에서 이미 번역이 완료된 언어는 바로 표시되지만, 아직 번역이 완료되지 않은 언어의 경우, 아래와 같이 기다리라는 메시지가 뜨게 됩니다. 게시글의 분량에 따라 번역 시간은 차이가 나지만, 짧으면 30초에서 길면 5분 정도 걸립니다.

게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”

번역이 완료되면, 아래와 같이 메시지가 바뀝니다.

게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”

번역 기능은 제가 Hackers' Pub을 맨 처음 구상할 때부터 핵심 기능으로 고려하고 있던 것이었습니다. 소프트웨어 프로그래머로서 일정 수준 이상 성장하기 위해서는 반드시 영어를 배워야만 하는 불합리함이나 그리고 일본어나 중국어 등 영어가 아닌 언어로 쓰인 다양한 자료에 대부분의 외국인은 접근하지 못한다는 아쉬움을 오래 전부터 느꼈기 때문입니다. 다행히 얼마 전부터 LLM의 번역 품질이 아주 좋아졌고, 이를 활용하여 꽤 괜찮은 품질의 번역 기능을 Hackers' Pub 같은 작은 웹사이트에서도 구현할 수 있게 되었네요.

참고로 현재 번역에 쓰이는 모델은 Claude Sonnet 3.7입니다. 저렴하다고는 할 수 없는 모델인데요. 시범적으로 운영해 보고, 비용이 너무 부담된다고 여겨지면 Gemini 2.5 Flash 같은 다른 모델로 전환하는 것도 고려하고 있습니다.

아무튼, 모처럼 추가한 번역 기능이니 많은 분들이 유용함을 누리셨으면 좋겠습니다.

아래는 제가 샘플로 미리 만들어 둔 번역본들입니다:

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to ここあにゃん :surprised_karin_chibi:'s post

@AmaseCocoa 有りますよ。例:https://hackers.pub/.well-known/webfinger?resource=https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

アプリケーション開発の観点から見たDrizzle ORMとKyselyの比較」(日本語)

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


TypeScript로 백엔드 서버를 개발하면서 적절한 ORM 선택은 항상 중요한 결정 중 하나입니다. 최근 제 프로젝트에서 Drizzle ORM과 Kysely를 모두 사용해 볼 기회가 있었는데, 개인적으로는 Drizzle ORM이 더 편리하고 생산성이 높았던 경험을 공유하고자 합니다.

두 ORM에 대한 간략한 소개

Drizzle ORM은 TypeScript용 ORM으로, 타입 안전성과 직관적인 API를 강점으로 내세우고 있습니다. 스키마 정의부터 마이그레이션, 쿼리 빌더까지 풀스택 개발 경험을 제공합니다.

Kysely는 “타입 안전한 SQL 쿼리 빌더”로 자신을 소개하며, 타입스크립트의 타입 시스템을 활용해 쿼리 작성 시 타입 안전성을 보장합니다.

두 도구 모두 훌륭하지만, 제 개발 경험에 비추어 볼 때 Drizzle ORM이 몇 가지 측면에서 더 편리했습니다.

Drizzle ORM을 선호하게 된 이유

스키마 정의의 직관성

Drizzle ORM의 스키마 정의 방식은 매우 직관적이고 선언적입니다:

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').unique().notNull(),
  age: integer('age')
});

Drizzle ORM은 이 스키마 정의로부터 자동으로 CREATE TABLE SQL을 생성할 수 있어, 스키마와 코드가 항상 동기화되어 있습니다.

반면 Kysely는 타입 정의에 더 중점을 두고 있어 스키마와 타입 정의가 분리되는 경향이 있습니다:

interface Database {
  users: {
    id: Generated<number>;
    name: string;
    email: string;
    age: number | null;
  };
}

이 타입 정의는 TypeScript 코드에서 타입 안전성을 제공하지만, 이 타입 정의만으로는 CREATE TABLE SQL을 생성할 수 없다는 것이 결정적인 단점입니다. 실제로 테이블을 생성하려면 별도의 SQL 스크립트나 마이그레이션 코드를 작성해야 합니다. 이는 타입과 실제 데이터베이스 스키마 간의 불일치 가능성을 높입니다.

Drizzle의 접근 방식이 데이터베이스 스키마와 TypeScript 타입을 더 긴밀하게 연결해주어 개발 과정에서 혼란을 줄여주었습니다.

마이그레이션 경험

Drizzle ORM의 마이그레이션 도구(drizzle-kit)는 정말 인상적이었습니다. 스키마 변경사항을 자동으로 감지하고 SQL 마이그레이션 파일을 생성해주는 기능이 개발 워크플로우를 크게 개선했습니다:

npx drizzle-kit generate:pg

이 명령어 하나로 스키마 변경사항에 대한 마이그레이션 파일이 생성되며, 이를 검토하고 적용하는 과정이 매우 간단했습니다.

반면 Kysely의 마이그레이션은 본질적으로 수동적입니다. 개발자가 직접 마이그레이션 파일을 작성해야 하며, 스키마 변경사항을 자동으로 감지하거나 SQL을 생성해주는 기능이 없습니다:

// Kysely의 마이그레이션 예시
async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('name', 'text', (col) => col.notNull())
    .addColumn('email', 'text', (col) => col.unique().notNull())
    .addColumn('age', 'integer')
    .execute();
}

async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

이러한 수동 방식은 복잡한 스키마 변경에서 실수할 가능성이 높아지고, 특히 큰 프로젝트에서는 작업량이 상당히 증가할 수 있었습니다.

하지만 Kysely의 마이그레이션에도 두 가지 중요한 장점이 있습니다:

  1. TypeScript 기반 마이그레이션: Kysely의 마이그레이션 스크립트는 TypeScript로 작성되기 때문에, 마이그레이션 로직에 애플리케이션 로직을 통합할 수 있습니다. 예를 들어, S3와 같은 오브젝트 스토리지의 데이터도 함께 마이그레이트하는 복잡한 시나리오를 구현할 수 있습니다. 반면 Drizzle ORM은 SQL 기반 마이그레이션이므로 이러한 통합이 불가능합니다.

  2. 양방향 마이그레이션: Kysely는 updown 함수를 모두 정의하여 업그레이드와 다운그레이드를 모두 지원합니다. 이는 특히 팀 협업 환경에서 중요한데, 다른 개발자의 변경사항과 충돌이 발생할 경우 롤백이 필요할 수 있기 때문입니다. Drizzle ORM은 현재 업그레이드만 지원하며, 다운그레이드 기능이 없어 협업 시 불편할 수 있습니다.

참고로, Python 생태계의 SQLAlchemy 마이그레이션 도구인 Alembic은 훨씬 더 발전된 형태의 마이그레이션을 제공합니다. Alembic은 비선형적인 마이그레이션 경로(브랜치포인트 생성 가능)를 지원하여 복잡한 팀 개발 환경에서도 유연하게 대응할 수 있습니다. 이상적으로는 JavaScript/TypeScript 생태계의 ORM도 이러한 수준의 마이그레이션 도구를 제공하는 것이 바람직합니다.

관계 설정의 용이성

Drizzle ORM에서 테이블 간 관계 설정이 매우 직관적이었습니다:

import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ one, many }) => ({
  profile: one(profiles, {
    fields: [users.id],
    references: [profiles.userId],
  }),
  posts: many(posts)
}));

이 방식은 데이터베이스 설계의 본질적인, 관계적인 측면을 명확하게 표현해주었습니다.

쿼리 작성의 편의성과 동일 이름 칼럼 문제 처리

두 ORM 모두 쿼리 작성을 위한 API를 제공하지만, Drizzle의 접근 방식이 더 직관적이고 관계형 모델을 활용하기 쉬웠습니다:

// Drizzle ORM - db.query 방식으로 관계 활용
const result = await db.query.posts.findMany({
  where: eq(posts.published, true),
  with: {
    user: true  // 게시물 작성자 정보를 함께 조회
  }
});

// 결과 접근이 직관적이고 타입 안전함
console.log(result[0].title);       // 게시물 제목
console.log(result[0].user.name);   // 작성자 이름 - 객체 구조로 명확하게 구분됨
console.log(result[0].user.id);     // 작성자 ID - 게시물 ID와 이름이 같아도 문제 없음

// Kysely
const result = await db
  .selectFrom('posts')
  .where('posts.published', '=', true)
  .leftJoin('users', 'posts.userId', 'users.id')
  .selectAll();

// 결과 접근 시 칼럼 이름 충돌 문제
console.log(result[0].id) // 오류: posts.id와 users.id 중 어떤 것인지 모호함
console.log(result[0].name) // 오류: 둘 다 name 칼럼이 있다면 모호함

Drizzle의 접근 방식이 테이블과 컬럼을 참조할 때 타입 안전성을 더 강력하게 보장하고, 관계를 활용한 쿼리 작성이 더 직관적이었습니다.

특히 여러 테이블 조인 시 동일한 이름의 칼럼 처리 부분에서 Drizzle ORM이 훨씬 더 편리했습니다. 이는 제 개발 경험에서 가장 중요한 차이점 중 하나였습니다.

// Drizzle ORM - 동일 이름 칼럼 처리
const result = await db.query.posts.findMany({
  with: {
    user: true  // posts.id와 users.id가 모두 있지만 자동으로 구분됨
  }
});

// 결과에 자연스럽게 접근 가능
console.log(result[0].id);        // 게시물 ID
console.log(result[0].user.id);   // 사용자 ID - 명확하게 구분됨
console.log(result[0].user.name); // 사용자 이름

// Kysely - 동일 이름 칼럼 처리를 위해 별칭 필요
const result = await db
  .selectFrom('posts')
  .leftJoin('users', 'posts.userId', 'users.id')
  .select([
    'posts.id as postId',       // 별칭 필수
    'posts.title',
    'posts.content',
    'users.id as userId',       // 별칭 필수
    'users.name as userName',   // 칼럼 이름이 같을 수 있으므로 별칭 필수
    'users.email as userEmail'  // 일관성을 위해 모든 사용자 관련 칼럼에 접두어 필요
  ]);

// 별칭을 통한 접근
console.log(result[0].postId);    // 게시물 ID
console.log(result[0].userId);    // 사용자 ID
console.log(result[0].userName);  // 사용자 이름

Drizzle ORM은 테이블과 칼럼을 객체로 참조하기 때문에, 동일한 이름의 칼럼이 있어도 자연스럽게 계층 구조로 처리되며 타입 추론도 정확하게 작동합니다. 반면 Kysely에서는 문자열 기반 접근 방식 때문에 별칭을 수동으로 지정해야 하는 경우가 많았고, 복잡한 조인에서 이런 작업이 번거로워졌습니다. 특히 여러 테이블에 같은 이름의 칼럼이 많을수록 모든 칼럼에 명시적인 별칭을 지정해야 하는 불편함이 있었습니다.

또한 Drizzle ORM은 결과 타입을 자동으로 정확하게 추론해주어 별도의 타입 지정 없이도 안전하게 결과를 사용할 수 있었습니다.

Kysely의 장점

물론 Kysely도 여러 강점이 있습니다:

  1. 더 가벼운 구조: 필요한 기능만 포함할 수 있는 모듈화된 구조
  2. SQL에 더 가까운 접근: SQL 구문에 매우 충실한 API 설계
  3. 유연성: 복잡한 쿼리에서 때로 더 유연한 작성이 가능

또한 앞서 언급했듯이, Kysely의 TypeScript 기반 마이그레이션과 양방향(up/down) 마이그레이션 지원은 특정 상황에서 Drizzle ORM보다 우위에 있는 기능입니다.

SQLAlchemy와의 비교 및 앞으로의 기대

JavaScript/TypeScript 생태계의 ORM을 이야기하기 전에, 여러 언어 중에서도 Python의 SQLAlchemy는 특별한 위치를 차지합니다. 개인적으로 여태 사용해본 다양한 언어의 ORM 중에서 SQLAlchemy가 가장 기능이 풍부하고 강력하다고 느꼈습니다. 복잡한 쿼리 구성, 고급 관계 매핑, 트랜잭션 관리, 이벤트 시스템 등 SQLAlchemy의 기능은 정말 방대합니다.

Drizzle ORM은 JavaScript 생태계에서 매우 인상적인 발전을 이루었지만, 아직 SQLAlchemy의 경지에는 이르지 못했다고 생각합니다. 특히 다음과 같은 부분에서 SQLAlchemy의 성숙도와 기능 풍부함이 돋보입니다:

  • 복잡한 서브쿼리와 윈도우 함수 지원
  • 다양한 이벤트 리스너와 훅
  • 다양한 상속 전략
  • 복잡한 트랜잭션 관리와 세션 관리
  • 대규모 프로젝트에서 검증된 안정성
  • Alembic을 통한 비선형적 마이그레이션 지원
  • 놀라울 정도로 방대하고 상세한 문서화

결론

두 ORM 모두 훌륭한 도구이지만, 제 개발 스타일과 프로젝트 요구사항에는 Drizzle ORM이 더 잘 맞았습니다. 특히 스키마 정의의 직관성, 강력한 마이그레이션 도구, 그리고 전반적인 개발자 경험 측면에서 Drizzle ORM이 더 생산적인 개발을 가능하게 해주었습니다.

동일 이름 칼럼 처리와 같은 실질적인 문제에서 Drizzle ORM의 객체 기반 접근 방식이 가져다주는 편리함은 실제 프로젝트에서 큰 차이를 만들었습니다.

ORM 선택은 결국 프로젝트 특성과 개인 선호도에 크게 좌우됩니다. 새로운 프로젝트를 시작한다면 두 도구 모두 간단히 테스트해보고 자신의 워크플로우에 더 적합한 것을 선택하는 것이 좋겠지만, 제 경우에는 Drizzle ORM이 명확한 승자였습니다.

앞으로 Drizzle ORM이 더욱 발전하여 SQLAlchemy 수준의 풍부한 기능과 유연성을 제공하게 되길 바랍니다. JavaScript/TypeScript 생태계에도 그런 수준의 강력한 ORM이 있으면 좋겠습니다. 다행히도 Drizzle ORM은 계속해서 발전하고 있으며, 그 발전 속도를 보면 기대가 큽니다.

여러분의 경험은 어떤가요? 다른 ORM 도구나 언어를 사용해보셨다면 의견을 공유해주세요!

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

Hackers' PubにLLMを活用した記事の自動翻訳機能が追加されました。これにより、Hackers' Pubに投稿された英語や韓国語の記事を日本語で閲覧できるだけでなく、日本語で書いた記事を海外の読者に紹介する事も可能に成りました。

なお、Hackers' Pubは招待制で運営されています。ご興味のある方は、DMでメールアドレスを教えていただければ、招待いたします。

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


LLM 기반의 게시글 번역 기능이 추가되었습니다. 우선, 자신이 쓴 게시글이 LLM을 이용해 번역되는 것을 허용하려면, 게시글 공개 설정에서 “LLM 기반 자동 번역 허용” 옵션을 켜 주셔야 합니다. 기존 게시글은 모두 이 옵션이 꺼져 있습니다만, 새로 쓰는 게시글의 경우 기본적으로 켜져 있습니다.

한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
한국어판 게시글 공개 설정 페이지에 추가된 “LLM 기반 자동 번역 허용” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션
영어판 게시물 공개 설정 페이지에 추가된 “Allow LLM-powered automatic translation” 옵션

위와 같이 옵션을 켜 준 게시글은 위쪽에 다음과 같이 “다른 언어로 읽기” 메뉴가 표시되게 됩니다. 이 메뉴에 나오는 언어 목록은 언어 설정에서 정할 수 있습니다.

게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “다른 언어로 읽기” 메뉴 (한국어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)
게시글 첫 부분에 표시되는 “Read in other languages” 메뉴 (영어판)

이 중에서 이미 번역이 완료된 언어는 바로 표시되지만, 아직 번역이 완료되지 않은 언어의 경우, 아래와 같이 기다리라는 메시지가 뜨게 됩니다. 게시글의 분량에 따라 번역 시간은 차이가 나지만, 짧으면 30초에서 길면 5분 정도 걸립니다.

게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역중입니다. 번역이 완료될 때까지 기다려 주세요.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”
게시글이 번역중이라는 메시지 (영어판): “This article is being translated from Korean to English. Please wait until the translation is complete.”

번역이 완료되면, 아래와 같이 메시지가 바뀝니다.

게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (한국어판): “이 게시글은 영어에서 한국어로 번역되었습니다.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”
게시글의 번역본 상단에 뜨는 메시지 (영어판): “This article has been translated from Korean to English.”

번역 기능은 제가 Hackers' Pub을 맨 처음 구상할 때부터 핵심 기능으로 고려하고 있던 것이었습니다. 소프트웨어 프로그래머로서 일정 수준 이상 성장하기 위해서는 반드시 영어를 배워야만 하는 불합리함이나 그리고 일본어나 중국어 등 영어가 아닌 언어로 쓰인 다양한 자료에 대부분의 외국인은 접근하지 못한다는 아쉬움을 오래 전부터 느꼈기 때문입니다. 다행히 얼마 전부터 LLM의 번역 품질이 아주 좋아졌고, 이를 활용하여 꽤 괜찮은 품질의 번역 기능을 Hackers' Pub 같은 작은 웹사이트에서도 구현할 수 있게 되었네요.

참고로 현재 번역에 쓰이는 모델은 Claude Sonnet 3.7입니다. 저렴하다고는 할 수 없는 모델인데요. 시범적으로 운영해 보고, 비용이 너무 부담된다고 여겨지면 Gemini 2.5 Flash 같은 다른 모델로 전환하는 것도 고려하고 있습니다.

아무튼, 모처럼 추가한 번역 기능이니 많은 분들이 유용함을 누리셨으면 좋겠습니다.

아래는 제가 샘플로 미리 만들어 둔 번역본들입니다:

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

카카오톡은 Linux 버전을 내놓든가 아니면 웹 버전을 내놓아라! (아니, 그냥 사람들이 카카오톡을 이제 그만 썼으면 좋겠다…)

dansup's avatar
dansup

@dansup@mastodon.social

This new FediDB v2 webapp can be self-hosted btw!

Working on adding a few new pages/features to FediDB, including:

- translation support (i18n)
- a federated wiki (👀)
- apps + clients
- popular accounts
- viral topics
- trends
- quarterly reports
- stats overview w/ geomaps + advanced filtering

And much more ✨

> git clone github.com/fedidb/fedidb-nuxt

🔗 : new.fedidb.org/

Deno's avatar
Deno

@deno_land@fosstodon.org

Deno 2.3 is here:
🌱 deno compile with FFI & Node native add-ons
📦 Local npm packages
⭐ deno fmt CSS/HTML/SQL in tagged templates
🔭 OTel event recording & tracing in distributed services
and more —

deno.com/blog/v2.3

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub

.github/copilot-instructions.md, .cursorrules, .windsurfrules, CLAUDE.md… 이것 말고도 많이 있을텐데, 어차피 들어가야 하는 내용은 다 거기서 거기. 지금은 한 파일에 적고 심볼릭 링크로 같은 곳을 바라보게 하고 있지만, .editorconfig처럼 그냥 어떤 식으로든 표준화가 되었으면 좋겠다.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

@hollo 따로 글이 지워지거나 옮겨지지는 않고요, 팔로워가 새 계정으로 옮겨집니다. 그리고 예전 계정은 아마 못 쓰게 될 겁니다. 로그인은 되는데 새 글을 올리거나 하는 게 막힐 거예요.

kiwiyou's avatar
kiwiyou

@kiwiyou@twt.rs

고기 먹으러 와서 이체자 찾기
焼き肉屋で異体字探し
(1) 石雲樓
(2) 睡穩葉舟輕,風徴浪不驚。仼君蘆葦㟁,終夜動秋聲。

robin's avatar
robin

@robin_maki@planet.moe

출시했다
typie.co

정진명의 굳이 써서 남기는 생각's avatar
정진명의 굳이 써서 남기는 생각

@jm@guji.jjme.me

『사회적 배제Social Exclusion』란 개인이 삶의 경제적, 사회적, 정치적, 또는 문화적 측면에서 온전히 참여할 수 없는 상태와 그런 상태를 유지하거나 그런 상태로 만드려는 과정을 가리키는 말입니다.(Report on the World Social Situation 2016, p.19)

『사회적 배제주의Social Exclusionism』라는 말은, 더 좋은 표현이 있을지도 모르겠지만, 그런 상태를 추구하는 움직임을 가리키기 위해 붙인 이름입니다. 대충 다음과 같은 사례들, 그런 사례를 옹호하고 주장하는 사례들을 보다가 이 문서를 쓰게 되었습니다.

  • 범죄자에 대한 극형, 제도적 사회 배제 요구
  • 장애인의 이동권 묵살 및 부적절한 시설 수용 행태에 대한 합리화
  • 성노동자 및 탈성노동자의 발언권 압수 시도와 탈성매매에 대한 방해
  • 미등록 상태/등록 상태의 이주노동자에 대한 차별 요구와 처우에 대한 합리화
  • 다양성에 대한 백래시

이런 사례의 대부분은, 사회적으로 배제하려는 대상이 사회에 어떻게 존재하는지에 대해 알려고 시도하지 않는 것과, '어쨌든 나는 보기 싫다'는 심리를 사회가 동의해야 한다고 믿는 자기중심적인 사고에서 온다고 생각합니다.

우리 사회 구조의 많은 장치들은 힘을 가진 권력자―대개는 국가겠지요―가 개인―사람을 포함하겠지만, 국가만큼이나 힘이 있는 자본가나 기업도 여기 포함되겠지요―에게 할 수 있는 폭거를 제지하기 위한 장치라고 생각합니다. 민주주의 사회라면 가져야 할 표현의 자유, 정치 참여의 자유 같은 개념들은, 사실 개인과 개인의 관계를 규정하기 위한 도구가 아니었다는 것이 제 생각입니다. 그만큼 국가가 개인에 대해서 행할 수 있는 폭거에 대해서는 우리가 여러 시행착오를 거쳐 여기까지 왔다고 생각합니다.

그렇다면 민주주의 사회에서, 그러니까 한 사람 한 사람이 주권을 가져야 하는 사회에서, 그 주권자들이 "어떠한 사람들을 사회적으로 배제해야 한다"고 주장한다면, 그것은 무엇으로 다스려야 할까요? 다스릴 수 있는 것일까요? 어떤 근거로? 누가? 저는 이 문제에 답해야 한다고 생각하지만, 어떻게 답을 내릴 수 있을지는 모르겠습니다.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

@hollo 감사합니다. Railway 설정 매뉴얼은 업데이트하도록 하겠습니다.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

@hollo 프로필 사진을 설정 안 하셔서 그 페이지에서 맨 처음으로 나오는 이미지가 걸린 것 같네요.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

@hollo 네, S3에는 이미지나 비디오 같은 미디어만 저장합니다.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

ソフトウェア開発に関する韓国語圏および英語圏の記事をLLMを活用して日本語に自動翻訳した記事が投稿されるサービスが有れば、利用する意向は有りますか?

OptionVoters
購読する6 (19%)
時々確認する18 (56%)
利用しない8 (25%)
洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

ソフトウェア開発に関する韓国語圏および英語圏の記事をLLMを活用して日本語に自動翻訳した記事が投稿されるサービスが有れば、利用する意向は有りますか?

OptionVoters
購読する6 (19%)
時々確認する18 (56%)
利用しない8 (25%)
← Newer
Older →