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

洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · 980 following · 1329 followers

An intersectionalist, feminist, and socialist 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) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@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) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

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

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

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

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

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

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

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

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

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

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

@hongminhee@hackers.pub


요즘 많은 사람들이 그러하듯이 나 역시 최근에는 LLM과 함께 코딩하는 일이 많아졌다. 내가 LLM과 함께 코딩한다고 말하면 의외라는 듯이 반응하는 사람들도 있고, 어떤 식으로 LLM을 활용하냐는 질문도 종종 받는다. 그래서 생각난 김에 내가 LLM을 코딩에 어떻게 활용하는지를 대략적으로 적어보고자 한다.

전제

당연하지만 내가 LLM을 활용하는 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있다. 따라서 일반적인 다른 코딩에는 잘 맞지 않을 수도 있다. 내가 주로 다루는 작업이란 협업자들이나 소비자들과 주로 서면을 통해 비동기로 의사 소통을 하는 오픈 소스 프로젝트이며, 그것도 주로 애플리케이션 개발이 아닌 라이브러리 개발이다. 주로 사용하는 프로그래밍 언어는 TypeScript로 상당히 LLM이 잘 다루는 축에 속한다. 한편으로는 비교적 기존 지식의 도태가 빠르게 이뤄지는 생태계이기도 해서, 어떤 면에서는 불리한 점도 있다고 볼 수 있다.

어찌 되었든 내 LLM 활용 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있으나, 그럼에도 이 글에서는 일반적인 코딩에 두루 적용할 수 있는 팁을 공유하려고 노력했다.

맥락이 왕이다

LLM 활용에 있어서 모델 자체의 성능 등 여러 고려 사항들이 있지만, 사람들이 LLM을 사용하는 것을 관찰했을 때 가장 흔히 놓치는 것이 충분한 맥락의 제공이다. 사람들은 자신이 무언가를 판단할 때 얼마나 많은 맥락에 의존하는지 의식하지 못한다. 머릿속 사소한 기억 조각부터 내가 접근 가능한 최신 문서들, 이슈에 담긴 캡처 이미지까지… 이들 대부분은 LLM이 기본적으로는 혼자서 접근할 수 없는 경우가 많다. 제아무리 LLM이 똑똑하다고 해도 필요한 맥락이 주어지지 않으면 엉뚱한 결과를 내놓기 마련이다. 주변에서 「LLM은 코딩을 너무 못한다」고 토로하는 경우를 보면 정말로 LLM이 풀기 어려운 문제를 주로 다루는 분들도 계셨지만, 대부분의 경우에는 LLM에게 충분히 맥락을 제공하지 못해서 그럴 때가 많았다.

아마도 이 글에서 다룰 대부분의 이야기는 결국 「어떻게 LLM에게 맥락을 잘 제공할까?」라는 고민에서 나온 팁들이라고 봐도 무방하다.

내가 쓰는 모델과 코딩 에이전트

2025년 9월 현재, 나는 거의 대부분의 작업에 Claude Code를 사용한다. 지난 몇 달 간 Claude Code를 주로 사용해 왔으며, 주기적으로 다른 도구들도 시도해 보고 있으나 여전히 Claude Code가 나에게 가장 맞다고 판단했다. 실은, 나는 대부분의 프로그래머에게 Claude Code가 가장 적합할 거라고 생각한다. 엄밀한 벤치마크에 근거한 것은 아니고, 체감일 따름이지만… 이유는 다음과 같다:

  • 다른 모델들은 도구 호출을 잘 못한다. 도구를 제공하면 필요한 순간에 도구를 활용할 수 있어야 한다. Claude 모델들은 이 부분에서 확실히 뛰어나다. 도구 호출은 풍부한 맥락을 제공하기 위해서는 반드시 필요하기 때문에, 도구 호출의 성능이 떨어지면 결과적으로 LLM을 동반한 코딩 자체의 성과가 떨어진다.

  • 다른 모델들은 질답이 길어질 때 성능이 떨어진다. 적어도 나는 LLM과 코딩할 때 한 방(one-shot)에 결과를 내놓는 방식, 즉 바이브 코딩(vibe coding)을 선호하지 않는다. 따라서 모델의 멀티턴(multi-turn) 성능이 중요한데, 다른 모델들은 질답이 길어짐에 따라 앞서 했던 이야기를 망각하는 경향이 있다.

  • 똑같이 Claude의 모델을 사용하더라도 Claude Code 자체의 성능이 다른 LLM 코딩 에이전트에 비해 뛰어나다. 이건 파인 튜닝이나 시스템 프롬프트 등에 비법 소스가 있기 때문이라고 여겨진다. 이 때문에 Claude Code를 Claude가 아닌 모델과 함께 쓸 수 있게 해주는 비공식 프록시 등도 존재한다.

물론, Claude 및 Claude Code에도 단점이 있다:

  • 상대적으로 문맥 윈도(context window)가 짧다. 그래서 토큰을 아껴야 한다. Claude Code의 대화 압축(conversation compaction)은 그럭저럭 잘 동작하는 편이긴 해도, 여전히 스트레스이긴 하다. (이를테면, 대화 압축은 영어로 이뤄지기 때문에 후속 세션에서는 갑자기 영어로 대답하기 시작한다. 아, 나는 프롬프트를 한국어로 적는다.)

  • 일부 LLM 코딩 에이전트들이 제공하는 LSP 지원이 아직 없다. 따라서 타입 오류나 린트 오류 등을 명령어 실행 등을 통해 따로 보여줄 수 있어야 한다. 대신 Claude Code에서는 (hooks) 기능이 제공되므로, 이를 잘 활용하면 어느 정도는 비슷한 효과를 볼 수 있다.

하지만 장점이 단점을 압도하기 때문에 앞으로도 상황에 큰 변화가 없다면 Claude Code를 주로 쓸 것 같다.

세부 지시는 서면으로

지시(prompt)는 세부적일 수록 좋다. 지시가 한 문장으로 끝난다면 좋은 지시가 아닐 가능성이 높다. 때로는 지시를 만드는 지시가 필요하기도 하다. 나의 지시 방식은 다음과 같다.

우선 대부분의 지시는 GitHub 이슈에 작성한다. 필요한 충분한 링크를 제공해야 하고, 가급적이면 캡처 이미지나 도표와 같은 시각적 정보에는 크게 의존하지 않아야 한다. 물론, 이슈는 기본적으로 LLM을 위한 것이 아니라 사람을 위한 것이므로 LLM에게만 필요한 정보는 이슈에 담고 싶지 않을 수 있다. 그런 건 이슈에 담지 않아도 된다.[1] 이미 남이 만든 이슈가 있다면 그 이슈를 활용해도 된다. 남이 만든 이슈에 맥락이 충분하지 않다고 여겨지면, 댓글로 맥락을 보충한다.

때로는 이슈 자체도 LLM으로 작성하기도 한다. 관련 문서나 상황을 충분히 공유하여 이슈를 작성해 달라고 하는 식이다. 예를 들면, 다음은 내가 만든 이메일 전송 라이브러리인 UpyoPlunk 트랜스포트를 추가하는 이슈 #11 Plunk transport를 작성하기 위해 사용했던 지시문이다:

Plunk라는 이메일 전송 프로바이더가 있습니다. Plunk의 트랜스포트를 Upyo에 추가하면 좋을 것 같은데요. 이슈 트래커에 일단 이슈를 먼저 만들고자 합니다. 이슈 제목과 내용을 영어로 작성해 주실 수 있을까요? 너무 문제 정의 및 해결책 제시가 구분되는 식의 형식적인 글 대신, 좀 더 사람이 쓴 것 같은 자연스러운 톤으로 부탁드립니다. 너무 길 필요도 없고요. 한두 문단 정도면 충분할 것 같습니다.

참고 링크:

단, 이 때 나는 Claude의 프로젝트 기능을 이용해서 Upyo의 기존 문서를 RAG로 제공한 상태에서 지시를 했다는 점을 밝힌다.

그 다음에 Claude Code의 계획 모드(plan mode)[2]에서 다음과 같이 지시한다.

https://github.com/dahlia/upyo/issues/11 이슈를 구현해야 합니다. 이슈 본문과 본문에서 링크된 관련 링크들을 모두 살펴본 뒤, Upyo 프로젝트에 Plunk 트랜스포트를 추가할 구현 계획을 세부적으로 세워주세요.

정확히는, 나는 이슈 링크를 제공하는 대신 이슈 번호만 제공하고 GitHub MCP를 이용해서 이슈를 직접 읽도록 하는 걸 선호한다. HTML이 아니라 Markdown 형식으로 이슈를 읽기 때문에 본문에 걸린 링크 등을 더 잘 따라가기 때문이다. 링크가 정말 중요한 경우 Claude Code의 지시에서 한 번 더 적기도 한다. 이슈에 미처 적지 못했던 LLM만을 위한 정보도 이 때 모두 적는다.

구현할 때 살펴봐야 할 소스 파일이 무엇인지 잘 알고 있다면, 그런 정보도 함께 제공하면 더 좋다. LLM이 코드베이스를 탐색하느라 삽질을 훨씬 덜 하고, 토큰도 덜 쓰기 때문이다.

나는 GitHub 이슈를 세부 지시를 적는 용도로 썼지만, PLAN.md 같은 문서 파일을 만들어서 거기다 적는 방법도 많이 쓰인다고 알고 있다.

설계는 사람이, 구현은 LLM이

내가 LLM을 코딩에 활용할 때의 기본적인 원칙은 큰 설계는 내 스스로 하고, 세부적인 구현은 LLM에게 맡긴다는 것이다. 지시할 때는 설계 의도를 정확히 제시하고, 구현 과정에서 실수할 수 있을 것 같은 우려점에 대해서 충분히 짚고 넘어간다. 특히, 나는 프로젝트를 처음 시작할 때 디렉터리나 패키지 구조를 여전히 직접 손으로 할 때가 많다. (하지만 이 부분은 내가 원체 LLM 시대 이전부터 쿠키커터 류의 프로젝트 템플릿도 좋아하지 않았기 때문일 수도 있다. 템플릿을 쓰든 LLM을 쓰든 내 마음에 들게 나오지 않기에.)

LLM은 자신이 가장 익숙한 기술로 문제를 해결하려는 경향이 있기 때문에, 기술 선택에 있어서도 명시적으로 지시를 하는 것이 좋다. 아직은 사소한 라이브러리 하나조차도 사람이 검토할 필요가 있다. LLM에게 모든 것을 맡기다 보면 보안 패치도 안 된 옛날 버전을 가져다 쓰거나 하는 일이 흔하기 때문이다. 나 같은 경우에는 후술할 AGENTS.md 문서에서 라이브러리를 설치하기 전에는 npm view 명령어를 통해 해당 패키지의 최신 버전이 무엇인지 먼저 확인하라는 지시를 포함시키기도 한다.

추상화를 할 때도, 적어도 API 설계는 여전히 내가 직접할 때가 많다. 어째서일까, LLM이 설계한 API는 아직은 별로일 때가 많다. 내가 받은 인상은, LLM은 구현해 나가며 필요할 때 API를 즉석(ad-hoc)에서 설계할 때가 잦다는 것이다. 물론, 사람이라도 이런 방식을 선호할 수 있는데, 그런 경우에는 LLM이 설계한 API에 불만이 없을지도 모른다. 하지만 적어도 내 경우에는 불만족스러울 때가 많다.

결국에는 LLM을 만능 노예가 아니라 똑똑하기도 하지만 미숙한 점도 많은 동료로 보고 LLM이 서툰 부분에는 최대한 사람이 도와서 일을 해낸다는 관점이 필요한 것 같다.

AGENTS.md

대부분의 LLM 코딩 에이전트들은 AGENTS.md 내지는 그에 준하는 기능을 제공하고 있다. 예를 들어 Claude Code는 CLAUDE.md 파일을 바라보며, Gemini CLIGEMINI.md를 바라보는 식인데, 점차 AGENTS.md 파일로 표준화되고 있는 추세이다. 나는 특정 벤더에서만 쓰는 파일들을 모조리 AGENTS.md로 심볼릭 링크를 건 다음 AGENTS.md 파일만을 정본으로 삼게 하고 있다. 이렇게 하면 나와 다른 LLM 코딩 에이전트를 사용하는 협업자들과 같은 지침을 공유할 수 있다.

아무튼, AGENTS.md 문서의 역할은 간단하다. 이 프로젝트에 대한 지침, 즉 시스템 프롬프트이다. 대부분의 LLM 코딩 에이전트들은 이 파일을 자동으로 생성하는 기능을 제공하는데, 안 쓸 이유는 없다. 자동으로 생성하게 한 후, 잘못된 부분만 고쳐서 써도 된다. 그보다 더 중요한 건 시간이 흐르면서 AGENTS.md 문서가 낡게 되는 것을 피하는 것이다. AGENTS.md 문서는 꾸준히 갈고 닦아야 한다. 특히, 대규모 리팩터링이 있거나 한 뒤에는 반드시 AGENTS.md 문서를 갱신해야 한다. 이 지시 자체도 AGENTS.md 문서에 넣어두는 게 좋다.

그러면 AGENTS.md 문서에는 어떤 내용을 넣을까? 나의 경우에는 다음과 같은 내용을 넣는다:

  • 프로젝트의 목표와 개요. 저장소 URL을 적어두는 것도 의외로 GitHub MCP 등을 활용할 때 쓸모가 있다.
  • 프로젝트가 사용하는 개발 도구. 예를 들어 npm은 절대 쓰지 않으며 pnpm만 쓴다는 식의 지시를 포함한다.
  • 디렉터리 구조와 각 디렉터리의 역할.
  • 빌드 및 테스트 방법. 특히, 그 프로젝트만의 특수한 절차가 있다면 반드시 기술한다. 예를 들어, 빌드나 테스트 전에 반드시 코드 생성을 해야 한다면, 이에 관해 적어야 한다—물론, 가장 좋은 것은 빌드 스크립트로 그러한 절차를 자동화하는 것이다. 그 편이 사람에게도 좋고, 토큰을 아끼는 데에도 좋다.
  • 코딩 스타일이나 문서 스타일. 포매터를 쓰는 방법을 적는 것도 좋다.
  • 개발 방법론. 이를테면 버그를 고칠 때는 회귀 테스트를 먼저 작성하고, 테스트가 실패하는 것으로 버그가 재현되는 것을 확인한 다음에 버그 수정을 하라는 지침 같은 것들.

일단은 위와 같이 시작하고, LLM 코딩 에이전트를 활용하면서 눈에 밟히는 실수들을 LLM이 할 때마다 지침을 추가하는 것을 권한다. 예를 들어, 나는 TypeScript 프로젝트에서 any 타입이나 as 키워드를 피하라는 지침을 추가하는 편이다.

다음은 내가 관리하는 프로젝트들의 AGENTS.md 문서들이다:

문서 제공

LLM에 지식 컷오프가 있다는 건 잘 알려져 있다. 방금 갓 나온 모델이 아닌 한, 내가 쓰는 라이브러리나 런타임 등의 API, CLI 도구 등에 대해 다소 낡은 지식을 가지고 있을 가능성이 높다는 뜻이다. 게다가 만약 비주류 프로그래밍 언어나 프레임워크 등을 쓰고 있다면 이 문제는 더욱 커진다.

따라서 내가 사용하는 프로그래밍 언어나 프레임워크 등에 대한 지식을 제공해야 하는데, 가장 쉽고 효율적인 방법은 Context7을 MCP로 붙이는 것이다. Context7은 다양한 기술 문서를 비교적 최신판으로 유지하면서 벡터 데이터베이스에 색인하고, LLM이 요청할 경우 관련된 문서 조각을 제공하는 RAG 서비스이다. 새로운 문서를 추가하는 것도 쉬워서, 만약 내가 필요한 문서가 등록되어 있지 않다면 얼마든지 새로 추가해서 사용할 수도 있다. 다만, 특별히 지시하지 않는 한 Context7을 따로 활용하지 않을 때도 있기 때문에, 「Context7 MCP를 통해 관련 문서를 확인해 보라」는 식의 지시가 필요할 수 있다.

RFC 같은 기술 명세 문서를 제공할 때는 평문(plain text) 형식이 제공되므로 평문 문서의 링크를 제공하는 게 좋다. 나의 경우 연합우주(fediverse) 관련 개발을 많이 하다 보니 FEP 문서를 제공해야 할 일이 많은데, 이 때도 HTML으로 렌더링 된 웹 페이지가 아닌 Markdown 소스 파일의 링크를 직접 제공하는 식으로 사용하고 있다.

이 외에도 웹사이트에서 /llms.txt/llms-full.txt 파일을 제공하는 관행이 퍼지고 있으므로, 이를 활용하는 것도 좋다. (내가 만든 소프트웨어 라이브러리들의 경우 프로젝트 웹사이트에서 모두 /llms.txt/llms-full.txt 파일을 제공하고 있다.)

다만 아무리 LLM에게 친화적인 평문이라고 해도 기술 문서 전체를 다 제공하는 건 아무래도 토큰 낭비가 심하기 때문에, Context7을 쓸 수 있다면 Context7을 쓰는 것을 추천한다.

계획 모드의 활용

Claude Code를 포함해 최근의 많은 LLM 코딩 에이전트는 계획 모드를 제공한다. 냅다 구현하는 것을 방지하고, 구현 계획을 LLM 스스로 세우게 한 뒤 사람이 먼저 검토할 수 있게 하는 것이다. 특히, 나는 계획 모드에서는 비싼 Claude Opus 4.1을 사용하고 실제 구현에서는 비교적 저렴한 Claude Sonnet 4를 사용하는 “Opus Plan Mode”를 사용하고 있다.[3]

나는 계획을 면밀히 검토하고 조금이라도 내 성에 차지 않으면 얼마든지 계획 수정을 요구한다. 작업에 따라 다르지만, 어떤 작업이든 적어도 서너 번 이상은 수정을 요구하는 것 같다. 반대로 얘기하면, 이 정도로 계획을 다듬지 않으면 내가 원하는 방향으로 구현하지 않을 가능성이 높다. LLM은 나와는 다른 전제를 품을 때가 많아서, 여러 세부 계획에서 나와는 동상이몽을 하고 있을 가능성이 높다. 그런 것들을 사전에 최대한 제거하여 내 의도에 일치시켜야 한다.

알아서 피드백 루프를 돌게

LLM 코딩 에이전트로 개발을 할 때 가장 중요하다고 여겨지는 부분은 바로, 스스로 웬만큼 방향을 조정할 수 있도록 여건을 마련하는 일이다. LLM의 각종 구현 실수를 하나하나 내가 지적하는 게 아니라, 각종 자동화된 테스트와 정적 분석을 통해 스스로 깨닫고 구현을 고칠 수 있도록 해주는 것이다.

예를 들어, CSS 버그를 고칠 때를 생각해 보자. LLM이 CSS 코드를 고치게 한 뒤, 내가 웹 브라우저를 확인하는 방식은 너무 번거롭다. 대신, Playwright MCP를 붙여서 스스로 화면을 볼 수 있게 하는 게 낫다. 요는, LLM의 작업 결과가 요구 사항을 충족하는지를 스스로 판단할 수 있게 하여, 요구 사항이 충족될 때까지 작업을 계속하게 만드는 것이다.

비슷한 이유에서, 구현하기에 앞서 테스트 코드를 먼저 작성하도록 지시하는 것이 여러모로 편하다. 테스트 코드를 작성하는 과정까지만 사람이 지켜보면 되고, 그 뒤는 상대적으로 신경을 덜 써도 되기 때문이다. 실은, 나는 LLM 코딩 에이전트를 활용할 때도 가끔은 테스트를 직접 짜기도 한다. 프롬프트로 요구 사항을 정확하게 검증하는 테스트 코드를 짜게 하는 것보다 내가 직접 테스트 코드를 짜는 게 빠르겠다고 느낄 때 그렇다.

이런 작업 흐름을 선호하다 보니, 좀 더 엄밀한 타입 시스템을 갖춘 프로그래밍 언어, 좀 더 엄격한 린트 규칙 등이 LLM 코딩 에이전트를 활용할 때 훨씬 유리하다고 생각하게 되었다. LLM 시대 이전에도 생각은 비슷하긴 했지만 말이다.

가끔은 손 코딩

하지만 여전히 LLM에 많은 한계점이 있기 때문에, 나는 아직도 가끔은 손 코딩을 한다. API를 설계할 때도 그렇고, 엄밀한 테스트 코드를 짜고 싶을 때도 그렇다. (LLM은 테스트 코드를 좀 대충 짜는 경향이 있다.) 그리고 무엇보다, 재밌을 것 같은 코딩은 내가 한다!

바이브 코딩에 깊게 심취했다가 코딩의 재미가 사라졌다는 소프트웨어 프로그래머들의 얘기를 종종 듣는다. 내 생각에는, 재미있는 부분은 LLM에게 시키지 않는 게 좋다. 결과의 품질 때문이 아니라, 소프트웨어 프로그래머로서 모티베이션을 유지하기 위해서 그렇다. 재미 없고 지루한 부분, 그러니까 코딩하기 싫어지게 하는 작업에서 최대한 LLM을 활용하는 것이 LLM과 공존하는 좋은 전략이 아닐까 생각한다. 뭐, 적어도 내게는 이 방식이 잘 먹히는 것 같다.


  1. 이건 한 가지 팁인데, LLM에게만 필요한 정보를 <!-- … --> 주석 안에 적는 방법도 있다. ↩︎

  2. Shift + Tab을 두 번 누르면 계획 모드에 진입할 수 있다. ↩︎

  3. Claude Code에서 /model 명령어를 통해 고를 수 있다. ↩︎

藤井太洋, Taiyo Fujii's avatar
藤井太洋, Taiyo Fujii

@taiyo@ostatus.taiyolab.com

行政と共産党の人たちが退出して(本当に一人もいなくなるんだよ!)作家のサミットに移行。
劉慈欣、キム・チョヨプ、サイバーパンク2077からはイゴール・ザリンスキ、文学界から杨晨、司会は科幻世界の曾筱洁。

藤井太洋, Taiyo Fujii's avatar
藤井太洋, Taiyo Fujii

@taiyo@ostatus.taiyolab.com

キム・チョヨプさんに、三体の韓国での受容について質問。劉慈欣と三体は中国SFのアイコンになってるとのこと。ハードSFについても言及。

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

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

@kodingwarrior 정말로 dadjokes.social 하나 만드셔야겠어요.

藤井太洋, Taiyo Fujii's avatar
藤井太洋, Taiyo Fujii

@taiyo@ostatus.taiyolab.com

2025銀河科幻大会、開幕します。
劉慈欣と話しました。AIの話。

木野どど松's avatar
木野どど松

@ddquino@ddoskey.com

物書堂アプリの UI がとんでもない感じに変わって ​:exclamation_question_mark:​ となっていたが 1 日で戻っていてちょっと笑った。誠実w

App Store の更新画面のスクリーンショット。「辞書 by 物書堂」「前回のアップデートでは、皆さまと共に 17 年にわたり創り上げてきた UI を台無しにしてしまい本当に申し訳ありませんでした。今回のアップデートでは以前の UI を改良したものにいたしました。今後はこの UI に対して不用意に手を入れないようにいたします。申し訳ありませんでした。」
ALT text detailsApp Store の更新画面のスクリーンショット。「辞書 by 物書堂」「前回のアップデートでは、皆さまと共に 17 年にわたり創り上げてきた UI を台無しにしてしまい本当に申し訳ありませんでした。今回のアップデートでは以前の UI を改良したものにいたしました。今後はこの UI に対して不用意に手を入れないようにいたします。申し訳ありませんでした。」
Chee Aun 🤔's avatar
Chee Aun 🤔

@cheeaun@mastodon.social · Reply to Chee Aun 🤔's post

Composing:
- Only can embed quote post by pressing Quote button/menu?
- How about pasting post link in composer? Immediately resolve to quote post or ask user if want to embed? What if there's already a quote post embeded while pasting a post link? 🤪
- After embed, can remove? (Mastodon web shows “x” button). Then how to add back or undo?

Chee Aun 🤔's avatar
Chee Aun 🤔

@cheeaun@mastodon.social · Reply to Chee Aun 🤔's post

🧠 Quick brain dump.

Rendering:
- For official Mastodon web & apps, links to posts will still open web view instead of native view. They don’t unfurl.
- Do we stop rendering non-native quote posts if the post contains a native quote post? Or render both? How to (visually?) differentiate native vs non-native?
- Is quotes count separated from boost count? Mastodon web sums them up when shown on timeline, separate them on post page.

Haze's avatar
Haze

@nebuleto@hackers.pub

"두통과 함께하는 사람들"은 다음 주(22일 ~ 28일) 편두통 인식 개선 주간을 맞이해서 광화문에서 커피차 이벤트를 진행합니다! 주변에 많은 공유와 참여 부탁드려요.

  • 📆 언제? 2025년 9월 22일 (월요일) 오전 10시 ~ 오후 2시
  • 📍 어디서? 광화문 한국프레스센터 광장 [네이버 지도]
  • 📋 무엇을 하나요? 편두통 질환과 캠페인을 소개하며 다양한 기념품(안대와 귀마개 등)과 음료를 드립니다! 🎁🥤
  • 왜 하나요? 국제적으로 진행하는 캠페인의 일환으로 편두통에 대한 오해를 해소하고 편두통을 알리는걸 목표로 합니다.

오랫동안 열심히 준비하던 것 중 하나입니다. 부스 놀러와주시면 기쁠 것 같아요.

편두통, 오해말고 이해를! 당일 배포될 팜플렛의 표지입니다.
ALT text details편두통, 오해말고 이해를! 당일 배포될 팜플렛의 표지입니다.
AmaseCocoa's avatar
AmaseCocoa

@cocoa@hackers.pub

First-version of My ActivityPub Implemention

https://fedimovie.com/w/a58Hs4BbCX4wvVfWBTjT1m

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

@eatch.dev@bsky.brid.gy

☘️ 잇창명 메인포스트 트리 > 비상연락망 * 마스토돈 @EatChangmyeong@planet.moe (브리지) * Hackers' Pub @eatch (브리지 준비 중) * 디스코드 eatchangmyeong (개인 서버) * 스팀 eatchangmyeong * 이메일 dlaud5379@naver.com 카카오톡 아이디는 비계 공지를 확인하거나 DM으로 요청해 주세요.

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

@EatChangmyeong@planet.moe

연합우주에 다른 계정을 만들었는데 자주 쓸지 모르겠네요

@eatch

AmaseCocoa's avatar
AmaseCocoa

@cocoa@hackers.pub

Deploy AnywhereなActivityPubサーバー書いてる

Hollo :hollo:'s avatar
Hollo :hollo:

@hollo@hollo.social

Hollo 0.6.11 significantly improves Bluesky interoperability via BridgyFed! Fixed AT Protocol URI parsing issues that were affecting various cross-platform interactions—not just likes, but overall federation with Bluesky users. 🌉

mary🐇's avatar
mary🐇

@mary.my.id@bsky.brid.gy

optique.dev seems interesting for cli opt parsing

Optique

Rachel Rawlings's avatar
Rachel Rawlings

@LinuxAndYarn@mastodon.social

Did I just do a dramatic reading of this for my wife after she heard me laughing--without even knowing what came after the fourth paragraph?

Reader, I did.

mcsweeneys.net/articles/the-em

오브젝티프's avatar
오브젝티프

@objectif@mitir.social

비슷하게 "짭"도 흥미롭다고 생각.

가짜 ← 假(거짓 가)에서 나온 것이 확실
짜가 ← 글자를 뒤집어서 더 모욕적인 멸칭
짝퉁 ← 더 모욕적인 멸칭. 아마도 미련퉁이, 눈퉁이(눈탱이) 등과 비슷한 조어 원리. *표준국어대사전에 실렸다.*
짭퉁 ← 더 변형됨
짭 ← 더 줄어듦

원래 "가짜"는 한국어에서 "그 한자를 써야 할 대상"을 표현할 때 자주 쓰이는 "-짜" 조어다. 이런 조어는 수두룩하다. (진짜, 공짜, 괴짜, 대짜, 퇴짜, 초짜, 생짜, 등등.) 음식점에서 주문할 때 "대짜, 중짜, 소짜" 하는 것도 정확히 여기 해당한다.

즉, 의미를 담고 있는 부분은 '가' 부분이다. 그런데 "짭"에서는 그 부분이 완전히 소멸해 버렸다. 그러면서도 인터넷 세대라면 누구나 "짭"이 뭔지 알아듣는 데에 아무 문제가 없다. "짤"이 원래 의미에서 완전히 이탈한 것과 비슷하다. 흥미롭죠.


RE: https://buttersc.one/notes/acpg9z3oec

헬렐's avatar
헬렐

@guiltyone@buttersc.one

짤<이란단어가 웃긴게
디씨인사이드가 디지털카메라커뮤니티로서만 기능했던 시절
짤림방지용사진
짤방

이 됐단게

bgl gwyng's avatar
bgl gwyng

@bgl@hackers.pub

IQ 테스트로 인간을 판별하고 구분짓는것에는 불편한 느낌이 들지만(그게 쓸모없단 얘긴 아님), 그와중에 우리집 강아지 견종인 요크셔테리어가 똑똑한 견종으로 분류되는 글을 보면 진심으로 뿌듯하다...

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

@hongminhee@hollo.social

I updated iOS/iPadOS/macOS to version 26, but Liquid Glass is still a bit hard on the eyes. Will I ever get used to this new look and feel…?

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

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

Upyo 0.3.0をリリースしました。複数のメールプロバイダー間で自動フェイルオーバーができるプールトランスポートと、ResendとPlunkのサポートを追加。メール配信の信頼性が大幅に向上します。

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

@hongminhee@hackers.pub


Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.

Pool transport for multi-provider resilience

The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.

The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.

import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
import { SendGridTransport } from "@upyo/sendgrid";

// Define individual providers
const primaryProvider = new MailgunTransport({
  apiKey: "your-mailgun-api-key",
  domain: "mg.example.com",
});

const backupProvider = new SendGridTransport({
  apiKey: "your-sendgrid-api-key",
});

const emergencyProvider = new SmtpTransport({
  host: "smtp.example.com",
  port: 587,
  auth: { user: "user@example.com", pass: "password" },
});

// Create pool transport
const pool = new PoolTransport({
  strategy: "priority",
  transports: [
    { transport: primaryProvider, priority: 100 },
    { transport: backupProvider, priority: 50 },
    { transport: emergencyProvider, priority: 10 },
  ],
  maxRetries: 3,
});

const receipt = await pool.send(message);

This transport proves particularly valuable for high-availability systems that cannot tolerate email delivery failures. It also enables cost optimization by routing bulk emails to more economical providers while sending transactional emails through premium services. Organizations migrating between email providers can use weighted distribution to gradually shift traffic from one provider to another. The pool transport handles resource cleanup properly through AsyncDisposable support and provides comprehensive error reporting that aggregates failures from all attempted providers.

For detailed configuration options and usage patterns, refer to the pool transport documentation.

Installation

npm  add     @upyo/pool
pnpm add     @upyo/pool
yarn add     @upyo/pool
deno add jsr:@upyo/pool
bun  add     @upyo/pool

Resend transport

The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.

One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.

import { ResendTransport } from "@upyo/resend";

const transport = new ResendTransport({
  apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});

const receipt = await transport.send(message);

Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.

The Resend transport guide provides comprehensive documentation on configuration and advanced features.

Installation

npm  add     @upyo/resend
pnpm add     @upyo/resend
yarn add     @upyo/resend
deno add jsr:@upyo/resend
bun  add     @upyo/resend

Plunk transport

The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.

For many teams, the ability to self-host email infrastructure is crucial for compliance or data sovereignty reasons. Plunk's self-hosted option runs as a Docker container using the driaug/plunk image, giving you complete control over your email infrastructure while maintaining a simple, modern API. The same codebase works seamlessly with both cloud and self-hosted instances, requiring only a different base URL configuration.

import { PlunkTransport } from "@upyo/plunk";

// Cloud-hosted
const cloudTransport = new PlunkTransport({
  apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});

// Self-hosted
const selfHostedTransport = new PlunkTransport({
  apiKey: "your-self-hosted-api-key",
  baseUrl: "https://mail.yourcompany.com/api",
});

The Plunk transport includes the production features you'd expect, such as retry logic with exponential backoff, comprehensive error handling, and support for attachments (up to 5 per message as per Plunk's API limits). Message organization through tags helps track different types of emails, while priority levels ensure urgent messages receive appropriate handling. The transport also supports request cancellation through AbortSignal, allowing your application to gracefully handle timeouts and user-initiated cancellations.

Complete documentation and deployment guidance is available in the Plunk transport documentation.

Installation

npm  add     @upyo/plunk
pnpm add     @upyo/plunk
yarn add     @upyo/plunk
deno add jsr:@upyo/plunk
bun  add     @upyo/plunk

Migration guide

All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "sender@example.com",
  to: "recipient@example.com",
  subject: "Hello from Upyo!",
  content: { text: "Works with any transport!" },
});

const receipt = await transport.send(message);

What's next

We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.


For the complete changelog and technical details, see CHANGES.md.

For questions or issues, please visit our GitHub repository.

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

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

Upyo 0.3.0을 릴리스했습니다. 이제 여러 이메일 제공 업체 간 자동 페일오버를 가능하게 해주는 풀 트랜스포트와 Resend, Plunk 트랜스포트가 추가되었습니다. 한 이메일 제공 업체가 다운되어도 다른 이메일 제공 업체를 통해 이메일이 계속 전송될 수 있습니다.

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

@hongminhee@hackers.pub


Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.

Pool transport for multi-provider resilience

The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.

The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.

import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
import { SendGridTransport } from "@upyo/sendgrid";

// Define individual providers
const primaryProvider = new MailgunTransport({
  apiKey: "your-mailgun-api-key",
  domain: "mg.example.com",
});

const backupProvider = new SendGridTransport({
  apiKey: "your-sendgrid-api-key",
});

const emergencyProvider = new SmtpTransport({
  host: "smtp.example.com",
  port: 587,
  auth: { user: "user@example.com", pass: "password" },
});

// Create pool transport
const pool = new PoolTransport({
  strategy: "priority",
  transports: [
    { transport: primaryProvider, priority: 100 },
    { transport: backupProvider, priority: 50 },
    { transport: emergencyProvider, priority: 10 },
  ],
  maxRetries: 3,
});

const receipt = await pool.send(message);

This transport proves particularly valuable for high-availability systems that cannot tolerate email delivery failures. It also enables cost optimization by routing bulk emails to more economical providers while sending transactional emails through premium services. Organizations migrating between email providers can use weighted distribution to gradually shift traffic from one provider to another. The pool transport handles resource cleanup properly through AsyncDisposable support and provides comprehensive error reporting that aggregates failures from all attempted providers.

For detailed configuration options and usage patterns, refer to the pool transport documentation.

Installation

npm  add     @upyo/pool
pnpm add     @upyo/pool
yarn add     @upyo/pool
deno add jsr:@upyo/pool
bun  add     @upyo/pool

Resend transport

The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.

One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.

import { ResendTransport } from "@upyo/resend";

const transport = new ResendTransport({
  apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});

const receipt = await transport.send(message);

Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.

The Resend transport guide provides comprehensive documentation on configuration and advanced features.

Installation

npm  add     @upyo/resend
pnpm add     @upyo/resend
yarn add     @upyo/resend
deno add jsr:@upyo/resend
bun  add     @upyo/resend

Plunk transport

The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.

For many teams, the ability to self-host email infrastructure is crucial for compliance or data sovereignty reasons. Plunk's self-hosted option runs as a Docker container using the driaug/plunk image, giving you complete control over your email infrastructure while maintaining a simple, modern API. The same codebase works seamlessly with both cloud and self-hosted instances, requiring only a different base URL configuration.

import { PlunkTransport } from "@upyo/plunk";

// Cloud-hosted
const cloudTransport = new PlunkTransport({
  apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});

// Self-hosted
const selfHostedTransport = new PlunkTransport({
  apiKey: "your-self-hosted-api-key",
  baseUrl: "https://mail.yourcompany.com/api",
});

The Plunk transport includes the production features you'd expect, such as retry logic with exponential backoff, comprehensive error handling, and support for attachments (up to 5 per message as per Plunk's API limits). Message organization through tags helps track different types of emails, while priority levels ensure urgent messages receive appropriate handling. The transport also supports request cancellation through AbortSignal, allowing your application to gracefully handle timeouts and user-initiated cancellations.

Complete documentation and deployment guidance is available in the Plunk transport documentation.

Installation

npm  add     @upyo/plunk
pnpm add     @upyo/plunk
yarn add     @upyo/plunk
deno add jsr:@upyo/plunk
bun  add     @upyo/plunk

Migration guide

All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "sender@example.com",
  to: "recipient@example.com",
  subject: "Hello from Upyo!",
  content: { text: "Works with any transport!" },
});

const receipt = await transport.send(message);

What's next

We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.


For the complete changelog and technical details, see CHANGES.md.

For questions or issues, please visit our GitHub repository.

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

@hongminhee@hollo.social

Just released Upyo 0.3.0! Now with pool transport for multi-provider failover, plus Resend and Plunk support. Your emails can now automatically fail over between providers when one goes down.

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

@hongminhee@hackers.pub


Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.

Pool transport for multi-provider resilience

The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.

The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.

import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
import { SendGridTransport } from "@upyo/sendgrid";

// Define individual providers
const primaryProvider = new MailgunTransport({
  apiKey: "your-mailgun-api-key",
  domain: "mg.example.com",
});

const backupProvider = new SendGridTransport({
  apiKey: "your-sendgrid-api-key",
});

const emergencyProvider = new SmtpTransport({
  host: "smtp.example.com",
  port: 587,
  auth: { user: "user@example.com", pass: "password" },
});

// Create pool transport
const pool = new PoolTransport({
  strategy: "priority",
  transports: [
    { transport: primaryProvider, priority: 100 },
    { transport: backupProvider, priority: 50 },
    { transport: emergencyProvider, priority: 10 },
  ],
  maxRetries: 3,
});

const receipt = await pool.send(message);

This transport proves particularly valuable for high-availability systems that cannot tolerate email delivery failures. It also enables cost optimization by routing bulk emails to more economical providers while sending transactional emails through premium services. Organizations migrating between email providers can use weighted distribution to gradually shift traffic from one provider to another. The pool transport handles resource cleanup properly through AsyncDisposable support and provides comprehensive error reporting that aggregates failures from all attempted providers.

For detailed configuration options and usage patterns, refer to the pool transport documentation.

Installation

npm  add     @upyo/pool
pnpm add     @upyo/pool
yarn add     @upyo/pool
deno add jsr:@upyo/pool
bun  add     @upyo/pool

Resend transport

The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.

One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.

import { ResendTransport } from "@upyo/resend";

const transport = new ResendTransport({
  apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});

const receipt = await transport.send(message);

Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.

The Resend transport guide provides comprehensive documentation on configuration and advanced features.

Installation

npm  add     @upyo/resend
pnpm add     @upyo/resend
yarn add     @upyo/resend
deno add jsr:@upyo/resend
bun  add     @upyo/resend

Plunk transport

The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.

For many teams, the ability to self-host email infrastructure is crucial for compliance or data sovereignty reasons. Plunk's self-hosted option runs as a Docker container using the driaug/plunk image, giving you complete control over your email infrastructure while maintaining a simple, modern API. The same codebase works seamlessly with both cloud and self-hosted instances, requiring only a different base URL configuration.

import { PlunkTransport } from "@upyo/plunk";

// Cloud-hosted
const cloudTransport = new PlunkTransport({
  apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});

// Self-hosted
const selfHostedTransport = new PlunkTransport({
  apiKey: "your-self-hosted-api-key",
  baseUrl: "https://mail.yourcompany.com/api",
});

The Plunk transport includes the production features you'd expect, such as retry logic with exponential backoff, comprehensive error handling, and support for attachments (up to 5 per message as per Plunk's API limits). Message organization through tags helps track different types of emails, while priority levels ensure urgent messages receive appropriate handling. The transport also supports request cancellation through AbortSignal, allowing your application to gracefully handle timeouts and user-initiated cancellations.

Complete documentation and deployment guidance is available in the Plunk transport documentation.

Installation

npm  add     @upyo/plunk
pnpm add     @upyo/plunk
yarn add     @upyo/plunk
deno add jsr:@upyo/plunk
bun  add     @upyo/plunk

Migration guide

All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "sender@example.com",
  to: "recipient@example.com",
  subject: "Hello from Upyo!",
  content: { text: "Works with any transport!" },
});

const receipt = await transport.send(message);

What's next

We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.


For the complete changelog and technical details, see CHANGES.md.

For questions or issues, please visit our GitHub repository.

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

@hongminhee@hackers.pub


Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.

Pool transport for multi-provider resilience

The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.

The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.

import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
import { SendGridTransport } from "@upyo/sendgrid";

// Define individual providers
const primaryProvider = new MailgunTransport({
  apiKey: "your-mailgun-api-key",
  domain: "mg.example.com",
});

const backupProvider = new SendGridTransport({
  apiKey: "your-sendgrid-api-key",
});

const emergencyProvider = new SmtpTransport({
  host: "smtp.example.com",
  port: 587,
  auth: { user: "user@example.com", pass: "password" },
});

// Create pool transport
const pool = new PoolTransport({
  strategy: "priority",
  transports: [
    { transport: primaryProvider, priority: 100 },
    { transport: backupProvider, priority: 50 },
    { transport: emergencyProvider, priority: 10 },
  ],
  maxRetries: 3,
});

const receipt = await pool.send(message);

This transport proves particularly valuable for high-availability systems that cannot tolerate email delivery failures. It also enables cost optimization by routing bulk emails to more economical providers while sending transactional emails through premium services. Organizations migrating between email providers can use weighted distribution to gradually shift traffic from one provider to another. The pool transport handles resource cleanup properly through AsyncDisposable support and provides comprehensive error reporting that aggregates failures from all attempted providers.

For detailed configuration options and usage patterns, refer to the pool transport documentation.

Installation

npm  add     @upyo/pool
pnpm add     @upyo/pool
yarn add     @upyo/pool
deno add jsr:@upyo/pool
bun  add     @upyo/pool

Resend transport

The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.

One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.

import { ResendTransport } from "@upyo/resend";

const transport = new ResendTransport({
  apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});

const receipt = await transport.send(message);

Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.

The Resend transport guide provides comprehensive documentation on configuration and advanced features.

Installation

npm  add     @upyo/resend
pnpm add     @upyo/resend
yarn add     @upyo/resend
deno add jsr:@upyo/resend
bun  add     @upyo/resend

Plunk transport

The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.

For many teams, the ability to self-host email infrastructure is crucial for compliance or data sovereignty reasons. Plunk's self-hosted option runs as a Docker container using the driaug/plunk image, giving you complete control over your email infrastructure while maintaining a simple, modern API. The same codebase works seamlessly with both cloud and self-hosted instances, requiring only a different base URL configuration.

import { PlunkTransport } from "@upyo/plunk";

// Cloud-hosted
const cloudTransport = new PlunkTransport({
  apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});

// Self-hosted
const selfHostedTransport = new PlunkTransport({
  apiKey: "your-self-hosted-api-key",
  baseUrl: "https://mail.yourcompany.com/api",
});

The Plunk transport includes the production features you'd expect, such as retry logic with exponential backoff, comprehensive error handling, and support for attachments (up to 5 per message as per Plunk's API limits). Message organization through tags helps track different types of emails, while priority levels ensure urgent messages receive appropriate handling. The transport also supports request cancellation through AbortSignal, allowing your application to gracefully handle timeouts and user-initiated cancellations.

Complete documentation and deployment guidance is available in the Plunk transport documentation.

Installation

npm  add     @upyo/plunk
pnpm add     @upyo/plunk
yarn add     @upyo/plunk
deno add jsr:@upyo/plunk
bun  add     @upyo/plunk

Migration guide

All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "sender@example.com",
  to: "recipient@example.com",
  subject: "Hello from Upyo!",
  content: { text: "Works with any transport!" },
});

const receipt = await transport.send(message);

What's next

We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.


For the complete changelog and technical details, see CHANGES.md.

For questions or issues, please visit our GitHub repository.

Chee Aun 🤔's avatar
Chee Aun 🤔

@cheeaun@mastodon.social

Kinda disappointed that the resize cursor doesn't smoothly rotate around the curves of the window edges.

Screen recording of the pointer moving along the top left edges of the Finder window on macOS Tahoe. It turns from default cursor to "resize" cursor.
ALT text detailsScreen recording of the pointer moving along the top left edges of the Finder window on macOS Tahoe. It turns from default cursor to "resize" cursor.
AmaseCocoa's avatar
AmaseCocoa

@cocoa@hackers.pub


This tutorial will guide you through building a simple ActivityPub bot using Python. The bot will listen for mentions and, when it receives a message in a specific format, it will schedule and send a reminder back to the user after a specified delay.

For example, if a user mentions the bot with a message like "@reminder@your.host.com 10m check the oven", the bot will reply 10 minutes later with a message like "🔔 Reminder for @user: check the oven".

Prerequisites

To follow this tutorial, you will need Python 3.10+ and the following libraries:

  • apkit[server]: A powerful toolkit for building ActivityPub applications in Python. We use the server extra, which includes FastAPI-based components.
  • uvicorn: An ASGI server to run our FastAPI application.
  • cryptography: Used for generating and managing the cryptographic keys required for ActivityPub.
  • uv: An optional but recommended fast package manager.

You can install these dependencies using uv or pip.

# Initialize a new project with uv
uv init

# Install dependencies
uv add "apkit[server]" uvicorn cryptography

Project Structure

The project structure is minimal, consisting of a single Python file for our bot's logic.

.
├── main.py
└── private_key.pem
  • main.py: Contains all the code for the bot.
  • private_key.pem: The private key for the bot's Actor. This will be generated automatically on the first run.

Code Walkthrough

Our application logic can be broken down into the following steps:

  1. Imports and Configuration: Set up necessary imports and basic configuration variables.
  2. Key Generation: Prepare the cryptographic keys needed for signing activities.
  3. Actor Definition: Define the bot's identity on the Fediverse.
  4. Server Initialization: Set up the apkit ActivityPub server.
  5. Data Storage: Implement a simple in-memory store for created activities.
  6. Reminder Logic: Code the core logic for parsing reminders and sending notifications.
  7. Endpoint Definitions: Create the necessary web endpoints (/actor, /inbox, etc.).
  8. Activity Handlers: Process incoming activities from other servers.
  9. Application Startup: Run the server.

Let's dive into each section of the main.py file.

1. Imports and Configuration

First, we import the necessary modules and define the basic configuration for our bot.

# main.py

import asyncio
import logging
import re
import uuid
import os
from datetime import timedelta, datetime

# Imports from FastAPI, cryptography, and apkit
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization as crypto_serialization

from apkit.config import AppConfig
from apkit.server import ActivityPubServer
from apkit.server.types import Context, ActorKey
from apkit.server.responses import ActivityResponse
from apkit.models import (
    Actor, Application, CryptographicKey, Follow, Create, Note, Mention, Actor as APKitActor, OrderedCollection,
)
from apkit.client import WebfingerResource, WebfingerResult, WebfingerLink
from apkit.client.asyncio.client import ActivityPubClient

# --- Logging Setup ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Basic Configuration ---
HOST = "your.host.com"  # Replace with your domain
USER_ID = "reminder"      # The bot's username

Make sure to replace your.host.com with the actual domain where your bot will be hosted. These values determine your bot's unique identifier (e.g., @reminder@your.host.com).

2. Key Generation and Persistence

ActivityPub uses HTTP Signatures to secure communication between servers. This requires each actor to have a public/private key pair. The following code generates a private key and saves it to a file if one doesn't already exist.

# main.py (continued)

# --- Key Persistence ---
KEY_FILE = "private_key.pem"

# Load the private key if it exists, otherwise generate a new one
if os.path.exists(KEY_FILE):
    logger.info(f"Loading existing private key from {KEY_FILE}.")
    with open(KEY_FILE, "rb") as f:
        private_key = crypto_serialization.load_pem_private_key(f.read(), password=None)
else:
    logger.info(f"No key file found. Generating new private key and saving to {KEY_FILE}.")
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    with open(KEY_FILE, "wb") as f:
        f.write(private_key.private_bytes(
            encoding=crypto_serialization.Encoding.PEM,
            format=crypto_serialization.PrivateFormat.PKCS8,
            encryption_algorithm=crypto_serialization.NoEncryption()
        ))

# Generate the public key from the private key
public_key_pem = private_key.public_key().public_bytes(
    encoding=crypto_serialization.Encoding.PEM,
    format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')

3. Actor Definition

Next, we define the bot's Actor. The Actor is the bot's identity in the ActivityPub network. We use the Application type, as this entity is automated.

# main.py (continued)

# --- Actor Definition ---
actor = Application(
    id=f"https://{HOST}/actor",
    name="Reminder Bot",
    preferredUsername=USER_ID,
    summary="A bot that sends you reminders. Mention me like: @reminder 5m Check the oven",
    inbox=f"https://{HOST}/inbox",      # Endpoint for receiving activities
    outbox=f"https://{HOST}/outbox",    # Endpoint for sending activities
    publicKey=CryptographicKey(
        id=f"https://{HOST}/actor#main-key",
        owner=f"https://{HOST}/actor",
        publicKeyPem=public_key_pem
    )
)

4. Server Initialization

We initialize the ActivityPubServer from apkit, providing it with a function to retrieve our Actor's keys for signing outgoing activities.

# main.py (continued)

# --- Key Retrieval Function ---
async def get_keys_for_actor(identifier: str) -> list[ActorKey]:
    """Returns the key for a given Actor ID."""
    if identifier == actor.id:
        return [ActorKey(key_id=actor.publicKey.id, private_key=private_key)]
    return []

# --- Server Initialization ---
app = ActivityPubServer(apkit_config=AppConfig(
    actor_keys=get_keys_for_actor  # Register the key retrieval function
))

5. In-Memory Storage and Cache

To serve created activities, we need to store them somewhere. For simplicity, this example uses a basic in-memory dictionary as a store and a cache. In a production application, you would replace this with a persistent database (like SQLite or PostgreSQL) and a proper cache (like Redis).

# main.py (continued)

# --- In-memory Store and Cache ---
ACTIVITY_STORE = {} # A simple dict to store created activities
CACHE = {}          # A cache for recently accessed activities
CACHE_TTL = timedelta(minutes=5) # Cache expiration time (5 minutes)

6. Reminder Parsing and Sending Logic

This is the core logic of our bot. The parse_reminder function uses a regular expression to extract the delay and message from a mention, and send_reminder schedules the notification.

# main.py (continued)

# --- Reminder Parsing Logic ---
def parse_reminder(text: str) -> tuple[timedelta | None, str | None, str | None]:
    """Parses reminder text like '5m do something'."""
    # ... (implementation omitted for brevity)

# --- Reminder Sending Function ---
async def send_reminder(ctx: Context, delay: timedelta, message: str, target_actor: APKitActor, original_note: Note):
    """Waits for a specified delay and then sends a reminder."""
    logger.info(f"Scheduling reminder for {target_actor.id} in {delay}: '{message}'")
    await asyncio.sleep(delay.total_seconds()) # Asynchronously wait
    
    logger.info(f"Sending reminder to {target_actor.id}")
    
    # Create the reminder Note
    reminder_note = Note(...)
    # Wrap it in a Create activity
    reminder_create = Create(...)
    
    # Store the created activities
    ACTIVITY_STORE[reminder_note.id] = reminder_note
    ACTIVITY_STORE[reminder_create.id] = reminder_create
    
    # Send the activity to the target actor's inbox
    keys = await get_keys_for_actor(f"https://{HOST}/actor")
    await ctx.send(keys, target_actor, reminder_create)
    logger.info(f"Reminder sent to {target_actor.id}")

7. Endpoint Definitions

We define the required ActivityPub endpoints. Since apkit is built on FastAPI, we can use standard FastAPI decorators. The main endpoints are:

  • Webfinger: Allows users on other servers to discover the bot using an address like @user@host. This is a crucial first step for federation.
  • /actor: Serves the bot's Actor object, which contains its profile information and public key.
  • /inbox: The endpoint where the bot receives activities from other servers. apkit handles this route automatically, directing activities to the handlers we'll define in the next step.
  • /outbox: A collection of the activities created by the bot. but this returns placeholder collection.
  • /notes/{note_id} and /creates/{create_id}: Endpoints to serve specific objects created by the bot, allowing other servers to fetch them by their unique ID.

Here is the code for defining these endpoints:

# main.py (continued)

# The inbox endpoint is handled by apkit automatically.
app.inbox("/inbox")

@app.webfinger()
async def webfinger_endpoint(request: Request, acct: WebfingerResource) -> Response:
    """Handles Webfinger requests to make the bot discoverable."""
    if not acct.url:
        # Handle resource queries like acct:user@host
        if acct.username == USER_ID and acct.host == HOST:
            link = WebfingerLink(rel="self", type="application/activity+json", href=actor.id)
            wf_result = WebfingerResult(subject=acct, links=[link])
            return JSONResponse(wf_result.to_json(), media_type="application/jrd+json")
    else:
        # Handle resource queries using a URL
        if acct.url == f"https://{HOST}/actor":
            link = WebfingerLink(rel="self", type="application/activity+json", href=actor.id)
            wf_result = WebfingerResult(subject=acct, links=[link])
            return JSONResponse(wf_result.to_json(), media_type="application/jrd+json")
    return JSONResponse({"message": "Not Found"}, status_code=404)

@app.get("/actor")
async def get_actor_endpoint():
    """Serves the bot's Actor object."""
    return ActivityResponse(actor)

@app.get("/outbox")
async def get_outbox_endpoint():
    """Serves a collection of the bot's sent activities."""
    items = sorted(ACTIVITY_STORE.values(), key=lambda x: x.id, reverse=True)
    outbox_collection = OrderedCollection(
        id=actor.outbox,
        totalItems=len(items),
        orderedItems=items
    )
    return ActivityResponse(outbox_collection)

@app.get("/notes/{note_id}")
async def get_note_endpoint(note_id: uuid.UUID):
    """Serves a specific Note object, with caching."""
    note_uri = f"https://{HOST}/notes/{note_id}"
    
    # Check cache first
    if note_uri in CACHE and (datetime.now() - CACHE[note_uri]["timestamp"]) < CACHE_TTL:
        return ActivityResponse(CACHE[note_uri]["activity"])
        
    # If not in cache, get from store
    if note_uri in ACTIVITY_STORE:
        activity = ACTIVITY_STORE[note_uri]
        # Add to cache before returning
        CACHE[note_uri] = {"activity": activity, "timestamp": datetime.now()}
        return ActivityResponse(activity)
        
    return Response(status_code=404) # Not Found

@app.get("/creates/{create_id}")
async def get_create_endpoint(create_id: uuid.UUID):
    """Serves a specific Create activity, with caching."""
    create_uri = f"https://{HOST}/creates/{create_id}"
    
    if create_uri in CACHE and (datetime.now() - CACHE[create_uri]["timestamp"]) < CACHE_TTL:
        return ActivityResponse(CACHE[create_uri]["activity"])
        
    if create_uri in ACTIVITY_STORE:
        activity = ACTIVITY_STORE[create_uri]
        CACHE[create_uri] = {"activity": activity, "timestamp": datetime.now()}
        return ActivityResponse(activity)
        
    return Response(status_code=404)

8. Activity Handlers

We use the @app.on() decorator to define handlers for specific activity types posted to our inbox.

  • on_follow_activity: Automatically accepts Follow requests.
  • on_create_activity: Parses incoming Create activities (specifically for Note objects) to schedule reminders.
# main.py (continued)

# Handler for Follow activities
@app.on(Follow)
async def on_follow_activity(ctx: Context):
    """Automatically accepts follow requests."""
    # ... (implementation omitted for brevity)

# Handler for Create activities
@app.on(Create)
async def on_create_activity(ctx: Context):
    """Parses mentions to schedule reminders."""
    activity = ctx.activity
    # Ignore if it's not a Note
    if not (isinstance(activity, Create) and isinstance(activity.object, Note)):
        return Response(status_code=202)

    note = activity.object
    
    # Check if the bot was mentioned
    is_mentioned = any(
        isinstance(tag, Mention) and tag.href == actor.id for tag in (note.tag or [])
    )
    
    if not is_mentioned:
        return Response(status_code=202)

    # ... (Parse reminder text)
    delay, message, time_str = parse_reminder(command_text)

    # If parsing is successful, schedule the reminder as a background task
    if delay and message and sender_actor:
        asyncio.create_task(send_reminder(ctx, delay, message, sender_actor, note))
        reply_content = f"<p>✅ OK! I will remind you in {time_str}.</p>"
    else:
        # If parsing fails, send usage instructions
        reply_content = "<p>🤔 Sorry, I didn\'t understand. Please use the format: `@reminder [time] [message]`.</p><p>Example: `@reminder 10m Check the oven`</p>"

    # ... (Create and send the reply Note)

9. Running the Application

Finally, we run the application using uvicorn.

# main.py (continued)

if __name__ == "__main__":
    import uvicorn
    logger.info("Starting uvicorn server...")
    uvicorn.run(app, host="0.0.0.0", port=8000)

How to Run the Bot

  1. Set the HOST and USER_ID variables in main.py to match your environment.

  2. Run the server from your terminal:

    uvicorn main:app --host 0.0.0.0 --port 8000
  3. Your bot will be running at http://0.0.0.0:8000.

Now you can mention your bot from anywhere in the Fediverse (e.g., @reminder@your.host.com) to set a reminder.

Next Steps

This tutorial covers the basics of creating a simple ActivityPub bot. Since it only uses in-memory storage, all reminders will be lost on server restart. Here are some potential improvements:

  • Persistent Storage: Replace the in-memory ACTIVITY_STORE with a database like SQLite or PostgreSQL.
  • Robust Task Queuing: Use a dedicated task queue like Celery with a Redis or RabbitMQ broker to ensure reminders are not lost if the server restarts.
  • Advanced Commands: Add support for more complex commands, such as recurring reminders.

We hope this guide serves as a good starting point for building your own ActivityPub applications!

https://fedi-libs.github.io/apkit/

https://github.com/fedi-libs/apkit

https://github.com/AmaseCocoa/activitypub-reminder-bot

perlmint's avatar
perlmint

@me@social.perlmint.app · Reply to perlmint's post

여행 정산 하는 중에 만든 정산 시트 템플릿 대충 정산할 항목 환율이 여럿인 경우와 건별 결제자, 균등 분할 금액, 개별 금액 처리등을 커버합니다.

https://docs.google.com/spreadsheets/d/1e9Wufcf9y_5MToncbuXgaKAfvdk2K8boKLOI4VbmnDc/edit?usp=drivesdk

이적행위's avatar
이적행위

@yijuckhangwe@qdon.space

마쓰시타 류이치 저자의 『동아시아반일무장전선』
추천

한날's avatar
한날

@hannal@hackers.pub

이미지로만 보던 그 유명한 파도 그림을 보고 왔습니다. 실제로 보니 질감과 세밀함이 정말 감동스럽더라고요. 작품 교체를 하는데, 후지산이나 파도 그림은 전반기에 전시하는 것 같아요. 전반기는 11월 2일까지.

"국립 "청주" 박물관에서 전시합니나. "충주" 아니에요. 이상 충주에 가서 밥 먹고 청주에 가서 관람한 1인이었슴미다.

https://cheongju.museum.go.kr/www/speclExbiView.do?key

파도
ALT text details파도
청주 국립 박물관 청명관 입구
ALT text details청주 국립 박물관 청명관 입구
산책로
ALT text details산책로
산책로
ALT text details산책로
Jaeyeol Lee's avatar
Jaeyeol Lee

@kodingwarrior@hackers.pub

@z9mb1 @catamorphic @msy @joonnot @d01c2 @icecream_mable @minju2996 @2chanhaeng @lionhairdino @akastoot @jihyeok @linea @woaol @theeluwin @rangho @robin @1ho @nebuleto @moreal @harei106 @realfishbread @jcha0713 @hyunjoon @krepe90 @cetaceanightsky @aioo @ink_punctuation @yg1ee

오늘 Hackers' Public 행사 첫번째 모임에 와주셔서 정말 감사합니다! 정말 즐거웠습니다! 잘 들어가세요!



RE: https://hackers.pub/@kodingwarrior/0198e9db-763b-7135-aa89-1808e9e99227

Jaeyeol Lee's avatar
Jaeyeol Lee

@kodingwarrior@hackers.pub

Hackers' Public @ Seoul 1회차 모임 (1차 모집)

서울에서 열리는 Hackers' Pub 오프라인 밋업, "Hackers' Public @ Seoul"이 2025월 9월 14일(일) 처음으로 개최됩니다. 처음 열리는 밋업인 만큼, 참여하는 많은 분들이 재밌게 느낄 수 있는 소재 위주로 연사자 분들을 섭외했습니다.

  • 일시 : 9월 14일 (일) 오후 3시 ~ 오후 6시
  • 장소 : 서울특별시 성동구 상원길 26, 튜링의사과
  • 주제
    • Code As A Canvas : 코드에서 예술작품이 되기까지 (@jakeseo)
    • 폰트는 어떻게 만들어지는가 - NeoDGM 사례로 살펴보는 개발 후일담 (@dalgona)

강연이 끝나고 난 뒤에 자유롭게 네트워킹하는 시간을 가질 예정입니다. 각자 얘기하고 싶은 주제를 들고 오시면 좋습니다.

참여 신청

오프라인 밋업은 여기서 참여신청이 가능합니다. https://event-us.kr/hackerspubseoul/event/110961

  • 모집 기간
    • 1차 모집 : 8월 27일 ~ 9월 1일 (Hackers' Pub에서만 모집)
    • 2차 모집 : 9월 3일 ~ 9월 7일 (Hackers' Pub 외부에서도 공개적으로 모집)

주의사항

  • 본 행사는 Hackers' Pub에서 진행하는 오프라인 행사이며, Hackers' Pub 계정을 가지지 않은 분이 신청하셨을 경우 환불처리될 수 있습니다.
  • Hackers' Pub 외부에서 유입하시는 경우, 각 모집기간이 끝나고 24시간 안에는 Hackers' Pub에 가입이 되어 있으셔야 참여자로 확정됩니다.
洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub

오늘은 Hackers' Public 첫 번째 모임! 다들 튜링의 사과(@TuringAppleDev)에서 오후 3시에 모여요!

← Newer
Older →