파이어폭스 버그인지는 모르겠는데 <noscript> 안에 깊숙이 들어가있는 <span>이 밖으로 빠져나온다
잇창명 EatChangmyeong💕🐱
@eatch@hackers.pub · 20 following · 22 followers
*Encounter the Wider World*
🏠
- eatch.dev
🦋
- @eatch.dev
🐘
- @EatChangmyeong@planet.moe
🐙
- @EatChangmyeong
📝₂
- blog.eatch.dev
📝₁
- eatchangmyeong.github.io
왜냐면 이 글을 재작성하고 있고 인터랙티브 부분이 복잡해서 타입 안정성은 웬만하면 챙기고 싶은데 타입스크립트도 안되고 ReScript도 안되고 굳이 Idris를 가져다 쓰는 건 오버킬같고...
많은 우여곡절이 있었지만 어쨌든 해냈습니다. 내일 배포해야지
When code is more complex than it needs to be, its under-engineered, not over
I was wondering when browsers started calling the UI "chrome" (it's not a Google thing!)
Amazingly, the Firefox (then Mozilla) commit that introduced the "chrome" tree into the source code dates back to Sep 4, 1998... which is also the same day Google was founded!
Edit: Netscape used the term much earlier though! Not as much in filenames, but in the actual source code it's all over the place.
Calling all #fediverse developers for help: I'm currently trying to implement a #reporting (#flag) feature for Hackers' Pub, an #ActivityPub-enabled community for software engineers. Is there a formal specification for how cross-instance reporting should work in ActivityPub? Or, is there any well-documented material that explains how the major implementations handle it?
리디에서 마크다운 이벤트를 할 때마다 **이 markdown**인 줄 알고 당황하는 사람
왜요??????
아 DM으로 보냈어야 되는데 공개설정 잘못했다
🏃➡️
혹시 해커스펍에는 신고 기능이 없나요....?? 행동강령에서는 분명 있다고 하는데
문맥적으로 알아서 어떤 타입이어야 하는지 추론해주는 정적 분석기가 달린 언어는 없나
언제까지 (a:number, b:number) => a + b, (a:string, b:string) => a + b, <T>(a: T, b: T) => a + b 를 해줘야 하나고
그냥 대충 눈치껏 (a, b) => a + b 하면 'b 는 a 와 더할 수 있어야 하는 타입이고 a 는 무언가와 더할 수 있는 타입이구나' 하고 추론할 수 있는 분석기가 달린 언어가 필요함
갑자기 삘이 와서 블로그 리팩토링을 한참 했고 이제 각주에도 수식을 쓸 수 있다!!
어떻게 구현했길래 수식을 못 썼냐고요??
- (before)
.mdx파일에 별도 익스포트로 각주 내용을 썼었는데 왠지모르게 JSX 문법은 되지만 마크다운 문법은 안 되는 상황에 봉착... Astro가 별도 익스포트를 못 봐서import.global.meta로 불러오고 원래는 볼 일 없는AstroVNode타입을 써가면서 온몸비틀기로 구현 - (after) 모든 각주가 별도 파일(🤣).
import.global.meta는 아직 있지만 고치기 전보다 훨씬 깔끔해진 느낌
갑자기 삘이 와서 블로그 리팩토링을 한참 했고 이제 각주에도 수식을 쓸 수 있다!!
JS 프레임워크 성격테스트도 있네 😂
I'm Solid! You're the performance-obsessed perfectionist! Take the quiz to find your JS framework match! js-framework-quiz.vercel.app/result/solid
I'm Solid!
겟앰프드가 아직도 서비스 중이라고 해서 웹사이트를 들어가봤는데 파비콘을 보고 깜짝놀랐다
잇창명 EatChangmyeong💕🐱 shared the below article:
Stop writing if statements for your CLI flags
洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
If you've built CLI tools, you've written code like this:
if (opts.reporter === "junit" && !opts.outputFile) {
throw new Error("--output-file is required for junit reporter");
}
if (opts.reporter === "html" && !opts.outputFile) {
throw new Error("--output-file is required for html reporter");
}
if (opts.reporter === "console" && opts.outputFile) {
console.warn("--output-file is ignored for console reporter");
}
A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options.
In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all.
We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one.
The state of TypeScript CLI parsers
The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader.
But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously:
// cmd-ts
const app = command({
args: {
reporter: option({ type: string, long: 'reporter' }),
outputFile: option({ type: string, long: 'output-file' }),
},
handler: (args) => {
// args.reporter: string
// args.outputFile: string
},
});
// Clipanion
class TestCommand extends Command {
reporter = Option.String('--reporter');
outputFile = Option.String('--output-file');
}
These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress.
But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system.
So you end up writing validation code anyway:
handler: (args) => {
// Both cmd-ts and Clipanion need this
if (args.reporter === "junit" && !args.outputFile) {
throw new Error("--output-file required for junit");
}
// args.outputFile is still string | undefined
// TypeScript doesn't know it's definitely string when reporter is "junit"
}
Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type.
If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type?
Modeling relationships with conditional()
Optique treats option relationships as a first-class concept. Here's the test reporter scenario:
import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";
const parser = conditional(
option("--reporter", choice(["console", "junit", "html"])),
{
console: object({}),
junit: object({
outputFile: option("--output-file", string()),
}),
html: object({
outputFile: option("--output-file", string()),
openBrowser: option("--open-browser"),
}),
}
);
const [reporter, config] = run(parser);
The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value.
TypeScript infers the result type automatically:
type Result =
| ["console", {}]
| ["junit", { outputFile: string }]
| ["html", { outputFile: string; openBrowser: boolean }];
When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type.
Now your business logic gets real type safety:
const [reporter, config] = run(parser);
switch (reporter) {
case "console":
runWithConsoleOutput();
break;
case "junit":
// TypeScript knows config.outputFile is string
writeJUnitReport(config.outputFile);
break;
case "html":
// TypeScript knows config.outputFile and config.openBrowser exist
writeHtmlReport(config.outputFile);
if (config.openBrowser) openInBrowser(config.outputFile);
break;
}
No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you.
A more complex example: database connections
Test reporters are a nice example, but let's try something with more variation. Database connection strings:
myapp --db=sqlite --file=./data.db
myapp --db=postgres --host=localhost --port=5432 --user=admin
myapp --db=mysql --host=localhost --port=3306 --user=root --ssl
Each database type needs completely different options:
- SQLite just needs a file path
- PostgreSQL needs host, port, user, and optionally password
- MySQL needs host, port, user, and has an SSL flag
Here's how you model this:
import { conditional, object } from "@optique/core/constructs";
import { withDefault, optional } from "@optique/core/modifiers";
import { option } from "@optique/core/primitives";
import { choice, string, integer } from "@optique/core/valueparser";
const dbParser = conditional(
option("--db", choice(["sqlite", "postgres", "mysql"])),
{
sqlite: object({
file: option("--file", string()),
}),
postgres: object({
host: option("--host", string()),
port: withDefault(option("--port", integer()), 5432),
user: option("--user", string()),
password: optional(option("--password", string())),
}),
mysql: object({
host: option("--host", string()),
port: withDefault(option("--port", integer()), 3306),
user: option("--user", string()),
ssl: option("--ssl"),
}),
}
);
The inferred type:
type DbConfig =
| ["sqlite", { file: string }]
| ["postgres", { host: string; port: number; user: string; password?: string }]
| ["mysql", { host: string; port: number; user: string; ssl: boolean }];
Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less.
With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility.
Try expressing this with requires_if attributes. You can't. The relationships are too rich.
The pattern is everywhere
Once you see it, you find this pattern in many CLI tools:
Authentication modes:
const authParser = conditional(
option("--auth", choice(["none", "basic", "token", "oauth"])),
{
none: object({}),
basic: object({
username: option("--username", string()),
password: option("--password", string()),
}),
token: object({
token: option("--token", string()),
}),
oauth: object({
clientId: option("--client-id", string()),
clientSecret: option("--client-secret", string()),
tokenUrl: option("--token-url", url()),
}),
}
);
Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid.
Why conditional() exists
Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()?
The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely.
But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence.
// This won't work as intended
const parser = or(
object({ reporter: option("--reporter", choice(["console"])) }),
object({
reporter: option("--reporter", choice(["junit", "html"])),
outputFile: option("--output-file", string())
}),
);
When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally.
conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions.
The structure is the constraint
Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint.
| Traditional approach | Optique approach |
|---|---|
| Parse → Validate → Use | Parse (with constraints) → Use |
| Types and validation logic maintained separately | Types reflect the constraints |
| Mismatches found at runtime | Mismatches found at compile time |
The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating.
Try it
If this resonates with a CLI you're building:
- Documentation
- Tutorial
conditional()reference- GitHub
Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead?
The structure of your parser is the constraint. You might not need that validation code at all.
🏃➡️
software gore
12月 6日 서울에서 開催되는 liftIO 2025에서 〈Optique: TypeScript에서 CLI 파서 컴비네이터를 만들어 보았다〉(假題)라는 主題로 發表를 하게 되었습니다. 아직 liftIO 2025 티켓은 팔고 있으니, 函數型 프로그래밍에 關心 있으신 분들의 많은 參與 바랍니다!
오늘 liftIO 2025에서 發表한 〈Optique: TypeScript의 타입 推論으로 CLI 有效性 檢査를 代替하기〉의 發表 資料를 共有합니다! 들어주신 모든 분들께 感謝 드립니다.
사실은 eatch.dev도 next.js인데 SSG로 올려서 그런지 React2Shell 뚫리는지 보니까 안되더라 (그래도 업데이트는 했습니다.)
지금 ‘React2Shell(리액트투쉘)’이라는 이름 하나로 술렁이고 있다. CVSS 10.0 등급이 부여된 신규 취약점 CVE-2025-55182가 공개되면서 전 세계 개발자와 보안전문가들은 “2025년판 Log4Shell”이라는 표현까지 사용하며 심각성을 경고
React/Next.js 쓰신다면 지금 당장 패치하세요. 19.0.1 / 19.1.2 / 19.2.1
2025년판 Log4Shell 이라고 합니다
https://www.dailysecu.com/news/articleView.html?idxno=203111
C++ rant만으로 2시간 분량을 채울 수 있다는 게 놀랍다 (CW: AI 삽화) https://youtu.be/7fGB-hjc2Gc
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ pd6156.tistory.com/63
https://eatchangmyeong.github.io/2022/04/22/interest-driven-mu-recursive-functions.html 이 글을 재작성하고 싶은데 예시로 뭘 들어야 될지 모르겠다..... μ를 반드시 써야 되고 구현 난이도가 적당한 거였으면 좋겠는데 생각나는 게 너무 어려운 것밖에 없다
구글에서 이분 매칭 알고리즘을 검색하면 예를 들어서 https://blog.naver.com/ndb796/221240613074 이런 게 검색되고 코드도 나와서 참고할 수는 있는데... 아무리 찾아봐도 정확성 증명도 없고 이게 왜
LaTeX을 라텏으로 쓰는 사람
out of context svelte
out of context svelte
하스켈에서 다음과 같은 에러를 만날 경우에
withFile: resource busy (file is locked)
readFile 대신 readFile'을 써보셔요!
readFile은 lazy 버전이고readFile'은 strict 버전입니다!
System.IO 모듈 문서에 다음과 같은 설명이 있습니다.
경고:
readFile연산은 파일의 전체 내용을 모두 소비할 때까지 그 파일에 대해 부분적으로 닫힌(semi-closed) 핸들을 유지한다. 따라서 이전에readFile로 연 파일에 대해(writeFile등을 사용하여) 쓰기를 시도하면, 일반적으로isAlreadyInUseError오류와 함께 실패하게 된다.
🏃➡️
마크다운은 토씨랑 상성이 진짜 안맞아서 불편하다... *이것을 찾으셨나요?*에라고 쓰면 "*이것을 찾으셨나요?*에"가 되고 <em> 태그를 써야 제대로 적용이 된다
러스트 문서가 시전하는 이것을 찾으셨나요?에 큰 호감을 느끼고 있다
오늘 백준에서 2문제를 풀었는데 둘다 롱롱죽겠지를 당했다 (long long을 써야 되는데 int를 썼다는 뜻)
금감원에서도 토스 옵션건에 대해 경고를 함
@eatch잇창명 EatChangmyeong💕🐱 다른 서버에서 흘러 들어온 CW 걸린 글에 블러 처리는 해주는데, Hackers' Pub에서 CW 걸린 글을 쓰지는 못해요! (필요 없다고 생각해서 안 만들었는데… 필요할까요?)
@hongminhee洪 民憙 (Hong Minhee) 사실 문제의 그 글을 여기 쓰고 블스로 알피하려는 생각을 했었는데 CW 설정이 없어서 블스에 쓰고 여기로 공유했어요 😅 있으면 좋을 것 같아요
RP) 오 여기도 블라인드 구현이 있나보네 여기서 써서 올릴 때도 블라인드 설정이 가능한가요?
Explicit or potentially disturbing media
CSS IS AWESOMEㅠ (CW: 단순 자살 언급)
오 예상보다 빨리 나왔다
그와중에 99_999_999번부터 100_000_002번까지 1억번을 노린 듯한 예능제출이라는 게 웃기다
백준 지금 제출 속도대로라면 11월 8일 저녁 6시에 제출 번호 1억번이 나오겠다
오 예상보다 빨리 나왔다
ㅇㄴ 포켓CU앱 진짜 골때리네 비밀번호 규칙을 '영문+숫자+특수문자 8자리 이상'이라고만 해놓고 12글자 이상이면 퇴짜놓고 특정한 특수문자를 쓰면 퇴짜놓고 이게 맞냐? 그런 규칙이 있으면 나한테 알려달라고
🏃➡️
새로운 서비스를 실험하고 있는데요.
너무 연속으로 컨텐츠를 봐서 피로해지는 서비스가 아닌, 어쩌다 접속해서 멍 때릴 수 있는 서비스를 고민하며 기획을 했습니다. 가끔 버스 창밖을 바라보며 멍때리는 것처럼요. 멍때리다 창밖의 간판들이 가끔 눈에 들어 오듯, 글이나 낙서가 눈에 띄면 어떨까 싶어서, 초기 인연이 있는 분들에게 부탁해서 다양한 글을 좀 채워 넣으려 했습니다. (AI로 목업을 채워 넣으면 맛이 없을 것 같아서, 실제 다양한 사람들의 글을 원했습니다.) 이게 매우 어려운 벽이다를 실감하고 있습니다.
- SNS 성격의 서비스는 이용하기 싫다.
- 이미 이용 중인 SNS가 여러 개라, 또 추가하기 싫다.
- 로그인 해서 보니, 그다지 나한테 맞지 않는다.
- 몇 번 로그인해서 봐도 흥미가 생기지 않는다.
- 가끔 접속해서 보는 소소한 재미가 있을지도 모르겠다.
- ...
0번은 어차피 제외고, 초기 지인 분들은 적어도 3번까지는 가 주길 기대했는데, 1번조차 넘질 못하고 있습니다. 쓸만한 서비스 혹은, 기획을 조정하면서 고민해 볼 가치가 있는지 보기 위해선, 그래도 1번은 넘어 가야 뭘 할텐데 말입니다. 부탁을 받은 지인들 조차 1번을 넘기 어려운데, SNS 서비스를 홍보한다는 건 꽤 험난한 길이겠습니다.
처음 제가 해커스펍의 1번 문턱을 넘었던 이유를 생각해보면, 저는 사람이었던 것 같습니다. 같은 직군에 있는 사람들이 모여 있어, 대화가 잘 통할 것 같아서 선뜻 들어 온 게 아닐까 싶습니다. 몇 달을 써 보면서 결론은, 해커스펍은 분명 자기만의 영역이 있는 서비스란 생각이 듭니다. 좋다는 생각을 가지기 까지는 좀 써봐야 아는 건데, 해커스펍이 꽤 어려운 걸 돌파했구나란 생각이 듭니다.
혹시 ikariam이라는 게임을 즐겨 본 분 계신가요? 그거, 은근 재밌게 했는데, 주변에서 제가 하는 걸 보더니 "어떻게 그런 게 재밌냐"고 묻는 사람들이 대부분이긴 했습니다. 오랜만에 찾아 보니 아직도 ikariam은 잘 살아 있네요. 멍때림이 싫지 않은 사람들이 분명 있긴 있을텐데, 어떻게 그 분들을 찾아 1번을 넘어가게 할까 고민이네요.
Gleam의 흥미로운 점 두 가지.
- 함수 오버로딩도 인터페이스 다형성도 타입클래스도 없다. 심지어 이건 기본 타입과 연산자에서도 동일해서 정수 타입인
Int와 부동소수점 실수 타입인Float는 사용하는 연산자가 다르다! 실수 두 개를 더하려면5.0 + 3.0이 아니라5.0 +. 3.0이라고 써야 한다. https://tour.gleam.run/basics/floats/ - 위의 링크를 눌러 보았다면 눈치챘을지도 모르겠지만, Gleam에서 0으로 나누기는 에러가 아니다! 이건 정수와 실수 모두 동일하며, 그렇다고
Infinity나-Infinity,NaN따위가 나오는 것도 아니다. Gleam에서a / 0은0이다.
zed에서 mdx 확장 쓰려고 하니까 이렇게 터지는데 제보를 zed에 해야 되는지 확장 개발자한테 해야 되는지 모르겠다
오늘 밤 첫 한파주의보... 가을 "서비스 종료"
- 역명부기: 역이름 옆에 광고를 함
- 혁명부기: 혁명을낉여오라하지않앗느냐
























![Zed의 확장에서 출력된 오류 메시지:
Language server mdx-language-server:
initializing server mdx-language-server, id 7: Server reset the connection
-- stderr --
node:internal/modules/cjs/loader:1247
throw err;
^
Error: Cannot find module 'C:\C:\Users\dlaud\AppData\Local\Zed\extensions\work\mdx\node_modules\.bin\mdx-language-server'
at Function._resolveFilename (node:internal/modules/cjs/loader:1244:15)
at Function._load (node:internal/modules/cjs/loader:1070:27)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:217:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
at node:internal/main/run_main_module:36:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v22.13.1](https://media.hackers.pub/note-media/ed29fc1c-0ddb-41a9-88f6-7b43ee022af0.webp)

