요약: 카카오페이는 전사 프론트엔드 표준 패키지 매니저를 Yarn Berry에서 pnpm으로 전환했습니다. 이 글은 과거의 해결책이었던 Yarn Berry가 변화된 환경에서 새로운 병목이 된 배경과, 이를 해결하기 위해 pnpm을 선택한 과정을 담고 있습니다. 단순히 두 기술의 기능을 비교하는 데 그치지 않고, 카카오페이의 환경을 분석하고 데이터를 검증하며 새로운 기술을 도입하기까지의 의사결정 과정을 공유합니다.
💡 리뷰어 한줄평
ben.lee 새로운 기술 도입이 단순한 시도에 그치지 않고 회사가 가지고 있는 문제를 본질적으로 개선할 수 있는지에 대한 고민, 이를 뒷받침하는 객관적인 지표를 수집하고 기술 도입에 대한 당위성을 실질적인 성과로 증명해낸 과정을 잘 다루고 있습니다. 기술 도입의 타당성을 고민하는 모든 이들에게 실질적인 가이드가 될 수 있는 좋은 글입니다.
john.bosco 패키지 매니저와 관련한 글이지만, 환경의 변화가 기술 선택에 영향을 줄 수 있다는 것을 생각하며 읽으면 좋을 것 같습니다. 환경 변화에 따른 변경점과 그에 따른 기술 선택 과정을 실제 데이터 기반으로 잘 풀어낸 글입니다.
시작하며
안녕하세요, 카카오페이에서 프론트엔드 개발을 하고 있는 라즈입니다.
작년, 카카오페이는 전사 프론트엔드 표준 패키지 매니저를 Yarn Berry에서 pnpm으로 전환했습니다.
이 글은 과거의 해결책이었던 Yarn Berry가 변화된 환경에서 새로운 병목이 된 배경과, 이를 해결하기 위해 pnpm을 선택한 과정을 담고 있습니다. 단순히 두 기술의 기능을 비교하는 데 그치지 않고, 카카오페이의 환경을 분석하고, 데이터를 검증하며, 새로운 기술을 도입하기까지의 의사결정 과정을 공유합니다.
Yarn Berry 도입 배경
카카오페이가 Yarn Berry를 처음 도입할 당시에는 의존성을 설치하기 위해 사용하는 사내 레지스트리 서버 환경이 좋지 못했습니다. 대부분의 레포지토리가 한 개의 서비스만 담당하도록 쪼개져 있었음에도 불구하고 단순 의존성 설치에도 10분 정도가 소요되었습니다. 심지어 타임아웃으로 배포가 실패하는 경우도 종종 발생해 생산성에 큰 지장을 주고 있었습니다.
이런 환경을 극복하기 위해, 한 번 설치된 의존성을 재사용하여 의존성 설치 단계를 생략하는 Zero-installs 방식의 Yarn Berry를 적용했습니다. 그 결과 의존성 설치 시간이 최대 95%까지 단축되었고, 더 이상 레지스트리 서버 상태에 영향을 받지 않고도 빠르게 개발하고 배포할 수 있었습니다.
Yarn Berry + Zero-installs의 문제점
카카오페이의 성장과 함께 각 레포지토리의 규모와 복잡도도 올라갔고, 과거에는 없었던 Zero-installs를 사용하는 Yarn Berry의 문제들이 수면 위로 드러나기 시작했습니다.
- Git 부담
모든 의존성이 파일 시스템에 저장되어 Git에 포함되기 때문에 fetch와 clone 등의 일상적인 명령을 실행할 때의 비용이 늘어났습니다.
모노레포로 구성된 레포지토리에서는 배포 파이프라인에서 checkout 시간이 문제가 될 정도로 길어지면서 shallow clone으로 최적화를 해야만 했습니다.
- 사용성
PnP(Plug and Play)와 Zero-installs 방식에는 사소해 보이지만 개발자 경험(DX)을 저해하는 불편함이 존재했습니다.
- PR diff 확인의 어려움: 의존성 변경 사항이 PR에 포함되기 때문에, 대규모 리팩토링이나 의존성 변경이 많은 경우 PR diff가 3000개가 넘어가면서 코드 리뷰 시 실제 수정 사항을 확인하기 어려운 경우가 종종 발생했습니다.
- IDE 설정의 번거로움: 자바스크립트 생태계의 사실상 표준인
node_modules를 사용하지 않기 때문에, 타입 추론이나 린팅 등 IDE에서 제공하는 기능을 사용하려면 추가적인 에디터용 SDK를 설치해야 했습니다. - 디버깅의 어려움: 라이브러리 내부 구현을 확인하거나 디버깅하기 위해 파일을 열면 ZipFS 경로로 연결되어 읽기 전용으로 열립니다. 코드를 직접 수정하며 테스트하는 것이 불가능하기 때문에 디버깅에 제약이 있었습니다.
- 메모리 스파이크
가장 치명적인 문제는 배포 빌드 단계에서 발생하는 메모리 스파이크였습니다.
일부 SSR 서비스의 도커 이미지 빌드 단계에서 OOM(Out of Memory)으로 배포가 실패하는 경우가 발생했습니다.
원인을 파악해 보니 build 초기 단계에서 Yarn PnP가 의존성 데이터를 메모리에 로드하는 그 짧은 순간에 배포 머신에 할당된 메모리 한도를 초과하고 있었습니다.
초기 대응으로 메모리 한도를 증가시켜 해결했습니다. 하지만 문제는 반복되었고, SSR 서버 배포는 전사 서버 배포 파이프라인을 공통으로 사용하고 있었기에 매번 한도를 늘리는 방법에는 한계가 있었습니다.
보다 근본적인 해결책이 필요했습니다.
대안 검토
메모리 스파이크 문제를 근본적으로 해결하기 위해서는 PnP 방식에서 벗어나야 했습니다. 다행히 사내 인프라가 꾸준하게 개선되어 레지스트리 서버도 안정적인 상태가 되었기 때문에 더 이상 Zero-installs 방식을 고집할 필요가 없었습니다.
새로운 대안을 선정하는 기준은 명확했습니다. PnP 방식이 아니어야 했고, 호이스팅으로 인한 유령 의존성(Phantom Dependency) 문제가 없어야 했습니다. 이 기준에 따라 여전히 유령 의존성 문제에서 자유롭지 못한 npm은 가장 먼저 후보에서 제외되었습니다.
선택지는 두 가지로 좁혀졌습니다. Yarn Berry를 유지하되 설정만 nodeLinker: pnpm으로 변경할 것인지, 아니면 아예 pnpm으로 전환할 것인지의 문제였습니다.
Yarn의 pnpm 모드를 사용하면 단순 설정 변경만으로 기존의 문제를 해결할 수 있기 때문에 전환 비용이 매우 낮다는 장점이 있었습니다. 하지만 수많은 서비스를 안정적으로 배포하고 운영하는 것이 최우선인 카카오페이 환경에서 유지보수 관점의 리스크가 크다고 판단했습니다. Yarn의 정체성이자 생태계가 모두 PnP를 중심으로 이루어지고 있었기 때문에, 비주류인 pnpm 모드는 문제가 발생했을 때 참고할 수 있는 레퍼런스가 부족하고 장기적으로 생태계의 원활한 지원을 기대하기 어려워 보였습니다.
마지막 선택지인 pnpm은 설계부터 PnP가 아니었고, 이미 충분히 성숙한 생태계를 가지고 있었기 때문에 안정성에도 문제가 없었습니다. 다만 의존성 설치를 생략하는 Zero-installs 방식과 달리 pnpm은 매번 의존성을 설치하는 방식이기 때문에 배포 파이프라인 시간이 늘어날 것에 대한 우려가 있었습니다.
따라서 저희는 pnpm을 유력한 후보로 두고 실제 사내 환경에서 우려 사항을 검증하는 단계로 넘어갔습니다.
검증
pnpm이 우리 환경에서 유효한 선택인지 확인하기 위해 두 가지 항목을 중점적으로 검증했습니다.
메모리 스파이크 문제가 해결되는가?
아래 차트에서 볼 수 있듯 메모리 스파이크 문제는 깔끔하게 해결되었습니다. 빌드 단계에서 Yarn은 평균에 비해 최대 메모리 사용량이 압도적으로 높았던 반면, pnpm은 끝까지 일정하게 메모리를 사용했습니다.
왜 이런 차이가 발생하는 걸까요?
Yarn PnP는 빌드 초기 단계에서 의존성 정보에 접근하기 위해 .pnp.cjs 파일을 파싱해서 메모리에 적재합니다.
이때 Next.js 및 여러 번들러들은 병렬적으로 빌드를 처리하기 위해 다수의 Worker를 생성하는데,
Worker마다 각자 .pnp.cjs 파일을 파싱하면서 순간적으로 메모리 사용량이 폭발적으로 늘어납니다.
반면 pnpm은 심링크(Symlink) 기반의 표준 node_modules 구조를 사용하기 때문에 Node.js의 기본 모듈 해석 방식을 그대로 따릅니다.
전체 의존성을 한번에 메모리에 올리는 PnP와 달리, 실제로 import나 require가 호출되는 시점에 해당 모듈의 경로만 해석하므로
다수의 Worker가 동시에 동작하더라도 메모리 사용량이 안정적으로 유지됩니다.
얼마나 느려지는가?
앞서 언급했던 의존성 설치 단계가 추가되면서 발생하는 배포 시간 증가 우려를 검증하기 위한 테스트도 진행했습니다.
놀랍게도 배포 파이프라인에서 pnpm 의존성 설치 시간은 Yarn Berry와 비교해서 유의미한 차이가 없었습니다. 제가 패키지 매니저 벤치마크를 보고 가지고 있던 막연한 생각과는 전혀 달랐습니다. 원인을 파악해 보니 크게 두가지 이유가 있었습니다.
- 사내 배포 환경에서 Yarn Berry Zero-installs는 진정한 의미의 zero install이 아니었습니다.
Zero-installs를 사용하더라도 OS 아키텍처에 종속적인 의존성(swc, sharp 등)은 캐시에 포함되지 않고 .yarn/unplugged라는 디렉터리에 따로 격리됩니다.
개발 PC와 배포 머신의 아키텍처가 다른 사내 환경에서는 Zero-installs를 사용하더라도 해당 네이티브 의존성들을 배포마다 다시 빌드하고 설치하는 단계를 꼭 거쳐야 했습니다.
- pnpm의 순수 의존성 설치 속도가 빨랐습니다.
매번 캐시가 초기화되는 CI 환경에서조차도, pnpm의 글로벌 스토어와 하드 링크를 사용하는 방식 덕분에 의존성 설치가 매우 빠르게 완료되었습니다.
결과적으로 사내 환경에서 사용하는 Yarn Berry는 완전한 Zero-installs이 아니었고 pnpm의 설치 속도가 충분히 빠르다 보니, 배포 시 두 패키지 매니저 간의 의존성 설치 시간 차이는 사실상 없었습니다.
예상치 못한 수확
검증 과정에서 예상하지 못했던 큰 수확도 있었습니다. 바로 최종 도커 이미지의 크기가 대폭 줄어든 것입니다.
Next.js는 Standalone 빌드 시 의존성 tracing을 통해 런타임에 꼭 필요한 의존성만 포함하여 경량화된 node_modules를 생성합니다.
하지만 Yarn PnP는 ZipFS와 가상 파일 시스템 경로를 사용하기 때문에 추가적인 설정 없이는 Next.js의 의존성 tracing 기능을 온전히 활용할 수 없었고, 전체 의존성이 포함된 .yarn/cache 디렉터리를 최종 이미지에 포함해야 했습니다.
반면 pnpm은 표준 node_modules를 따르기 때문에 Next.js가 생성한 경량화 디렉터리를 그대로 사용할 수 있었습니다.
덕분에 별다른 노력 없이 도커 이미지 크기를 크게 줄일 수 있었고, 도커 이미지를 push/pull 하는 시간도 함께 줄어들며 전체적인 배포 파이프라인 시간이 오히려 단축되는 결과로 이어졌습니다.
전환
검증 단계에서 메모리 스파이크 문제가 해결되는 것과 더불어 전체적인 배포 시간까지 개선되는 것을 확인하고, 본격적인 전사 전환을 준비하기 시작했습니다.
매일 사용하는 익숙한 툴을 변경하는 것은 개발자들에게 적지 않은 피로감을 줄 수 있습니다. 따라서 pnpm으로의 전환이 귀찮은 ‘숙제’가 아니라 ‘기대되는 변화’로 느껴지길 바랐습니다. 개발자를 설득하는 가장 효과적인 방법은 역시 지표가 어떻게 개선되는지를 객관적으로 보여주는 것이라고 판단하여, 전환 전후 성능 수치를 시각화하는 대시보드를 구축하기로 결정했습니다.
배포 파이프라인에서 의존성 설치 및 빌드 소요 시간, CPU 및 메모리 사용량, 최종 도커 이미지 크기 등을 수집하여 서버에 저장하는 로직을 추가했습니다. 도커 이미지가 빌드되는 환경 자체가 도커인 DinD(Docker-in-Docker) 구조였기 때문에, 정확하게 지표를 측정하기 위해 현재 컨테이너의 cgroup 경로를 동적으로 파악하고 값을 참조하도록 구현했습니다.
결과
대시보드를 통해 전환 이후 데이터를 확인해 보니 검증 단계에서 예상했던 것과 유사하게 긍정적인 지표 개선이 있었습니다.
- 도커 이미지 크기 약 83% 감소
- 최대 메모리 사용량 약 64% 감소
- 전체적인 배포 시간 감소
수치적인 성과뿐만 아니라 개발자 경험 측면에서 체감되는 사용성 개선도 예상보다 컸습니다.
많은 개발자들이 표준 node_modules 구조에서 오는 편안함, 로컬과 배포 환경에서의 속도 개선, 그리고 전반적인 쾌적함을 느낀다고 공감해 주셨습니다.
성공적인 초기 전환 데이터와 긍정적인 피드백을 바탕으로 점진적으로 전환을 이어나갔고, 현재는 전사 대부분의 레포지토리에서 pnpm을 사용하게 되었습니다.
마치며
전환을 마치고 과정을 돌아보니, 과거에 불안정한 레지스트리 문제를 해결하기 위해 도입했던 Yarn Berry가 이제는 우리의 발목을 잡는 기술이 되었다는 게 새삼 놀라웠습니다. 최고의 기술이란 고정된 것이 아니라, 우리가 직면한 현재 상황의 문제를 가장 효과적으로 해결해 주는 도구라는 것을 다시금 깨달았습니다.
오늘의 문제를 해결하기 위해 적용한 pnpm도 언젠가 새로운 병목이 될지도 모릅니다. 하지만 우리는 이번 경험을 바탕으로, 그때도 변화한 환경에 맞춰 우리에게 가장 맞는 답을 찾아낼 것입니다.