개발 slecs

파서 감시 패키지를 선택적 의존성으로 분리

목차

package.json을 뜯어보다가 거슬리던 게 하나 있었다. @parcel/watcher-darwin-arm64dependencies에 박혀 있던 것. macOS ARM64 전용 네이티브 바이너리인데, 이걸 일반 의존성으로 두면 Linux CI 서버에서 npm install 할 때 아예 실패하거나 경고 폭탄이 날아온다.

@parcel/watcher는 플랫폼별로 네이티브 바이너리를 따로 제공하는 구조다. darwin-arm64용이 dependencies에 들어가 있으면 다른 플랫폼 환경에서 설치할 때 불필요하게 긴장하게 된다. 실제로 설치 로그에 WARN optional 메시지가 뜨거나, 엄격 모드로 돌리는 CI에서 아예 뻗는 경우도 생긴다.

optionalDependencies가 하는 일

npm(그리고 yarn, pnpm)의 optionalDependencies는 이름 그대로 "설치 실패해도 괜찮은 패키지"를 선언하는 자리다. 해당 패키지 설치 중 에러가 나도 npm install 전체가 멈추지 않고 넘어간다. 네이티브 바이너리, 플랫폼별 바인딩, 선택적 기능 모듈처럼 "있으면 쓰고 없으면 말고"인 것들을 여기 넣는다.

// 변경 전
{
  "dependencies": {
    "@parcel/watcher": "^2.4.1",
    "@parcel/watcher-darwin-arm64": "^2.4.1"
  }
}

// 변경 후
{
  "dependencies": {
    "@parcel/watcher": "^2.4.1"
  },
  "optionalDependencies": {
    "@parcel/watcher-darwin-arm64": "^2.4.1"
  }
}

이 한 줄 이동으로 달라지는 게 뭐냐면, Linux 기반 CI/CD 파이프라인에서 npm ci를 돌릴 때 더 이상 darwin-arm64 바이너리 때문에 삐걱거리지 않는다. @parcel/watcher는 런타임에 플랫폼을 감지해서 적절한 바이너리를 로드하는 구조라, darwin-arm64용이 없어도 Linux용(@parcel/watcher-linux-x64-musl 같은 것)이 있으면 알아서 쓴다. optionalDependencies에 두는 게 원래 설계 의도에도 맞다.

왜 이런 게 미뤄지나

솔직히 이런 패키지 정리 작업은 "급한 게 아니라" 미뤄지기 쉽다. 로컬 맥에서 개발하면 darwin-arm64 바이너리가 잘 깔려서 문제가 안 보인다. CI가 Linux라면 언젠가 한 번쯤 install 로그에서 이상한 걸 봤을 수도 있고, 그냥 무시하고 넘어간 경우도 많다.

문제가 수면 위로 올라오는 건 보통 이런 타이밍이다.

  • 새 CI 서버 세팅하다가 npm ci가 이유 없이 느려지거나 실패
  • arm64 빌드용 Docker 이미지에서 환경 맞추다가 꼬임
  • --omit=optional 플래그를 쓰는 배포 스크립트와 조합했을 때 동작이 달라짐

작업 자체는 단순하다. package.json 한 줄 옮기고, lock 파일이 제대로 반영됐는지 확인하고, CI 한 번 돌려서 install이 깨끗하게 되는지 보는 것. 그게 다다. 근데 이걸 챙기려면 의존성 트리를 한 번이라도 들여다봐야 하는데, 그게 생각보다 잘 안 된다. 기능 개발이 밀려 있으면 이런 정리 작업은 항상 뒤로 밀린다.

플랫폼 바이너리 의존성 관리할 때 생각할 것들

구분 dependencies optionalDependencies
설치 실패 시 npm install 전체 실패 무시하고 계속 진행
적합한 패키지 앱 실행에 필수인 것 플랫폼별 바이너리, 선택 기능
lock 파일 항상 포함 포함되지만 실패 허용
--omit=optional 효과 무관 아예 설치 안 함

@parcel/watcher처럼 내부적으로 플랫폼 감지 후 해당 바이너리를 로드하는 패키지들은 보통 각 플랫폼별 패키지를 optionalDependencies로 선언하도록 문서에 안내돼 있다. 그걸 따르지 않고 dependencies에 박아두면 멀티플랫폼 배포할 때 조금씩 발목이 잡힌다.

비슷한 케이스가 꽤 있다. esbuild, sharp, better-sqlite3 같은 네이티브 바인딩 가진 패키지들이 대표적이다. 이것들은 대부분 플랫폼별 바이너리를 optional로 선언하거나, postinstall에서 플랫폼에 맞는 바이너리를 다운로드하는 방식을 쓴다. 결국 같은 문제를 다른 방법으로 푸는 것인데, 공통점은 "특정 플랫폼에서만 쓰는 것을 전체 필수 의존성으로 때려 넣지 말자"는 원칙이다.

한 가지 트레이드오프는 있다. optionalDependencies에 두면 "설치 실패해도 됨"이라는 신호를 npm에 보내는 거라, darwin-arm64 환경인데 실수로 이 패키지가 빠진 경우 파일 감시가 조용히 작동 안 할 수 있다. 그래서 런타임 쪽에서 바이너리 로드 실패를 적절히 로깅하거나, 폴백 동작을 명확히 해두는 게 중요하다. 있으면 쓰고 없으면 말고인 상황에서 "없으면 말고"가 정말로 안전한지 확인하고 넘어가야 한다.


이런 작업들이 쌓이면서 package.json이 조금씩 깔끔해진다. 당장 기능이 바뀌는 것도 아니고 PR 올려도 한 줄짜리 diff라 성취감이 별로 없는 작업이긴 한데, 새 팀원이 온보딩하거나 배포 환경을 새로 세팅할 때 이런 것들이 눈에 보이지 않는 마찰을 줄여준다.

당장 티가 안 나는 작업이지만 나중에 디버깅 시간을 크게 줄여주는 건 이런 것들이다. 6개월 뒤에 CI 로그 뒤지면서 "왜 darwin 바이너리가 Linux 서버에서 설치되려고 하지?"라고 의아해하는 상황 자체를 없애는 것. 미래의 나, 그리고 다음 개발자를 위한 작업이다.

댓글 0

첫 댓글 달아줘.