Posts
npm이 거절한 이름 xl3, 그래서 만든 xl3-lang

npm이 거절한 이름 xl3, 그래서 만든 xl3-lang

npm 자동 유사이름 차단에 걸려 xl3라는 이름을 영영 못 쓰게 된 이야기. 이름 하나 막혔을 뿐인데 이 프로젝트가 패키지인지 표준인지 스스로도 정하지 않고 있었다는 게 드러났고, 거절 메일 한 통에서 org 생성과 릴리스까지 굴러간 하루의 기록

브랜치 하나를 애매하게 남겨둔 채로 이틀이 지났다.

이름은 unscoped-xl3-publish. npm 패키지 이름을 @jinyoung4478/xl3에서 스코프 없는 맨 이름 xl3로 바꾸려던 브랜치였다. 미머지 상태라 지우지도 머지하지도 못하고 그냥 뒀다.

지울지 둘지 결정을 못 한 이유는 단순하다. xl3라는 이름을 아직 포기 못 했기 때문이다. npm view xl3를 치면 404가 뜬다. 비어 있는 이름이다. 비어 있으면 쓸 수 있는 거 아닌가.

근데 publish를 하면 이런 에러가 났다.

Package name too similar to existing packages

그래서 npm support에 문의를 넣어뒀고, 답장을 기다리는 중이었다.

답장

답장은 문의 넣은 그날 밤에 이미 도착해 있었다. 열어본 건 이틀 뒤였고.

npm Support의 답변 메일. 자동 유사이름 차단에 걸린 unscoped 이름은 수동 승인도 일회성 예외도 불가능하고, 본인 소유 스코프로 퍼블리시하라는 내용

When an unscoped name is blocked by this system, we cannot manually approve it or make a one-off exception for that unscoped name, even if npm view xl3 returns a 404 and even if the project is legitimate.

세 문장으로 정리하면 이렇다.

  • 이 에러는 스코프 없는 이름에 걸리는 npm의 자동 유사이름 차단에서 나온다
  • 이 시스템이 막은 이름은 수동 승인도, 일회성 예외도 안 된다. npm view가 404를 뱉어도, 프로젝트가 정당해도
  • 현실적인 길은 본인 소유 스코프로 퍼블리시하고 CLI 명령어 이름만 bin 필드로 유지하는 것

비어 있는 이름과 쓸 수 있는 이름은 다르다. 타이포스쿼팅 방지 로직이 기존 패키지들과 이름이 너무 비슷하다고 판단하면 자동으로 막고, 그건 사람이 풀어줄 수 없다. xl, xlsx, xls3 같은 것들 사이에 xl3가 끼려고 했으니 걸릴 만도 하다.

교훈 하나. 새 프로젝트 이름 정할 때 npm view로 확인하는 건 검증이 아니다. npm publish --dry-run까지 돌려봐야 한다. 나는 이름을 다 정하고, 문서를 다 쓰고, 웹사이트까지 만들고 나서 알았다.

브랜치 부검

답장을 받고 나서 그 애매한 브랜치가 뭘 하는 브랜치였는지 다시 봤다.

37 files changed, 66 insertions(+), 66 deletions(-)
 {
-  "name": "@jinyoung4478/xl3",
+  "name": "xl3",
   "version": "0.9.0",

37개 파일, 66줄 추가에 66줄 삭제. package.json 하나 빼면 나머지는 전부 문서와 웹사이트의 @jinyoung4478/xl3xl3로 바꾸는 문자열 치환이었다. 한국어 문서, 일본어 문서, 중국어 문서까지 골고루.

브랜치의 존재 이유가 통째로 증발한 순간이다. 목표가 막혔으니 남길 이유가 없다.

미련 없이 지우되 복구 경로는 남기는 게 좋다.

git rev-parse unscoped-xl3-publish   # 1830a06 — reflog로 90일 복구 가능
git branch -D unscoped-xl3-publish

SHA를 먼저 찍어두고 강제 삭제. 브랜치 목록이 main 하나로 깨끗해졌다.

여기까지가 2분이다. 그리고 여기서 끝났으면 이 글도 없었다.

"이건 그저 JS 라이브러리 이름 문제일 뿐"

브랜치를 지우고 2분 30초쯤 지나서, 나는 이렇게 썼다.

xl3는 내가 제안한 엑셀 변환 규칙 표준이야. 이거 js npm 라이브러리에 xl3를 못 쓰는 것은 뭐 그저 js 라이브러리가 이름 사용 불가한 이슈일 뿐.

나는 xl3 라이브러리를 markdown처럼 표준을 제시하는 저장소로 만들고 싶어.

쓰고 나서야 내가 뭘 말했는지 알았다.

npm 레지스트리는 상가 건물이고, 패키지 이름은 그 안의 호실 이름표다. 나는 xl3라는 이름표를 못 달았다. 그래서 두 달째 그 이름표에 매달려 있었다.

근데 애초에 이름표를 달려던 게 아니었다. 내가 만들려던 건 가게가 아니라 업종이었다.

Markdown이 딱 그렇다. Markdown은 npm 패키지 이름이 아니다. 포맷 이름이다. 그 포맷을 구현한 파서들은 marked, markdown-it, remark처럼 각자 자기 이름을 달고 산다. Markdown이라는 이름이 npm에서 비어 있냐 아니냐는 Markdown이 표준이라는 사실과 아무 상관이 없다.

xl3도 그래야 했다. 그런데 나는 xl3라는 이름을 네 군데에 동시에 쓰고 있었다.

  • 내가 제안하는 표준(엑셀 = 실행 가능한 템플릿)
  • 그 표준의 JS 레퍼런스 구현
  • npm 패키지
  • GitHub 저장소

전부 xl3. 네 개가 한 이름을 공유하니 npm에서 이름 하나 막힌 게 프로젝트 전체가 막힌 것처럼 느껴진 거다. 계층이 안 나뉘어 있으니까.

한 줄로 못 박으면 끝나는 문제였다.

xl3              = 선언적 엑셀 변환 표준 (포맷)
XTL              = 그 안에서 쓰는 표현식 언어
@xl3-lang/xl3    = xl3 표준의 레퍼런스 구현 하나

이 세 줄이 README 맨 위에 있으면 JS 구현은 자동으로 "여러 구현 중 하나"로 읽힌다. 스코프가 붙었다는 게 오히려 정확한 표기가 된다. @xl3-lang/xl3는 "xl3 표준을 만드는 곳에서 낸 xl3 구현"이라고 읽히니까.

재료는 다 있는데 그렇게 안 읽히던 저장소

정체성을 정리하고 저장소를 다시 보니 웃긴 게, 표준 저장소의 재료는 이미 다 있었다.

  • spec/ — XTL 스펙과 ADR
  • conformance/ — 언어 중립 실행 스펙, 픽스처 157개
  • PORTERS_GUIDE.md, IMPLEMENTATIONS.md — 구현이 이미 셋(TS 레퍼런스 / Rust·WASM 일부 / Python 초안), 각각 별도 저장소
  • GOVERNANCE.md, STABILITY.md — 스펙 버전(XTL 0.1)과 패키지 버전(0.9.0)을 따로 관리

스펙 옆에 레퍼런스 구현을 두고 다른 언어 구현은 별도 저장소로 빼는 건 CommonMark, TOML, JSON Schema가 쓰는 바로 그 모델이다. 구조는 이미 그렇게 돼 있었다.

문제는 읽히는 방식이었다.

README 첫 문장이 Declarative Excel Template Engine으로 시작했다. 엔진, 즉 도구 프레이밍이다. 그리고 루트 디렉터리에 data.xlsx, template.xlsx, 2026-09-report.xlsx, demo.py, GEO-*.md가 널브러져 있었다. 내가 테스트하면서 던져둔 것들.

스펙이 아무리 잘 쓰여 있어도 루트에 demo.py가 있으면 "작업 중인 개인 repo"로 읽힌다. 표준 저장소는 루트가 spec/ conformance/ README GOVERNANCE IMPLEMENTATIONS 정도로 깔끔해야 권위가 생긴다.

그래서 그날 한 일은 이렇다.

  1. README를 표준 선언으로 다시 씀 (엔진 소개 → 표준 소개 + 이 저장소가 스펙·컨포먼스·레퍼런스 구현을 담는다는 문장)
  2. 루트에 널린 작업 파일 정리
  3. GitHub org 이전 — jinyoung4478/xl3xl3-lang/xl3
  4. npm 스코프 이전 — @jinyoung4478/xl3@xl3-lang/xl3

org를 판 이유

3번이 제일 컸다.

표준 저장소들 주소를 모아놓고 보면 생김새가 비슷하다.

toml-lang/toml
commonmark/commonmark-spec
json-schema-org/json-schema-spec

전부 org 아래에 있고, org 이름이 곧 표준 이름이다. 개인 아이디가 안 보인다.

jinyoung4478/xl3는 주소만 봐도 개인 프로젝트다. GOVERNANCE.md를 아무리 정성 들여 써놔도, 저장소 주소에 내 아이디가 박혀 있으면 "이 사람이 혼자 만든 것"으로 먼저 읽힌다. 표준은 누가 만들었는지보다 누구나 구현할 수 있는지로 읽혀야 하는데, 개인 계정은 계속 반대 신호를 보낸다.

혼자 하는 프로젝트에 org를 파는 게 좀 허세 같기도 했다. 근데 org를 파는 게 곧 "여러 명이 한다"는 주장은 아니다. 이름의 소유권을 개인에게서 떼어놓는 선언에 가깝다. 내가 손을 놔도 이 스펙과 구현들이 같은 자리에 남아 있을 수 있다는 것. 구현이 이미 셋인데 그중 하나(TS)만 내 계정 아래 있는 것도 이상했고.

그래서 xl3-lang을 만들고 저장소를 옮겼다. npm 스코프도 따라갔다.

jinyoung4478/xl3     ->  xl3-lang/xl3
@jinyoung4478/xl3    ->  @xl3-lang/xl3

여기서 처음에 잃었던 이름이 돌아온다. 맨 이름 xl3는 끝내 못 얻었지만, xl3-lang이라는 org 이름으로는 아무 문제 없이 얻었다. npm이 막은 건 unscoped 패키지 이름 하나였지 xl3라는 단어가 아니었던 거다.

두 달을 매달렸던 게 실은 레지스트리 최상위 네임스페이스 한 칸이었다는 것도 이때 알았다.

절반은 순서

할 일이 정해지고 나서 순서를 정하는 데 시간을 좀 썼는데, 이게 꽤 중요했다.

1. 끝난 것 먼저          — 열려 있던 PR 병합
2. 비가역·기반 먼저       — org 이전 (GitHub UI, 나만 할 수 있음)
3. 싼 기계적 작업        — URL 208곳 일괄 스윕
4. 비싼 위험한 리팩터 마지막 — JS 스캐폴딩 이관

org 이전을 먼저 한 게 핵심이다. 이전을 먼저 해두면 이후 모든 PR이 최종 위치에서 만들어진다. 순서를 뒤집어서 URL부터 고쳤으면 이전 후에 또 고쳐야 했다. GitHub이 옛 주소를 자동 리다이렉트해주니까 이전하는 동안 깨지는 것도 없다.

그리고 URL 스윕과 JS 이관은 반드시 다른 PR로 나눴다. 208곳 문자열 치환과 디렉터리 구조 리팩터를 한 PR에 담으면 리뷰가 불가능한 괴물이 된다. 혼자 하는 프로젝트여도 나중의 내가 리뷰어다.

--access public을 줬는데 restricted로 올라간 패키지

새 스코프 @xl3-lang/xl3의 첫 publish에서 걸렸다.

배포하고 나서 상태를 확인했는데 이랬다.

  • 구 패키지 @jinyoung4478/xl3 → deprecate 성공, "새 이름으로 가세요" 안내 붙음
  • 신 패키지 @xl3-lang/xl3 → npm에 없음 (404)

구 패키지는 새 이름으로 가라고 안내하는데, 정작 새 패키지가 안 올라간 상태. 제일 안 좋은 조합이다.

원인은 첫 publish가 --access public 플래그를 줬는데도 restricted로 올라간 것이었다. 증상이 헷갈렸던 게, 내 계정으로 웹에서 보면 페이지가 멀쩡히 보인다. 익명으로 조회할 때만 404가 난다. 소유자한테는 안 보이는 문제다.

npm access set status=public @xl3-lang/xl3

이거 한 줄로 해결. 새 스코프에서 처음 publish할 때는 플래그를 믿지 말고 배포 후에 익명으로 한 번 확인해보는 게 맞다.

코드에 안 남는 사실

이번 일에서 제일 오래 남을 것 같은 건 이 부분이다.

"npm이 xl3라는 이름을 영구적으로 막았다"는 사실은 저장소 어디에도 안 남는다. 커밋 메시지에도 없고, 코드에도 없고, 문서에도 없다. 남는 건 @xl3-lang/xl3라는 결과뿐이다.

그러면 6개월 뒤의 내가 package.json을 열어보고 이렇게 생각한다. "스코프 붙어 있으니까 좀 길다. 그냥 xl3로 바꿔볼까?" 그리고 똑같은 브랜치를 다시 판다.

실제로 이미 한 번 그랬다. unscoped-xl3-publish가 그거다.

그래서 메모를 하나 적어뒀다.

unscoped xl3 — npm 자동 유사이름 차단. 영구 불가.
support 확인함, 예외 없음. npm view가 404여도 마찬가지.
→ @xl3-lang/xl3로 간다
→ 다시 시도하지 말 것. 리네임 브랜치 다시 파지 말 것.

여기서 중요한 건 마지막 줄이다. "스코프 유지"라고만 써두면 6개월 뒤에 "왜?"가 안 남는다. 왜 막혔는지랑 뭘 하지 말아야 하는지까지 있어야 안 판다.

코드로 표현이 안 되는 외부 제약은 이렇게 따로 적어두는 수밖에 없다.

돌아보며

거절이 이름을 뺏은 게 아니라 계층을 정리해줬다.

그전까지 나는 xl3가 표준인지 라이브러리인지 스스로도 정하지 않은 채로 둘 다인 척하고 있었다. 표준이라고 말하면서 npm 이름에 매달렸고, 라이브러리라고 하기엔 스펙과 컨포먼스 스위트를 열심히 쓰고 있었다. 그 모호함이 비용을 안 물리니까 그냥 뒀던 거다.

npm이 이름을 막으면서 처음으로 비용이 발생했고, 그제서야 골랐다.

간판은 @xl3-lang/xl3로 걸었다. 업종 이름은 여전히 xl3다.

이게 오히려 맞다. 나중에 Rust 구현이나 Python 구현이 프로덕션급이 되면 걔들도 자기 이름표를 달고 나올 거고, 그때 xl3가 특정 언어 패키지 이름으로 선점돼 있었으면 그게 더 이상했을 거다. marked가 Markdown을 소유하지 않는 것처럼.

이름을 못 얻은 대신 이름의 자리를 얻었다.