const-tommy.dev
기록을 불러오는 중입니다
pnpm과 Turborepo를 기반으로 muroom 프로젝트의 뼈대를 잡으며 했던 고민들을 복기해본다. 이번 설계의 핵심은 코드 중복을 최소화하고, 1인 개발 환경에서 배포와 관리의 공수를 어떻게 줄일 것인가에 있었다.

muroom은 서비스를 이용하는 '뮤지션'과 공간을 관리하는 '사장님'이라는 두 명확한 타겟이 존재한다.
apps/musician: 일반 사용자용 탐색 및 예약 인터페이스apps/manager: 사장님용 대시보드 및 예약 관리 시스템처음부터 이 두 환경을 분리한 이유는 각 서비스가 요구하는 비즈니스 로직의 성격(App의 느낌) 이 완전히 달랐기 때문이다. 하지만 디자인 시스템의 근간이 되는 UI 구성 요소나 공통 설정(ESLint, Tailwind, TypeScript)은 대부분의 코어 로직을 공유하고 있었다. 만약 별도의 레포지토리로 관리했다면, 공통 UI 하나를 고칠 때마다 양쪽 레포를 번갈아 가며 수정하는 지옥을 맛봤을 것이다.
이번 모노레포 구축에서 가장 신경 쓴 부분은 패키지 간의 참조 방식이다. 보통 모노레포에서 공통 패키지를 가져올 때 빌드된 결과물(dist)을 참조하곤 하지만, 나는 이를 Internal Packages(코드 레벨 직접 참조) 방식으로 구성했다.
ui, util)를 수정할 때마다 별도의 빌드 과정을 거쳐 dist 폴더를 생성할 필요가 없다. pnpm workspace를 통해 관련 패키지를 install하면, 코드 레벨에서 즉시 변경 사항이 반영되도록 설계했다.막연히 "좋다"고 알려진 pnpm을 선택한 데에는 사실 모노레포 환경에서 아주 중요한 기술적 이유가 있었다.
npm이나 yarn(v1)은 호이스팅(Hoisting)을 통해 설치하지 않은 패키지도 참조할 수 있는 '유령 의존성' 문제가 발생하기 쉽다. 하지만 pnpm은 심볼릭 링크(Symlink) 방식을 사용하여 package.json에 명시된 의존성만 엄격하게 참조할 수 있게 강제한다.
실제 muroom 프로젝트의 루트 및 개별 앱(apps/musician) 내부의 node_modules 구조를 확인해 보면, 패키지 이름 옆에 화살표(->)가 붙어 있는 것을 볼 수 있다.
# apps/musician/node_modules 내부 확인 예시
lrwxr-xr-x 1 shintaeil staff 87 Dec 28 17:27 next -> ../../../node_modules/.pnpm/next@15.5.7...
lrwxr-xr-x 1 shintaeil staff 81 Dec 28 17:27 nuqs -> ../../../node_modules/.pnpm/nuqs@2.7.3...의존성 격리: apps/musician에는 오직 이 앱이 허락한 도구들만 존재한다. 루트에 turbo가 설치되어 있어도 여기서는 보이지 않는다. 덕분에 설치하지 않은 패키지를 몰래 가져다 쓰는 사고를 원천 차단한다.
지하 창고(.pnpm) 와 이정표: 모든 패키지의 실체는 루트의 .pnpm 가상 저장소에 단 하나씩만 저장된다. 개별 앱 폴더에는 "진짜는 저기 있으니 그리로 가라"는 심볼릭 링크(이정표) 만 생성된다.
저장소 효율: 여러 앱이 같은 라이브러리를 써도 하드디스크 용량은 딱 1개분만 차지하며, 설치 시 파일을 복사하는 대신 링크만 연결하기 때문에 설치 속도가 압도적으로 빠르다.
결국 pnpm 도입은 단순한 유행이 아니라, 모노레포 내의 여러 앱이 서로의 의존성을 침범하지 않게 관리하고 개발 환경의 쾌적함을 유지하기 위한 '엔지니어링적 안전장치' 였다.
환경 불일치로 인해 발생하는 예외 상황을 방지하고, 어떤 환경에서도 동일한 런타임이 보장되도록 표준화 장치를 마련했다.
package.json에 packageManager 필드를 명시하고 SHA-512 해시값을 포함하여, 패키지 관리자의 버전과 무결성을 강제했다. 이를 통해 협업 시 서로 다른 라이브러리 버전을 설치하여 발생하는 의존성 파편화를 원천 차단했다.
"packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81".nvmrc를 통한 런타임 통일: 프로젝트에 필요한 Node.js 버전을 명시하여 개발 환경 간의 런타임 불일치를 제거했다.CI 파이프라인의 부하를 줄이고 코드의 품질을 유지하기 위해 Husky를 활용한 단계별 가드(Guard)를 구축했다.
pre-commit 훅: 커밋 시점에 lint-staged를 실행하여 변경된 파일에 대해서만 린트와 포맷팅을 강제한다.pnpm build 검증: 최종적으로 로컬에서 빌드 성공 여부를 확인한 뒤에만 원격 저장소에 푸시할 수 있도록 워크플로우를 설계했다. 이는 CI 과정에서 발견될 수 있는 사소한 에러를 로컬 단계에서 미리 차단하여 전체적인 개발 사이클의 안정성을 높여준다.결국 pnpm 도입과 환경 표준화는 단순히 유행을 따르는 것이 아니라, 모노레포 내의 여러 앱이 서로의 영역을 침범하지 않게 관리하고 지속 가능한 개발 환경을 유지하기 위한 구조적 선택이었다.
sync-main.sh현재 muroom 프로젝트는 개인 레포지토리가 아닌 오가니제이션(Organization) 레포지토리를 원본(Upstream)으로 관리하고 있다. 이는 당장의 1인 개발을 넘어, 추후 팀 단위 협업으로 전환될 때 발생할 수 있는 코드 충돌을 예방하고 원본 저장소의 안정성을 확보하기 위한 Forking Workflow를 전제로 한 설계다.
포크(Fork) 기반의 개발 환경에서는 원본 저장소(Upstream)와 나의 작업 저장소(Origin)의 main 브랜치를 주기적으로 일치시키는 과정이 필수적석이다. 이를 수동으로 처리할 경우 발생하는 반복적인 오버헤드를 줄이고, 싱크가 어긋난 상태에서 작업을 시작하는 휴먼 에러를 방지하기 위해 자동화 스크립트를 도입했다.
#!/bin/bash
# Upstream(Org)의 최신 이력을 가져와 Origin(Fork)의 main을 동기화하는 자동화 스크립트
# 1. 원본 저장소의 최신 변경 사항 확보
git fetch upstream
# 2. 로컬 메인 브랜치로 전환
git checkout main
# 3. 원본 최신 이력을 로컬 메인에 병합 (Fast-forward)
git merge upstream/main
# 4. 동기화된 로컬 메인을 나의 원격 저장소(Origin)로 업데이트
git push origin main
echo "✅ Upstream sync completed!"