Hello, I'm an open source software engineer in my late 30s living in #Seoul, #Korea, and an avid advocate of #FLOSS and the #fediverse.
I'm the creator of @fedify, an #ActivityPub server framework in #TypeScript, @hollo, an ActivityPub-enabled microblogging software for single users, and @botkit, a simple ActivityPub bot framework.
Finished the basic tutorial for #Fedify - I can now Follow and Unfollow the "me" account. Lots of useful debugging and dev tools built in, too.
I think what's most interesting about this framework is that there are quite a few AP vocabulary activitites available to you above and beyond the Mastodon mainstream.
Would love to tinker around with Offer, Reject, Listen, Question, Read
대부분의 액티비티펍 소프트웨어 인스턴스는 멀쩡하게 365일 24시간 동일한 위치에서 운영이 되고 있다고 가정이 된다. 내가 글을 올리면, 나를 팔로우 중인 모든 사람들의 inbox에 내가 글을 올렸다(Create(Note))는 Activity가 전달이 되는데, 각자가 운영되고 있는 서버 인스턴스가 멀쩡히 살아있다면.... 딱히 문제가 되지는 않는다.
문제는, 게시글을 작성하는 시점에 팔로워 중 누군가의 인스턴스가 죽어있을때도 있다는 점이다. 그런 경우를 대비해서 exponential backoff를 쓰든 아무튼 fallback 알고리즘이 동작하긴 하는데, 서버가 살아나면 당연히 전달이야 잘 되긴 한다. 그런데, Activity 전달이 실패하는 일이 잦으면 어떤 액티비티펍 소프트웨어를 쓰던간에 retry를 하기 위해서 계속해서 Queue에 쌓이고, 최종적으로는 Queue에 쌓인 것 때문에 적지 않은 오버헤드가 있을 것 같은데 모더레이터의 입장에선 어느 정도까지 감안할 수 있는가? 라는 생각이 문득 들었다.
사실 내가 왜 이런 글을 쓰고 있냐면, 위에서도 언급했다시피, 로컬호스트에서 실제로 서비스를 (맥북이 켜져있을때만) 서빙하고 있고 그걸 Tailscale로 연결해서 터널링을 하고 있다. 즉, 맥북을 켜놓고 있으면 Create(Note) Activity가 정상적으로 잘 전달되고, 맥북이 꺼져있으면 Activity 전달이 안되고 있다. 실제로, 이런 맥락에서 지금 테스트 중인 두 개의 인스턴스가 있다. 이런 실험적인 시도를 하면서 이래도 되는게 맞나 싶은 생각도 들고는 있다. 맥북을 켜놓으면, retry되고 있는 것도 다 consume되긴 하겠지만.... 찝찝하긴 찝찝하다.
개발하는 입장이라고 선해를 할 수는 있어도, 비뚤어진 관점에서 해석하면 누군가는 어뷰징의 관점으로 해석할 수 있는 가능성이 적지는 않다고 생각하고 있다. 이런 경우엔 모더레이터되는 분들한테, 내가 이런 tailscale 도메인으로 서빙하고 있다고 통지라도 하는게 나으려나... 아니면, 내가 구매해놓은 도메인을 tailscale 도메인으로 CNAME 걸어놓고 "이런 도메인으로 서비스 걸어놓을 예정이니까 이 도메인만은 제발 차단하지 말아주십쇼 헤헤" 라고 해야하나... 아예 서버를 만드는거다보니까 이런 고려사항이 생기는 것 같다.
근데, 한 편으로는 이런 생각이 든다. 물리적인 서버의 위치를 옮길 가능성이 많은 환경(예를 들면, 전시 상황)이면 어떡하지? ActivityPub이 사실은 분산된 웹 환경을 위해 나온 프로토콜이긴 하지만, 분산된 웹 환경이라는게 물리적으로 각자 다른 위치에 오랫동안 배치가 되어 있는 서버 뿐만이 아니라 위치가 자주 바뀔 수 있는 서버도 연합의 대상으로 포함이 될 수 있다면? 어떤 공상과학 영화(ex. 터미네이터4)들을 보면, 저항군이 독자적인 라디오 기지국 같은거 만들고 위치도 매번 다른 곳으로 옮기고 주파수를 매번 다르게 설정하면서 소식전달하는 모습을 볼 수 있는데, 액티비티펍도 어떻게 보면 그걸 고려한 설계도 포함될 수 있지 않나... 그런 생각도 든다..
클러스터화는 단점이기도 하지만 장점이기도 한게 제가 생각하기에 다른 커뮤니티 서비스랑 트위터류의 가장 큰 차이점은 적당한? 느슨한? 클러스터화라고 생각하거든요 트위터만 봐도 인플루언서-like하게 사용하는 사람도 자기들끼리 노는 사람도 있는데 되게 그런 사람들간에 사용 형태와 목적이 판이하게 다를탠데 동시에 같은 트위터에 존재하면서도 디스코드같은 아예 클러스터가 완전히 분리된 동네하고는 다르게 그 클러스터들 사이에도 느슨한 연결고리가 존재한다는게 매력이라고 생각해요
I have this bad habit. When something annoys me enough times,
I end up building a library for it. This time, it was CLI validation code.
See, I spend a lot of time reading other people's code. Open source projects,
work stuff, random GitHub repos I stumble upon at 2 AM. And I kept noticing this
thing: every CLI tool has the same ugly validation code tucked away somewhere.
You know the kind:
if (!opts.server && opts.port) { throw new Error("--port requires --server flag");}if (opts.server && !opts.port) { opts.port = 3000; // default port}// wait, what if they pass --port without a value?// what if the port is out of range?// what if...
It's not even that this code is hard to write. It's that it's everywhere.
Every project. Every CLI tool. The same patterns, slightly different flavors.
Options that depend on other options. Flags that can't be used together.
Arguments that only make sense in certain modes.
And here's what really got me: we solved this problem years ago for other types
of data. Just… not for CLIs.
The problem with validation
There's this blog post that completely changed how I think about parsing.
It's called Parse, don't validate by Alexis King. The gist? Don't parse data
into a loose type and then check if it's valid. Parse it directly into a type
that can only be valid.
Think about it. When you get JSON from an API, you don't just parse it as any
and then write a bunch of if-statements. You use something like Zod to parse
it directly into the shape you want. Invalid data? The parser rejects it. Done.
But with CLIs? We parse arguments into some bag of properties and then spend
the next 100 lines checking if that bag makes sense. It's backwards.
So yeah, I built Optique. Not because the world desperately needed another CLI
parser (it didn't), but because I was tired of seeing—and writing—the same
validation code everywhere.
Three patterns I was sick of validating
Dependent options
This one's everywhere. You have an option that only makes sense when another
option is enabled.
The type system now understands that when server is false, port literally
doesn't exist. Not undefined, not null—it's not there. Try to access it and
TypeScript yells at you. No runtime validation needed.
Mutually exclusive options
Another classic. Pick one output format: JSON, YAML, or XML. But definitely not
two.
I used to write this mess:
if ((opts.json ? 1 : 0) + (opts.yaml ? 1 : 0) + (opts.xml ? 1 : 0) > 1) { throw new Error('Choose only one output format');}
(Don't judge me, you've written something similar.)
Now?
const format = or( map(option("--json"), () => "json" as const), map(option("--yaml"), () => "yaml" as const), map(option("--xml"), () => "xml" as const));
The or() combinator means exactly one succeeds. The result is just
"json" | "yaml" | "xml". A single string. Not three booleans to juggle.
Environment-specific requirements
Production needs auth. Development needs debug flags. Docker needs different
options than local. You know the drill.
Instead of a validation maze, you just describe each environment:
No auth in production? Parser fails immediately. Trying to access --auth in
dev mode? TypeScript won't let you—the field doesn't exist on that type.
“But parser combinators though…”
I know, I know. “Parser combinators” sounds like something you'd need
a CS degree to understand.
Here's the thing: I don't have a CS degree. Actually, I don't have any degree.
But I've been using parser combinators for years because they're actually… not
that hard? It's just that the name makes them sound way scarier than they are.
I'd been using them for other stuff—parsing config files, DSLs, whatever.
But somehow it never clicked that you could use them for CLI parsing until
I saw Haskell's optparse-applicative. That was a real “wait, of course”
moment. Like, why are we doing this any other way?
Turns out it's stupidly simple. A parser is just a function. Combinators are
just functions that take parsers and return new parsers. That's it.
// This is a parserconst port = option("--port", integer());// This is also a parser (made from smaller parsers)const server = object({ port: port, host: option("--host", string())});// Still a parser (parsers all the way down)const config = or(server, client);
No monads. No category theory. Just functions. Boring, beautiful functions.
TypeScript does the heavy lifting
Here's the thing that still feels like cheating: I don't write types for my CLI
configs anymore. TypeScript just… figures it out.
TypeScript knows that if action is "deploy", then environment exists but
version doesn't. It knows replicas is a number. It knows force is
a boolean. I didn't tell it any of this.
This isn't just about nice autocomplete (though yeah, the autocomplete is great).
It's about catching bugs before they happen. Forget to handle a new option
somewhere? Code won't compile.
What actually changed for me
I've been dogfooding this for a few weeks. Some real talk:
I delete code now. Not refactor. Delete. That validation logic that used to
be 30% of my CLI code? Gone. It feels weird every time.
Refactoring isn't scary. Want to know something that usually terrifies me?
Changing how a CLI takes its arguments. Like going from --input file.txt to
just file.txt as a positional argument. With traditional parsers,
you're hunting down validation logic everywhere. With this?
You change the parser definition, TypeScript immediately shows you every place
that breaks, you fix them, done. What used to be an hour of “did I catch
everything?” is now “fix the red squiggles and move on.”
My CLIs got fancier. When adding complex option relationships doesn't mean
writing complex validation, you just… add them. Mutually exclusive groups?
Sure. Context-dependent options? Why not. The parser handles it.
But honestly? The biggest change is trust. If it compiles, the CLI logic works.
Not “probably works” or “works unless someone passes weird arguments.”
It just works.
Should you care?
If you're writing a 10-line script that takes one argument, you don't need this.
process.argv[2] and call it a day.
But if you've ever:
Had validation logic get out of sync with your actual options
Discovered in production that certain option combinations explode
Spent an afternoon tracking down why --verbose breaks when used with
--json
Written the same “option A requires option B” check for the fifth time
Then yeah, maybe you're tired of this stuff too.
Fair warning: Optique is young. I'm still figuring things out, the API might
shift a bit. But the core idea—parse, don't validate—that's solid.
And I haven't written validation code in months.
Still feels weird. Good weird.
Try it or don't
If this resonates:
Tutorial: Build something real, see if you hate it
I'm not saying Optique is the answer to all CLI problems. I'm just saying
I was tired of writing the same validation code everywhere, so I built something
that makes it unnecessary.
Take it or leave it. But that validation code you're about to write?
You probably don't need it.
The concept of the "male socialization" of trans women is deeply problematic. Most of the time when people use that phrase, it's to degender and degrade trans women and make them out to be less women than their cis counterparts. When people go down that path of reasoning, it's usually toxic and leads to a lot of wrong conclusions.
We often never got the kind of male privilege that people assume we did because we looked like dudes. The reality is that bullies often pick up on our gender differences even if we don't and it can get dark. We also didn't have boyhoods. Just because our parents dressed us in blue and bought us baseball hats doesn't mean we have the same relationship to those things that cis guys do. Often, it's some weird, fucked up mix.
But...
We also didn't have "normal" girlhoods, either, and this can leave a huge hole.
One of the things I'm constantly self-conscious about when trying to relate to cis women is that there's this vast swath of girlhood experiences that they just take for granted and assume every woman has.
I don't.
Now, yes, those assumptions are generally rubbish because they're heavily dependant on culture, race, and class. But even then, there are still common threads. I'm missing all the threads.
When talking to other women, I rely a lot on the experiences I've had over just the last 3 years as well as things I've read and things I watched my sisters go through. I have a few distinctly girl childhood experiences I can draw on as well but not many. The whole time I worry that I'm going to slip up and say the wrong thing. Or maybe they'll say something that assumes a shared experience and I'll just sit there dumbfounded because I know that's a common girl experience but I've got no clue.
Sometimes those interactions can be weirdly affirming because you know they see you as a girl subconsciously when they start making those assumptions rather than assuming "male socialization" (🤮) but it can still leave you dumbfounded and speechless.
The only thing I can say is that it does seem to be getting better with time, both as I form new authentically feminine experiences as an adult and as I grow in confidence. But still it's hard and I feel that hole in basically every relationship I have with a cis woman. 😢
I have this bad habit. When something annoys me enough times,
I end up building a library for it. This time, it was CLI validation code.
See, I spend a lot of time reading other people's code. Open source projects,
work stuff, random GitHub repos I stumble upon at 2 AM. And I kept noticing this
thing: every CLI tool has the same ugly validation code tucked away somewhere.
You know the kind:
if (!opts.server && opts.port) { throw new Error("--port requires --server flag");}if (opts.server && !opts.port) { opts.port = 3000; // default port}// wait, what if they pass --port without a value?// what if the port is out of range?// what if...
It's not even that this code is hard to write. It's that it's everywhere.
Every project. Every CLI tool. The same patterns, slightly different flavors.
Options that depend on other options. Flags that can't be used together.
Arguments that only make sense in certain modes.
And here's what really got me: we solved this problem years ago for other types
of data. Just… not for CLIs.
The problem with validation
There's this blog post that completely changed how I think about parsing.
It's called Parse, don't validate by Alexis King. The gist? Don't parse data
into a loose type and then check if it's valid. Parse it directly into a type
that can only be valid.
Think about it. When you get JSON from an API, you don't just parse it as any
and then write a bunch of if-statements. You use something like Zod to parse
it directly into the shape you want. Invalid data? The parser rejects it. Done.
But with CLIs? We parse arguments into some bag of properties and then spend
the next 100 lines checking if that bag makes sense. It's backwards.
So yeah, I built Optique. Not because the world desperately needed another CLI
parser (it didn't), but because I was tired of seeing—and writing—the same
validation code everywhere.
Three patterns I was sick of validating
Dependent options
This one's everywhere. You have an option that only makes sense when another
option is enabled.
The type system now understands that when server is false, port literally
doesn't exist. Not undefined, not null—it's not there. Try to access it and
TypeScript yells at you. No runtime validation needed.
Mutually exclusive options
Another classic. Pick one output format: JSON, YAML, or XML. But definitely not
two.
I used to write this mess:
if ((opts.json ? 1 : 0) + (opts.yaml ? 1 : 0) + (opts.xml ? 1 : 0) > 1) { throw new Error('Choose only one output format');}
(Don't judge me, you've written something similar.)
Now?
const format = or( map(option("--json"), () => "json" as const), map(option("--yaml"), () => "yaml" as const), map(option("--xml"), () => "xml" as const));
The or() combinator means exactly one succeeds. The result is just
"json" | "yaml" | "xml". A single string. Not three booleans to juggle.
Environment-specific requirements
Production needs auth. Development needs debug flags. Docker needs different
options than local. You know the drill.
Instead of a validation maze, you just describe each environment:
No auth in production? Parser fails immediately. Trying to access --auth in
dev mode? TypeScript won't let you—the field doesn't exist on that type.
“But parser combinators though…”
I know, I know. “Parser combinators” sounds like something you'd need
a CS degree to understand.
Here's the thing: I don't have a CS degree. Actually, I don't have any degree.
But I've been using parser combinators for years because they're actually… not
that hard? It's just that the name makes them sound way scarier than they are.
I'd been using them for other stuff—parsing config files, DSLs, whatever.
But somehow it never clicked that you could use them for CLI parsing until
I saw Haskell's optparse-applicative. That was a real “wait, of course”
moment. Like, why are we doing this any other way?
Turns out it's stupidly simple. A parser is just a function. Combinators are
just functions that take parsers and return new parsers. That's it.
// This is a parserconst port = option("--port", integer());// This is also a parser (made from smaller parsers)const server = object({ port: port, host: option("--host", string())});// Still a parser (parsers all the way down)const config = or(server, client);
No monads. No category theory. Just functions. Boring, beautiful functions.
TypeScript does the heavy lifting
Here's the thing that still feels like cheating: I don't write types for my CLI
configs anymore. TypeScript just… figures it out.
TypeScript knows that if action is "deploy", then environment exists but
version doesn't. It knows replicas is a number. It knows force is
a boolean. I didn't tell it any of this.
This isn't just about nice autocomplete (though yeah, the autocomplete is great).
It's about catching bugs before they happen. Forget to handle a new option
somewhere? Code won't compile.
What actually changed for me
I've been dogfooding this for a few weeks. Some real talk:
I delete code now. Not refactor. Delete. That validation logic that used to
be 30% of my CLI code? Gone. It feels weird every time.
Refactoring isn't scary. Want to know something that usually terrifies me?
Changing how a CLI takes its arguments. Like going from --input file.txt to
just file.txt as a positional argument. With traditional parsers,
you're hunting down validation logic everywhere. With this?
You change the parser definition, TypeScript immediately shows you every place
that breaks, you fix them, done. What used to be an hour of “did I catch
everything?” is now “fix the red squiggles and move on.”
My CLIs got fancier. When adding complex option relationships doesn't mean
writing complex validation, you just… add them. Mutually exclusive groups?
Sure. Context-dependent options? Why not. The parser handles it.
But honestly? The biggest change is trust. If it compiles, the CLI logic works.
Not “probably works” or “works unless someone passes weird arguments.”
It just works.
Should you care?
If you're writing a 10-line script that takes one argument, you don't need this.
process.argv[2] and call it a day.
But if you've ever:
Had validation logic get out of sync with your actual options
Discovered in production that certain option combinations explode
Spent an afternoon tracking down why --verbose breaks when used with
--json
Written the same “option A requires option B” check for the fifth time
Then yeah, maybe you're tired of this stuff too.
Fair warning: Optique is young. I'm still figuring things out, the API might
shift a bit. But the core idea—parse, don't validate—that's solid.
And I haven't written validation code in months.
Still feels weird. Good weird.
Try it or don't
If this resonates:
Tutorial: Build something real, see if you hate it
I'm not saying Optique is the answer to all CLI problems. I'm just saying
I was tired of writing the same validation code everywhere, so I built something
that makes it unnecessary.
Take it or leave it. But that validation code you're about to write?
You probably don't need it.
금융부터 전자상거래까지 확산된 일본의 FIDO2 패스키 전략, 한국은 ‘간편 인증’에 머무른 채 방향성조차 흐릿합니다
보안 사고가 촉발한 일본의 급반전
2025년 상반기, 일본 증권 업계를 강타한 대규모 불법 로그인 및 부정 거래 사태는 단순한 해킹 사건을 넘어, 디지털 인증 체계에 대한 신뢰 자체를 뒤흔드는 사건이었습니다. 수천 건의 계좌 탈취와 수백억 엔 규모의 […]
금융부터 전자상거래까지 확산된 일본의 FIDO2 패스키 전략, 한국은 ‘간편 인증’에 머무른 채 방향성조차 흐릿합니다
보안 사고가 촉발한 일본의 급반전
2025년 상반기, 일본 증권 업계를 강타한 대규모 불법 로그인 및 부정 거래 사태는 단순한 해킹 사건을 넘어, 디지털 인증 체계에 대한 신뢰 자체를 뒤흔드는 사건이었습니다. 수천 건의 계좌 탈취와 수백억 엔 규모의 시장 조작 시도가 이어지자, 일본 금융청과 증권업협회는 즉각적으로 움직였고, 주요 증권사들은 FIDO2 기반의 패스키 인증 시스템을 도입하겠다고 발표했습니다.
웰스나비, 모넥스, SBI 증권, PayPay 증권 등 주요 업체들이 빠르게 패스키 로그인 방식을 적용했고, 기존의 SMS나 OTP 인증보다 몇 배 빠르면서도 피싱에 강한 패스키의 특성은 사용성과 보안성 양면에서 효과를 입증했습니다. 실제로 메르카리, 야후재팬, au ID, 도코모 등 주요 전자상거래 및 플랫폼 기업에서도 패스키 도입이 확산되면서, 로그인 속도 향상, 성공률 증가, 고객센터 문의 감소 등 긍정적인 결과를 수치로 입증하고 있습니다. 특히 대형 중고거래 사이트인 메르카리는 월등히 높은 로그인 성공률, 4배 가까운 속도 감소 등을 보였습니다.
이는 정책, 업계, 기술이 유기적으로 맞물려 이루어진 결과라고 볼 수 있습니다. 일본 금융청은 패스키를 “피싱 저항 인증의 기본”으로 명시하며 업계 전반에 도입을 권고했고, FIDO Alliance Japan Working Group은 기술 보급과 사례 공유를 통해 확산을 이끌었습니다.
한국, 기술은 있는데 방향이 없습니다
반면 한국의 상황은 정반대입니다. 소비자 대상 FIDO2 패스키 도입을 완료한 금융기관은 현재까지 단 한 곳도 없습니다.
신한은행이나 우리은행 등 일부 은행에서 FIDO 기반 생체 인증을 도입한 사례는 존재하지만, 이는 FIDO2 패스키 방식과는 다르며, 주로 내부 인증이나 보조 로그인 수단으로 제한적으로 활용되고 있습니다. 고객용 인터넷 뱅킹이나 모바일 앱에서 비밀번호 없이 완전히 로그인할 수 있는 구조는 아직 갖춰지지 않았습니다.
IT 업계에서도 사정은 비슷합니다. 카카오, 네이버, SK텔레콤, KT 등 일부 대기업에서 패스키를 도입하긴 했지만, 적용 범위는 제한적이며, 사용자 기반 확산도 매우 미미한 수준입니다. 생태계 차원의 연동성이나 범용 인증 체계 구축도 이뤄지지 않고 있습니다.
제도는 마련됐지만 실행 의지가 부족합니다
이미 일본에서 효과를 본 패스키입니다만, 한국에서 이를 실행에 옮기려는 업계의 움직임은 매우 소극적입니다. 한국은 보안 사고에 대한 대응 속도는 빠른 편이지만, 인증 기술을 선제적으로 고도화하려는 전략적인 의지나 비전은 부족한 상황입니다. 공인인증서가 폐지된 이후에도 여전히 휴대폰 본인확인, 공동인증서, 일회용 비밀번호(OTP) 등 기존의 불편한 방식을 그대로 유지하고 있습니다.
정부나 금융당국 차원에서도 FIDO2 패스키를 채택하도록 유도하거나, 업계 협업을 통해 표준을 만들려는 노력이 거의 없습니다. ‘간편 인증’이 곧 사용자 경험 개선이라고 착각하는 단편적인 인식이 시장 전체를 가로막고 있는 현실입니다.
일본은 인증을 “보안”으로, 한국은 “편의”로만 보고 있습니다
결국 일본과 한국의 차이는 인증에 대한 관점의 차이에서 비롯된다고 볼 수 있습니다. 일본은 패스키를 단순한 편의 기능이 아니라, 신뢰 가능한 보안 인프라의 핵심 기술로 보고 전방위적으로 투자하고 있습니다. 반면 한국은 여전히 ‘편한 인증 수단’ 정도로만 이해하고 있으며, 보안에 미치는 영향에 대해서는 소극적인 태도를 보이고 있습니다.
패스키는 단순한 로그인 도구가 아니라, 사용자 식별과 신뢰 기반을 재정립하는 차세대 인증 기술입니다. 일본은 이를 과감히 수용했지만, 한국은 기술이 있음에도 불구하고 실행력과 추진 의지가 부족한 상황입니다.
언제까지 “안전하지만 불편한 인증”에 머물러야 할까요?
지금도 많은 금융기관은 ‘비밀번호 + OTP’ 혹은 ‘비밀번호 + 인증서’ 방식에 의존하고 있습니다. 하지만 점점 더 교묘해지는 피싱과 계정 탈취 수법을 고려할 때, 더 이상 구식 인증 방식에 의존할 수는 없습니다.
“패스워드 없는 세상”은 기술의 문제가 아니라 의지의 문제입니다. 일본은 보안 사고를 계기로 신속하게 움직였고, 지금은 눈에 보이는 성과를 만들어내고 있습니다. 한국은 언제까지 사용자에게 불편을 강요하면서, 보안을 지키고 있다고 착각하는 구조에 머무를 것인지 되묻고 싶습니다.
이제는 질문을 던져야 할 시점입니다. “한국의 디지털 보안은 앞으로 10년 동안도 여전히 비밀번호에 기대야 할까요?”
금융부터 전자상거래까지 확산된 일본의 FIDO2 패스키 전략, 한국은 ‘간편 인증’에 머무른 채 방향성조차 흐릿합니다
보안 사고가 촉발한 일본의 급반전
2025년 상반기, 일본 증권 업계를 강타한 대규모 불법 로그인 및 부정 거래 사태는 단순한 해킹 사건을 넘어, 디지털 인증 체계에 대한 신뢰 자체를 뒤흔드는 사건이었습니다. 수천 건의 계좌 탈취와 수백억 엔 규모의 […]
금융부터 전자상거래까지 확산된 일본의 FIDO2 패스키 전략, 한국은 ‘간편 인증’에 머무른 채 방향성조차 흐릿합니다
보안 사고가 촉발한 일본의 급반전
2025년 상반기, 일본 증권 업계를 강타한 대규모 불법 로그인 및 부정 거래 사태는 단순한 해킹 사건을 넘어, 디지털 인증 체계에 대한 신뢰 자체를 뒤흔드는 사건이었습니다. 수천 건의 계좌 탈취와 수백억 엔 규모의 시장 조작 시도가 이어지자, 일본 금융청과 증권업협회는 즉각적으로 움직였고, 주요 증권사들은 FIDO2 기반의 패스키 인증 시스템을 도입하겠다고 발표했습니다.
웰스나비, 모넥스, SBI 증권, PayPay 증권 등 주요 업체들이 빠르게 패스키 로그인 방식을 적용했고, 기존의 SMS나 OTP 인증보다 몇 배 빠르면서도 피싱에 강한 패스키의 특성은 사용성과 보안성 양면에서 효과를 입증했습니다. 실제로 메르카리, 야후재팬, au ID, 도코모 등 주요 전자상거래 및 플랫폼 기업에서도 패스키 도입이 확산되면서, 로그인 속도 향상, 성공률 증가, 고객센터 문의 감소 등 긍정적인 결과를 수치로 입증하고 있습니다. 특히 대형 중고거래 사이트인 메르카리는 월등히 높은 로그인 성공률, 4배 가까운 속도 감소 등을 보였습니다.
이는 정책, 업계, 기술이 유기적으로 맞물려 이루어진 결과라고 볼 수 있습니다. 일본 금융청은 패스키를 “피싱 저항 인증의 기본”으로 명시하며 업계 전반에 도입을 권고했고, FIDO Alliance Japan Working Group은 기술 보급과 사례 공유를 통해 확산을 이끌었습니다.
한국, 기술은 있는데 방향이 없습니다
반면 한국의 상황은 정반대입니다. 소비자 대상 FIDO2 패스키 도입을 완료한 금융기관은 현재까지 단 한 곳도 없습니다.
신한은행이나 우리은행 등 일부 은행에서 FIDO 기반 생체 인증을 도입한 사례는 존재하지만, 이는 FIDO2 패스키 방식과는 다르며, 주로 내부 인증이나 보조 로그인 수단으로 제한적으로 활용되고 있습니다. 고객용 인터넷 뱅킹이나 모바일 앱에서 비밀번호 없이 완전히 로그인할 수 있는 구조는 아직 갖춰지지 않았습니다.
IT 업계에서도 사정은 비슷합니다. 카카오, 네이버, SK텔레콤, KT 등 일부 대기업에서 패스키를 도입하긴 했지만, 적용 범위는 제한적이며, 사용자 기반 확산도 매우 미미한 수준입니다. 생태계 차원의 연동성이나 범용 인증 체계 구축도 이뤄지지 않고 있습니다.
제도는 마련됐지만 실행 의지가 부족합니다
이미 일본에서 효과를 본 패스키입니다만, 한국에서 이를 실행에 옮기려는 업계의 움직임은 매우 소극적입니다. 한국은 보안 사고에 대한 대응 속도는 빠른 편이지만, 인증 기술을 선제적으로 고도화하려는 전략적인 의지나 비전은 부족한 상황입니다. 공인인증서가 폐지된 이후에도 여전히 휴대폰 본인확인, 공동인증서, 일회용 비밀번호(OTP) 등 기존의 불편한 방식을 그대로 유지하고 있습니다.
정부나 금융당국 차원에서도 FIDO2 패스키를 채택하도록 유도하거나, 업계 협업을 통해 표준을 만들려는 노력이 거의 없습니다. ‘간편 인증’이 곧 사용자 경험 개선이라고 착각하는 단편적인 인식이 시장 전체를 가로막고 있는 현실입니다.
일본은 인증을 “보안”으로, 한국은 “편의”로만 보고 있습니다
결국 일본과 한국의 차이는 인증에 대한 관점의 차이에서 비롯된다고 볼 수 있습니다. 일본은 패스키를 단순한 편의 기능이 아니라, 신뢰 가능한 보안 인프라의 핵심 기술로 보고 전방위적으로 투자하고 있습니다. 반면 한국은 여전히 ‘편한 인증 수단’ 정도로만 이해하고 있으며, 보안에 미치는 영향에 대해서는 소극적인 태도를 보이고 있습니다.
패스키는 단순한 로그인 도구가 아니라, 사용자 식별과 신뢰 기반을 재정립하는 차세대 인증 기술입니다. 일본은 이를 과감히 수용했지만, 한국은 기술이 있음에도 불구하고 실행력과 추진 의지가 부족한 상황입니다.
언제까지 “안전하지만 불편한 인증”에 머물러야 할까요?
지금도 많은 금융기관은 ‘비밀번호 + OTP’ 혹은 ‘비밀번호 + 인증서’ 방식에 의존하고 있습니다. 하지만 점점 더 교묘해지는 피싱과 계정 탈취 수법을 고려할 때, 더 이상 구식 인증 방식에 의존할 수는 없습니다.
“패스워드 없는 세상”은 기술의 문제가 아니라 의지의 문제입니다. 일본은 보안 사고를 계기로 신속하게 움직였고, 지금은 눈에 보이는 성과를 만들어내고 있습니다. 한국은 언제까지 사용자에게 불편을 강요하면서, 보안을 지키고 있다고 착각하는 구조에 머무를 것인지 되묻고 싶습니다.
이제는 질문을 던져야 할 시점입니다. “한국의 디지털 보안은 앞으로 10년 동안도 여전히 비밀번호에 기대야 할까요?”
Recently there has been a lot of discourse about ActivityPub and AT Protocol which has been quite dividing and heated.
Yesterday at the Social Web CG meeting (the group that maintains the ActivityPub and related specifications), I proposed releasing a statement that counters the narrative that one of these protocols must win, when both protocols can co-exist and have a lot to learn from each other.
The statement has been co-signed by various members of both Social Web CG, SocialCG, and the AT Protocol community.
“We do not win by tearing each other down, which only emboldens and empowers those who do not want either protocol to succeed.”
“Arguing between us only emboldens those that seek to derail and destroy efforts to build an open social web.”
We're excited to announce Optique 0.4.0, which brings significant improvements
to help text organization, enhanced documentation capabilities, and introduces
comprehensive Temporal API support.
Optique is a type-safe combinatorial CLI parser for TypeScript that makes
building command-line interfaces intuitive and maintainable. This release
focuses on making your CLI applications more user-friendly and maintainable.
Better help text organization
One of the most visible improvements in Optique 0.4.0 is the enhanced help text
organization. You can now label and group your options more effectively, making
complex CLIs much more approachable for users.
Labeled merge groups
The merge() combinator now accepts an optional label parameter, solving
a common pain point where developers had to choose between clean code structure
and organized help output:
// Before: unlabeled merged options appeared scatteredconst config = merge(connectionOptions, performanceOptions);// Now: group related options under a clear sectionconst config = merge( "Server Configuration", // New label parameter connectionOptions, performanceOptions);
This simple addition makes a huge difference in help text readability,
especially for CLIs with many options spread across multiple reusable modules.
The resulting help output clearly organizes options under
the Server Configuration section:
Demo app showcasing labeled merge groupsUsage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries INTEGERServer Configuration: --host STRING Server hostname or IP address --port INTEGER Port number for the connection --timeout INTEGER Connection timeout in seconds --retries INTEGER Number of retry attempts
The new group() combinator
For cases where merge() doesn't apply, the new group()
combinator lets you wrap any parser with a documentation label:
// Group mutually exclusive options under a clear sectionconst outputFormat = group( "Output Format", or( map(flag("--json"), () => "json"), map(flag("--yaml"), () => "yaml"), map(flag("--xml"), () => "xml"), ));
This is particularly useful for organizing mutually exclusive flags, multiple
inputs, or any parser that doesn't natively support labeling. The resulting
help text becomes much more scannable and user-friendly.
Here's how the grouped output format options appear in the help text:
Demo app showcasing group combinatorUsage: demo-group.ts --json demo-group.ts --yaml demo-group.ts --xmlOutput Format: --json Output in JSON format --yaml Output in YAML format --xml Output in XML format
Rich documentation support
Optique 0.4.0 introduces comprehensive documentation fields that can be added
directly through the run() function, eliminating the need to modify parser
definitions for documentation purposes.
Brief descriptions, detailed explanations, and footers
Both @optique/core/facade and @optique/run now support brief,
description, and footer options through the run() function:
import { run } from "@optique/run";import { message } from "@optique/core/message";const result = run(parser, { brief: message`A powerful data processing tool`, description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`, footer: message`Examples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs`, help: "option"});
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
The complete help output demonstrates the rich documentation features with brief
description, detailed explanation, option descriptions, default values,
and footer information:
A powerful data processing toolUsage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRINGThis tool provides comprehensive data processing capabilities with support formultiple formats and transformations. It can handle JSON, YAML, and CSV fileswith automatic format detection. --port INTEGER Server port number [3000] --format STRING Output format [json] --verbose STRING Verbosity levelExamples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
Display default values
A frequently requested feature is now available: showing default values
directly in help text. Enable this with the new showDefault option when
using withDefault():
Default values are automatically dimmed when colors are enabled, making them
visually distinct while remaining readable.
The help output shows default values clearly marked next to each option:
Usage: demo-defaults.ts [--port INTEGER] [--format STRING] --port INTEGER Server port number [3000] --format STRING Output format [json]
Temporal API support
Optique 0.4.0 introduces a new package, @optique/temporal, providing
comprehensive support for the modern Temporal API. This brings type-safe
parsing for dates, times, durations, and time zones:
import { instant, duration, zonedDateTime } from "@optique/temporal";import { option } from "@optique/core/parser";const parser = object({ // Parse ISO 8601 timestamps timestamp: option("--at", instant()), // Parse durations like "PT30M" or "P1DT2H" timeout: option("--timeout", duration()), // Parse zoned datetime with timezone info meeting: option("--meeting", zonedDateTime()),});
The temporal parsers return native Temporal objects with full functionality:
The merge() combinator now supports up to 10 parsers (previously 5), and
the tuple() parser has improved type inference using TypeScript's const
type parameter. These enhancements enable more complex CLI structures while
maintaining perfect type safety.
Breaking changes
While we've maintained backward compatibility for most APIs, there are a few
changes to be aware of:
The Parser.getDocFragments() method now uses DocState<TState> instead
of direct state values (only affects custom parser implementations)
The merge() combinator now enforces stricter type
constraints at compile time, rejecting non-object-producing parsers
Learn more
For a complete list of changes, bug fixes, and improvements, see the
full changelog.
We hope these improvements make building CLI applications with Optique even
more enjoyable. As always, we welcome your feedback and contributions on
GitHub.
We're excited to announce Optique 0.4.0, which brings significant improvements
to help text organization, enhanced documentation capabilities, and introduces
comprehensive Temporal API support.
Optique is a type-safe combinatorial CLI parser for TypeScript that makes
building command-line interfaces intuitive and maintainable. This release
focuses on making your CLI applications more user-friendly and maintainable.
Better help text organization
One of the most visible improvements in Optique 0.4.0 is the enhanced help text
organization. You can now label and group your options more effectively, making
complex CLIs much more approachable for users.
Labeled merge groups
The merge() combinator now accepts an optional label parameter, solving
a common pain point where developers had to choose between clean code structure
and organized help output:
// Before: unlabeled merged options appeared scatteredconst config = merge(connectionOptions, performanceOptions);// Now: group related options under a clear sectionconst config = merge( "Server Configuration", // New label parameter connectionOptions, performanceOptions);
This simple addition makes a huge difference in help text readability,
especially for CLIs with many options spread across multiple reusable modules.
The resulting help output clearly organizes options under
the Server Configuration section:
Demo app showcasing labeled merge groupsUsage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries INTEGERServer Configuration: --host STRING Server hostname or IP address --port INTEGER Port number for the connection --timeout INTEGER Connection timeout in seconds --retries INTEGER Number of retry attempts
The new group() combinator
For cases where merge() doesn't apply, the new group()
combinator lets you wrap any parser with a documentation label:
// Group mutually exclusive options under a clear sectionconst outputFormat = group( "Output Format", or( map(flag("--json"), () => "json"), map(flag("--yaml"), () => "yaml"), map(flag("--xml"), () => "xml"), ));
This is particularly useful for organizing mutually exclusive flags, multiple
inputs, or any parser that doesn't natively support labeling. The resulting
help text becomes much more scannable and user-friendly.
Here's how the grouped output format options appear in the help text:
Demo app showcasing group combinatorUsage: demo-group.ts --json demo-group.ts --yaml demo-group.ts --xmlOutput Format: --json Output in JSON format --yaml Output in YAML format --xml Output in XML format
Rich documentation support
Optique 0.4.0 introduces comprehensive documentation fields that can be added
directly through the run() function, eliminating the need to modify parser
definitions for documentation purposes.
Brief descriptions, detailed explanations, and footers
Both @optique/core/facade and @optique/run now support brief,
description, and footer options through the run() function:
import { run } from "@optique/run";import { message } from "@optique/core/message";const result = run(parser, { brief: message`A powerful data processing tool`, description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`, footer: message`Examples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs`, help: "option"});
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
The complete help output demonstrates the rich documentation features with brief
description, detailed explanation, option descriptions, default values,
and footer information:
A powerful data processing toolUsage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRINGThis tool provides comprehensive data processing capabilities with support formultiple formats and transformations. It can handle JSON, YAML, and CSV fileswith automatic format detection. --port INTEGER Server port number [3000] --format STRING Output format [json] --verbose STRING Verbosity levelExamples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
Display default values
A frequently requested feature is now available: showing default values
directly in help text. Enable this with the new showDefault option when
using withDefault():
Default values are automatically dimmed when colors are enabled, making them
visually distinct while remaining readable.
The help output shows default values clearly marked next to each option:
Usage: demo-defaults.ts [--port INTEGER] [--format STRING] --port INTEGER Server port number [3000] --format STRING Output format [json]
Temporal API support
Optique 0.4.0 introduces a new package, @optique/temporal, providing
comprehensive support for the modern Temporal API. This brings type-safe
parsing for dates, times, durations, and time zones:
import { instant, duration, zonedDateTime } from "@optique/temporal";import { option } from "@optique/core/parser";const parser = object({ // Parse ISO 8601 timestamps timestamp: option("--at", instant()), // Parse durations like "PT30M" or "P1DT2H" timeout: option("--timeout", duration()), // Parse zoned datetime with timezone info meeting: option("--meeting", zonedDateTime()),});
The temporal parsers return native Temporal objects with full functionality:
The merge() combinator now supports up to 10 parsers (previously 5), and
the tuple() parser has improved type inference using TypeScript's const
type parameter. These enhancements enable more complex CLI structures while
maintaining perfect type safety.
Breaking changes
While we've maintained backward compatibility for most APIs, there are a few
changes to be aware of:
The Parser.getDocFragments() method now uses DocState<TState> instead
of direct state values (only affects custom parser implementations)
The merge() combinator now enforces stricter type
constraints at compile time, rejecting non-object-producing parsers
Learn more
For a complete list of changes, bug fixes, and improvements, see the
full changelog.
We hope these improvements make building CLI applications with Optique even
more enjoyable. As always, we welcome your feedback and contributions on
GitHub.
We're excited to announce Optique 0.4.0, which brings significant improvements
to help text organization, enhanced documentation capabilities, and introduces
comprehensive Temporal API support.
Optique is a type-safe combinatorial CLI parser for TypeScript that makes
building command-line interfaces intuitive and maintainable. This release
focuses on making your CLI applications more user-friendly and maintainable.
Better help text organization
One of the most visible improvements in Optique 0.4.0 is the enhanced help text
organization. You can now label and group your options more effectively, making
complex CLIs much more approachable for users.
Labeled merge groups
The merge() combinator now accepts an optional label parameter, solving
a common pain point where developers had to choose between clean code structure
and organized help output:
// Before: unlabeled merged options appeared scatteredconst config = merge(connectionOptions, performanceOptions);// Now: group related options under a clear sectionconst config = merge( "Server Configuration", // New label parameter connectionOptions, performanceOptions);
This simple addition makes a huge difference in help text readability,
especially for CLIs with many options spread across multiple reusable modules.
The resulting help output clearly organizes options under
the Server Configuration section:
Demo app showcasing labeled merge groupsUsage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries INTEGERServer Configuration: --host STRING Server hostname or IP address --port INTEGER Port number for the connection --timeout INTEGER Connection timeout in seconds --retries INTEGER Number of retry attempts
The new group() combinator
For cases where merge() doesn't apply, the new group()
combinator lets you wrap any parser with a documentation label:
// Group mutually exclusive options under a clear sectionconst outputFormat = group( "Output Format", or( map(flag("--json"), () => "json"), map(flag("--yaml"), () => "yaml"), map(flag("--xml"), () => "xml"), ));
This is particularly useful for organizing mutually exclusive flags, multiple
inputs, or any parser that doesn't natively support labeling. The resulting
help text becomes much more scannable and user-friendly.
Here's how the grouped output format options appear in the help text:
Demo app showcasing group combinatorUsage: demo-group.ts --json demo-group.ts --yaml demo-group.ts --xmlOutput Format: --json Output in JSON format --yaml Output in YAML format --xml Output in XML format
Rich documentation support
Optique 0.4.0 introduces comprehensive documentation fields that can be added
directly through the run() function, eliminating the need to modify parser
definitions for documentation purposes.
Brief descriptions, detailed explanations, and footers
Both @optique/core/facade and @optique/run now support brief,
description, and footer options through the run() function:
import { run } from "@optique/run";import { message } from "@optique/core/message";const result = run(parser, { brief: message`A powerful data processing tool`, description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`, footer: message`Examples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs`, help: "option"});
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
The complete help output demonstrates the rich documentation features with brief
description, detailed explanation, option descriptions, default values,
and footer information:
A powerful data processing toolUsage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRINGThis tool provides comprehensive data processing capabilities with support formultiple formats and transformations. It can handle JSON, YAML, and CSV fileswith automatic format detection. --port INTEGER Server port number [3000] --format STRING Output format [json] --verbose STRING Verbosity levelExamples: myapp process data.json --format yaml myapp validate config.toml --strictFor more information, visit https://example.com/docs
These documentation fields appear in both help output and error messages
(when configured), providing consistent context throughout your CLI's user
experience.
Display default values
A frequently requested feature is now available: showing default values
directly in help text. Enable this with the new showDefault option when
using withDefault():
Default values are automatically dimmed when colors are enabled, making them
visually distinct while remaining readable.
The help output shows default values clearly marked next to each option:
Usage: demo-defaults.ts [--port INTEGER] [--format STRING] --port INTEGER Server port number [3000] --format STRING Output format [json]
Temporal API support
Optique 0.4.0 introduces a new package, @optique/temporal, providing
comprehensive support for the modern Temporal API. This brings type-safe
parsing for dates, times, durations, and time zones:
import { instant, duration, zonedDateTime } from "@optique/temporal";import { option } from "@optique/core/parser";const parser = object({ // Parse ISO 8601 timestamps timestamp: option("--at", instant()), // Parse durations like "PT30M" or "P1DT2H" timeout: option("--timeout", duration()), // Parse zoned datetime with timezone info meeting: option("--meeting", zonedDateTime()),});
The temporal parsers return native Temporal objects with full functionality:
The merge() combinator now supports up to 10 parsers (previously 5), and
the tuple() parser has improved type inference using TypeScript's const
type parameter. These enhancements enable more complex CLI structures while
maintaining perfect type safety.
Breaking changes
While we've maintained backward compatibility for most APIs, there are a few
changes to be aware of:
The Parser.getDocFragments() method now uses DocState<TState> instead
of direct state values (only affects custom parser implementations)
The merge() combinator now enforces stricter type
constraints at compile time, rejecting non-object-producing parsers
Learn more
For a complete list of changes, bug fixes, and improvements, see the
full changelog.
We hope these improvements make building CLI applications with Optique even
more enjoyable. As always, we welcome your feedback and contributions on
GitHub.