Prologue: 외부 패키지가 내 코드를 터뜨렸을 때
학부생 시절 사료 비판(Historical Criticism)을 배울 때 가장 인상 깊었던 조언은 "아무리 신뢰받는 일류 역사가의 텍스트일지라도, 그 안의 미세한 왜곡이나 누락을 비판적으로 뜯어보고 바로잡아야만 진정한 역사 해석에 도달할 수 있다"는 것이었습니다.
자바스크립트 생태계에서 개발을 배우고 다양한 오픈소스 패키지를 설치해 쓰다 보니, 이 사료 비판의 조언이 뼈저리게 와닿았습니다. 대형 라이브러리나 깃허브 스타가 수만 개인 믿음직한 오픈소스 패키지라 할지라도, 특정 상황에서는 치명적인 버그를 일으키거나 하위 의존성(Transitive Dependencies) 버전 충돌로 인해 프로젝트 전체를 중단시킬 수 있기 때문입니다.
최근 진행하던 모노레포 프로젝트에서, 유서 깊은 애니메이션 라이브러리가 사용하는 하위 헬퍼 패키지의 특정 버전에서 타입 오류가 터져 빌드가 통째로 실패하는 일이 발생했습니다.
"패키지 메인테이너가 내 풀 리퀘스트(PR)를 병합하고 새 버전을 배포해 줄 때까지 하염없이 기다려야 할까?" 아니면 "이 패키지 깃허브를 포크(Fork)해서 직접 수정한 뒤 내 깃 주소로 패키지를 다시 등록해야 할까?"
두 방법 모두 끔찍한 시간 낭비와 유지보수 지옥을 예고하고 있었습니다. 하지만 모던 패키지 매니저인 pnpm의 overrides와 patch 기능을 적용하면서, 단 몇 분 만에 이 우스꽝스러운 의존성 고차방정식을 해결할 수 있었습니다.
Concept: 오픈소스 수정을 위한 전통적인 꼼수들의 문제점
pnpm의 모던한 해결책을 알아보기 전에, 그동안 개발자들이 패키지 버그나 의존성 충돌을 해결하기 위해 썼던 전통적인 '우회로'들의 단점을 되짚어 볼 필요가 있습니다.
1. node_modules 직접 수정하기
가장 빠르지만 최악의 방법입니다. 로컬 컴퓨터의 node_modules/bad-package/index.js를 열어 버그를 고치면 로컬에선 잘 돌아갑니다.
- 치명적 단점: 이 수정 사항은 절대 Git에 기록되지 않습니다. 따라서 동료 개발자가 코드를 받거나, CI/CD 서버에서 빌드를 수행할 때
npm install을 새로 실행하면 수정 사항이 완전히 날아가고 다시 빌드 오류가 발생합니다.
2. 패키지 저장소 Fork 및 재배포
오픈소스 깃허브 레포지토리를 포크하여 버그를 수정한 뒤, 내 깃허브 계정에 푸시하고 package.json의 의존성을 내 레포 주소로 변경하는 방법입니다.
- 치명적 단점: 오픈소스에 새로운 업데이트나 보안 패치가 올라왔을 때 내 포크본과 싱크를 맞추는 과정(Upstream Sync)이 무척 번잡합니다. 시간이 지나면 결국 관리되지 않는 버려진 포크 패키지가 되어 기술 부채의 원흉이 됩니다.
3. patch-package 도구 활용
npm이나 yarn v1 시절에는 patch-package라는 외부 라이브러리를 설치해 패키지 변경 사항을 .patch 파일로 떠서 관리하곤 했습니다.
- 단점: 패키지 매니저의 기능이 아니기 때문에 플러그인을 추가 설치해야 하고, 패키지 매니저 버전과 궁합이 맞지 않으면 동작하지 않는 리스크가 존재했습니다.
Deep Dive: pnpm이 제공하는 두 가지 절대 조커 카드
고속 전용 설치와 디스크 효율성으로 주목받는 pnpm은 이 귀찮은 패키지 수정 과정을 아예 자체 코어 기능으로 우아하게 내장해 두었습니다.
1. pnpm.overrides: 하위 버전 강제 교통정리
pnpm.overrides는 **"내가 설치한 패키지들이 내부적으로 부르는 하위 패키지의 버전을, 특정 버전으로 강제 통일(Override)해 버리는 기능"**입니다.
예를 들어, 내 프로젝트가 A 패키지(v1.0)와 B 패키지(v2.0)를 쓰는데, 이 둘이 내부적으로 lodash 패키지를 각각 다른 구버전(v3.0, v4.0)으로 들고 있어 충돌이 나거나 보안 경고(Vulnerability)가 뜰 때 유용합니다. package.json 파일에 아래와 같이 명시해 주면 끝납니다.
// package.json
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21"
}
}
}
이렇게 선언해 두면 pnpm은 깊숙한 하위 종속성 트리에 매핑된 모든 lodash 호출을 무조건 v4.17.21로 강제 매핑하여 설치해 줍니다. 중복 설치도 방지되고 버전 불일치 버그도 깔끔하게 해소됩니다.
2. pnpm patch: 소스코드 즉석 봉합 수술
pnpm patch는 **"설치된 패키지의 특정 코드를 수정하고, 그 변경점을 패치(Patch) 파일로 떠서 Git에 영구 저장하는 기능"**입니다.
이 기능의 놀라운 점은 임시 격리 디렉토리를 자동으로 생성해 주고, 수정 작업을 마친 후 반영 명령만 치면 패키지 매니저가 자동으로 패치 구조를 떠서 관리해 준다는 것입니다.
Practical: pnpm patch 기능 실전 튜토리얼
타입 버그가 존재하는 가상의 패키지 broken-slider를 직접 패치하는 과정을 단계별로 소개하겠습니다.
1. 패치 대상 패키지 지정하기
터미널에서 패치를 시작하고 싶은 패키지와 버전을 명시해 명령어를 실행합니다.
pnpm patch broken-slider@1.2.3
명령어를 실행하면 pnpm은 아래와 같은 임시 작업 공간(Temp Directory) 경로를 출력하며 대기 상태에 들어갑니다.
You can now edit the package in the following temporary directory:
/var/folders/xx/xxxx/T/pnpm-patch-broken-slider-1.2.3/node_modules/broken-slider
Once you are done, run:
pnpm patch-commit /var/folders/xx/xxxx/T/pnpm-patch-broken-slider-1.2.3/node_modules/broken-slider
2. 소스코드 수정하기
안내된 임시 디렉토리를 에디터(VS Code 등)로 엽니다. 문제가 되었던 소스코드(예: dist/index.js 혹은 타입 정의 파일 index.d.ts)를 찾아 버그를 올바르게 수정합니다.
// index.d.ts 수정 전
export interface SliderProps {
onChange: (val: number) => void;
// value가 string으로만 고정되어 있어 number를 전달하면 컴파일이 깨짐
value: string;
}
// index.d.ts 수정 후
export interface SliderProps {
onChange: (val: number) => void;
value: string | number; // 타입을 유연하게 수정하여 버그 해결!
}
3. 패치 사항 확정하고 저장하기
수정을 마쳤다면, pnpm이 터미널에 안내했던 커밋(Commit) 명령어를 그대로 실행합니다.
pnpm patch-commit /var/folders/xx/xxxx/T/pnpm-patch-broken-slider-1.2.3/node_modules/broken-slider
4. 결과물 확인
성공적으로 커밋되면 프로젝트 루트 경로에 patches/ 폴더가 생성되고, 그 안에 broken-slider@1.2.3.patch 파일이 생성됩니다. 이와 동시에 프로젝트의 package.json 파일도 패치 구성을 가리키도록 자동으로 업데이트됩니다.
// package.json 자동 수정 내용
{
"dependencies": {
"broken-slider": "1.2.3"
},
"pnpm": {
"patchedDependencies": {
"broken-slider@1.2.3": "patches/broken-slider@1.2.3.patch"
}
}
}
이제 이 patches/ 폴더와 package.json의 수정 사항을 Git에 스테이징하여 커밋(git add patches/ package.json)하기만 하면 됩니다.
앞으로 내 동료 개발자나 배포 서버(CI/CD)에서 pnpm install을 실행할 때마다, pnpm은 원본 broken-slider를 다운로드한 직후 우리가 커밋한 patches/ 폴더 안의 .patch 코드를 자동으로 덮어씌워(Apply) 빌드를 수행합니다. 포크 레포도 필요 없고, 매번 수동으로 node_modules를 건드릴 필요도 없는 완벽한 자동화입니다.
Epilogue: 사료 비판과 의존성 비판
역사적 텍스트를 무조건 숭배하지 않고 왜곡을 바로잡는 사료 비판의 눈을 가져야 하듯, 현대 웹 개발자 또한 외부 패키지를 맹목적으로 믿고 끌어다 쓰기만 해서는 안 됩니다. 내가 만든 프로덕션 애플리케이션의 뼈대와 결합하는 외부 라이브러리에 버그가 있다면, 이를 주도적으로 분석하고 통제할 수 있는 힘을 갖추어야 합니다.
pnpm의 overrides와 patch라는 훌륭한 도구를 손에 쥔 이상, 우리는 이제 패키지의 사소한 결함 때문에 서비스 릴리즈 일정을 늦추거나 복잡한 빌드 구조로 우회할 필요가 없어졌습니다. 의존성 지옥을 내 통제 하에 두고 우아하게 땜질할 줄 아는 능력이 모던 프론트엔드 장인으로 가는 중요한 징검다리입니다.