
어제 vscode 어댑터도 추가하고, cli, eslint, vscode 가 동시 배포되는 워크플로도 세팅해서 버전 올라가는 pr 이 머지될 경우 action 이 돌아서 한번에 배포되게끔 세팅을 해두었다.
이쯤에서 내가 라이브러리를 개발하면서 얻은 인사이트를 정리하고 가면 좋을 것 같아서 글을 적게 되었다.
라이브러리를 처음 만들며 찾은 다른 점들
웹앱은 내가 부르는 코드를 내가 실행하는 구조라 자유롭다. 반면 라이브러리는 내가 모르는 남의 코드가, 내가 모르는 타이밍에, 내가 정해둔 규칙대로 내 코드를 불러 쓰는 구조다. 이 차이는 알고 있었지만 실제로 개발을 해보니 또 새로운 인사이트가 많았던 것 같다. 모르는게 많았던 만큼 클로드랑 토론도 많이 하고 다른 오픈소스나 ESlint, Prettier 들 구조도 살펴볼 수 있어서 배울 것들이 많았다.
1. export한 것은 전부 깨면 안되는 약속이다
웹앱에서는 함수 이름 하나를 바꾸면 그걸 호출하는 몇 곳만 고치면 끝나지만 라이브러리에서 index.ts로 export한 함수는 누가 어디서 쓰고 있는지 전혀 알 수 없기에 메이저 업데이트가 아닌이상 바꾸는 것에 신중을 기해야한다. 회사에서 개발을 하며 내가 사용하는 라이브러리의 메이저 업데이트가 있을 때 내 코드에 영향을 주는 부분이 있으면 수정하는 작업을 했던게 생각났다. 가끔 그걸 좀 늦게하면 난데없이 터진 에러에 쒸익거리며 라이브러리 버전업데이트 살피러 가던 추억이 .. 라이브러리를 개발할 땐 공개된 함수명을 바꾸는 순간 그 함수를 쓰던 남의 프로젝트가 <무조건> 깨진다는 생각을 해야한다는 관점의 전환이 흥미로웠던 것 같다. 그래서 index.ts는 일부러 꼭 필요한 것만 export했고, 내부 헬퍼 함수(babelInterop.ts의 순회 로직 같은 것들)는 내보내지 않았다. export 표면을 최대한 좁게 유지하는 것이 라이브러리 설계의 기본이라고 한다.
2. 실행 타이밍을 내가 아니라 호스트가 정한다
웹앱은 코드의 진입점을 내가 소유하지만 라이브러리는 남의 런타임이 결정한다. ESLint 룰의 Program()은 ESLint 엔진이 파일을 순회하다가 알아서 호출하고, VS Code 확장의 activate()는 VS Code가 activation event 조건을 만족할 때 알아서 호출한다. 나는 그 타이밍을 예측할 수 없기에 라이브러리 코드는 언제, 몇 번, 어떤 이상한 입력으로 불려도 죽지 않도록 방어적으로 짜야 한다. (extension.ts의 refresh()에 try/catch를 넣은 이유도 여기에 있다.) 웹앱이었다면 그냥 콘솔에 에러를 찍고 넘어가면 그만이지만, 라이브러리에서 처리하지 않은 예외는 남의 프로그램(VS Code 전체)을 통째로 죽인다.
3. dependencies / devDependencies / peerDependencies의 차이
웹앱은 셋 다 그냥 npm install해서 배포할 때 전체를 넘기기 때문에 크게 구분해서 사용하지 않았지만
dependencies는 내가 쓰지만 소비자의 node_modules에도 그대로 딸려가는 <짐>이다. 하나 추가할 때마다 남의 설치 용량과 시간이 늘어난다.
peerDependencies는 이건 내가 직접 설치하지 않을 테니, 너(소비자)가 이미 가진 걸 그대로 써라는 뜻이다. 어차피 eslint 기반 룰을 추가해서 쓰는 사용자는 eslint 를 이미 가지고 있을 테니.. 이를 peer로 선언하였다. 한 프로젝트 안에 ESLint가 여러 버전 동시에 떠 있으면 안 되기 때문에, 그 프로젝트가 이미 설치해둔 단 하나의 eslint를 공유해서 써야 한다.
devDependencies는 라이브러리를 만들 때만 필요하고 소비자에게는 전혀 넘어가지 않는다. files: ["dist"]가 이를 보장.
4. 배포되는 코드 ≠ 내 코드
웹앱은 코드와 실제로 돌아가는 것이 거의 같다. 바로 위에 적은것처럼 라이브러리는 files 필드와 빌드 단계로 걸러낸 일부만 배포된다. 나또한 src/**/*.test.ts는 tsconfig에서 제외했고, files: ["dist"]로 테스트 파일이나 원본 .ts가 소비자 설치본에 절대 들어가지 않도록 막아두었다.
*c.f. 웹앱
웹앱은 배포되는 게 남의 설치본이 아니라 번들러(vite/webpack/next)가 만든 결과물. 번들러는 파일명 패턴으로 거르는 게 아니라 진입점(main.tsx, app/page.tsx)에서 시작해서 import를 따라가며 그래프를 그리고, 거기 걸리는 것만 결과물에 넣는 방식이다. 테스트 파일(*.test.ts) 같은 경우 애초에 프로덕션 코드 어디서도 import 안 하니까, 따로 뭘 안 해도 자연스럽게 번들에서 빠지게 되는 것. 정리하자면 라이브러리처럼 필터링을 안 해서 빠지는 게 아니라, 처음부터 그래프에 안 걸려서 못 들어가는 것이다.
5. semver
라이브러리는 전혀 모르는 사람의 자동화 도구(^0.3.0 같은 범위)가 사람이 읽지도 않고 '이 새 버전을 믿고 받을지'를 결정한다. 그래서 patch 버전에 breaking change를 몰래 넣으면, 갑자기 빌드가 깨지는 일이 생긴다.
*semver: Semantic Versioning는 버전 번호 X.Y.Z의 각 자리에 의미를 부여하는 규칙
X (MAJOR) 기존 사용법을 깨는 변경이 있을 때만 올림 (예: export하던 함수를 지우거나 시그니처를 바꿈)
Y (MINOR) 기존 사용법은 그대로 두고, 새 기능만 추가했을 때. (예: suggestContrastFix 새로 추가)
Z (PATCH) 사용법은 안 바뀌고 버그만 고쳤을 때.
MAJOR/MINOR/PATCH를 정직하게 매겨야 남의 프로젝트가 안 깨진다.
이번에 한번 더 와닿았던 것: 0.x.y(즉 아직 1.0.0 전)일 때는 규칙이 다르다. ^ 기호는 보통 "왼쪽에서 첫 번째 0이 아닌 자리까지만 고정"인데, 1.0.0 이전엔 MINOR 자리가 사실상 MAJOR처럼 취급된다. (아직 안정 버전 아니니까 사소해 보이는 변경도 깨질 수 있다)
^1.2.3 → 1.2.3 ~ 2.0.0 미만 (MINOR/PATCH 자유)
^0.3.0 → 0.3.0 ~ 0.4.0 미만만 허용 (PATCH만 자유, MINOR도 못 올라감!)
6. '플러그인'류 라이브러리는 실행되는 게 아니라 등록된다
ESLint 룰, VS Code 확장, Babel/Vite 플러그인은 전부 설치되자마자 위에서 아래로 실행되는 게 아니라, '정해진 모양(shape)을 export하면 호스트가 필요할 때 알아서 불러 쓰는 구조다. 우리 ESLint 플러그인도 Program() visitor라는 정해진 모양을 export할 뿐, 스스로 언제 실행될지 정하는 코드는 한 줄도 없다.
7. README = API
웹앱에서의 Readme 의 중요성보다 훨씬 높음
라이브러리는 npm install 하기 전에 남이 소스코드를 미리 읽어보는 일이 거의 없고 README와 타입 정의가 거의 전부이기에 신경써야한다.
'Frontend > 🔨 JS' 카테고리의 다른 글
| Tailwind-a11y 개발 기록 2편 (Babel AST로 Tailwind 접근성 분석기 만든 과정) (0) | 2026.07.27 |
|---|---|
| Tailwind-a11y 개발 기록 (라이브러리 npm 배포!) (0) | 2026.07.27 |
| new Function로 함수 만들기 (0) | 2026.06.04 |
| 자바스크립트 효율적으로 처리하기 (requestAnimationFrame, requestIdleCallback, Web Workers) (0) | 2026.05.11 |
| getElementsByClassName 함수 구현하기 (+자바스크립트에서 유사배열과 배열, 변환 방법) (0) | 2025.09.03 |
댓글