npm workspaces 모노레포 적용 회고
생각 못한 부분에서 괴로웠고, 즐거웠던 여정

안녕하세요. 이번 글에서는 지난 4월 모노레포(Monorepo)를 도입하게 된 실제적인 배경과, npm workspaces를 활용하여 점진적으로 적용하는 과정에서 겪었던 시행착오, 그리고 그 해결 과정에 대한 회고를 공유합니다. 이 여정이 모노레포 도입을 고려하는 다른 분들에게 유의미한 경험담이 되기를 바랍니다.
모노레포 도입, 피할 수 없는 선택
제가 일하고 있는 프로젝트는 새로 도입하게 될 서비스를 설계하며 난관에 봉착했습니다. 서로 다른 서비스지만 동일한 UI 컴포넌트를 중복 사용해야만 하는 서비스를 만들게 된 것입니다. 예를 들어, 핵심 기능은 같지만 헤더 디자인만 다른 페이지를 여러 서비스에 걸쳐 구현해야 하는 경우가 많았습니다. 여러가지 문제가 발생할 수 있겠지만 그 중 컴포넌트의 비효율적인 관리와 개발 공수 증가가 예상됐습니다.
단순히 UI 컴포넌트뿐만이 아니었습니다. 데이터 통신을 담당하는 서비스 로직, 데이터 쿼리, 그리고 타입 정의까지도 여러 서비스에서 동일하게 사용될 것이 예상됐습니다. 개별 레포지토리에서 이러한 코드들을 복사-붙여넣기 하거나, 별도의 패키지로 관리하는 것은 매번 동기화 문제와 의존성 관리의 복잡성 등의 부작용을 야기할 수 있습니다.
또한, 현재 회사 인프라 구조상 서비스들의 배포 파이프라인이 동일한 구조를 가지고 있었기에, 개별 레포지토리에서 관리하는 것은 비효율적이었습니다. 이러한 문제들을 해결하고, 개발 생산성과 코드 재사용성을 극대화하기 위한 방안으로 모노레포 도입은 피할 수 없는 선택이었습니다.
npm workspaces: 익숙함 속의 점진적 변화
모노레포 도입을 결정한 후, 저희는 기존 패키지 매니저인 npm을 유지하면서 모노레포의 이점을 얻을 수 있는 npm workspaces를 선택했습니다. pnpm이나 Yarn Berry와 같은 새로운 패키지 도구로의 전환이 가져올 학습 곡선과 잠재적 리스크를 최소화하고, 익숙한 환경에서 점진적으로 모노레포를 구축하고자 했습니다.
도입은 다음과 같은 점진적인 단계로 진행되었습니다.
1. 초기 구조 설계 및 패키지 분리 테스트
가장 먼저, 모노레포의 핵심인 패키지 분리를 진행했습니다. 저희는 두 개의 주요 리액트 애플리케이션(app)과 이들이 공유할 코드를 담을 shared 패키지로 나누었습니다.
애플리케이션 (
react-app-1,react-app-2): 각 서비스의 고유한 로직과 UI를 담당합니다.공통 패키지 (
shared): 여러 서비스에서 재사용될 UI 컴포넌트, 유틸리티 함수, 타입 정의 등을 포함합니다.
이 과정에서 node_modules의 동작 방식에 대한 이해가 중요했습니다. 각 하위 패키지의 package.json에서 type: module을 명시하거나, 서로 다른 버전의 의존성을 선언할 경우 개별적인 node_modules가 생성될 수 있음을 확인했습니다. 이렇게 생성되는 게 npm workspace의 기본 동작 방식입니다.
2. 설정 공유를 통한 개발 환경 통합
개발 환경의 통일성은 모노레포의 큰 장점 중 하나입니다. 저는 타입스크립트, ESLint, Tailwind CSS와 같은 주요 설정들을 shared 패키지를 통해 공통으로 관리하고자 했습니다.
타입스크립트 (
tsconfig): 초기에는references방식을 고려했지만, 이는 주로 프로젝트 간 참조에 사용되는 옵션임을 파악했습니다. 대신tsconfig.base.json을 만들어 공통 설정을 정의하고, 각 애플리케이션의tsconfig.app.json및tsconfig.node.json에서 이를 확장(extends)하는 방식을 사용했습니다. 이로써 모든 프로젝트가 일관된 타입 검사 규칙을 따르게 되었습니다.ESLint: 모노레포 루트에 단 하나의
.eslintrc.js파일을 배치하고,tsconfig.eslint.json을 활용하여 모든 자식 패키지에 공통적인 린팅 규칙을 적용했습니다. 초기에는 자식 패키지의tsconfig설정과 충돌하여 ESLint가 제대로 동작하지 않는 문제가 있었지만, 루트 설정에서 올바르게override함으로써 해결했습니다.Tailwind CSS:
tailwind.config.js는 각 패키지마다 필요했지만,tailwind.config.css는 하나만으로도 충분했습니다.tailwind.config.ts의content경로 설정 오류로 스타일이 적용되지 않는 문제가 있었고,shared패키지의 컴포넌트를 올바르게 참조하도록 경로를 수정하여 해결했습니다.스토리북: 공통 컴포넌트를 위한 스토리북을
shared패키지에 통합하려 했으나, 스타일 독립성과 개발 서버의 캐싱 문제로 인해 통합 구현은 어려웠습니다. 결국shared패키지 자체와 각 애플리케이션(learner등) 하위에.storybook설정을 분리하여 관리했습니다. 이로 인해node_modules/.cache디렉토리가 각 프로젝트에 생성되는 부작용(side effect)이 있었습니다.
3. 점진적 코드 이전과 난관들
설정 통합을 마친 후, 본격적으로 기존 코드들을 shared 패키지로 이전하는 작업을 시작했습니다. 이 과정은 예상보다 훨씬 험난했습니다.
타입 시스템과의 씨름: 타입 선언, 추론 방식이 변경되면서 스토리북 및
useQuery반환 값의 타입이any로 추론되는 문제가 발생했습니다. 이는 두 개의tsconfig에서paths설정이 충돌한 것이 주요 원인이었습니다.references설정을 통해 해결을 시도했으나, 파일 경로 인식 문제와paths옵션 조정 과정에서 또 다른 문제가 발생하는 등 많은 시행착오를 겪었습니다.import경로 문제:package.json설정을 통해서 파일 확장자를 명시하는import가 동작하지 않아 기존 코드들을 상당 부분 수정해야 했습니다.(많은 코드들을 배럴파일 화 하는 등 추가 작업이 필요했습니다)shared패키지의 빌드 의존성:shared패키지의 코드가 다른 애플리케이션에서 사용되기 전에 먼저 빌드되어야 한다는 것을 깨달았습니다.package.json의scripts설정을 통해shared패키지의 선 빌드를 자동화하여 해결했습니다.스타일 깨짐 현상:
shared패키지로 이전된 컴포넌트의 스타일이 적용되지 않는 문제가 발생했습니다. 이는tailwind.config.ts의content경로가shared패키지를 올바르게 참조하도록 설정되지 않았기 때문이었습니다.
4. 실제 프로젝트 적용과 Git 관리
모노레포의 실제 프로젝트 적용은 기존 Git 레포지토리의 구조를 변경하는 작업이 포함되었습니다. 기존 운영 PR들을 머지하고, 새로운 모노레포 폴더 구조를 생성한 뒤 코드를 이전했습니다. 이후 모노레포 설정 적용, shared로 코드 이전 및 import 수정, 불필요한 package.json 정리, 그리고 설정을 위한 패키지 분리까지 완료했습니다.
이 과정에서 git-filter-repo와 같은 도구를 사용해야 하는지에 대한 고민이 있었습니다. git-filter-repo는 주로 서로 다른 Git 히스토리를 가진 독립적인 저장소들을 하나의 모노레포로 통합할 때 필요합니다. 하지만 저희 프로젝트의 경우, 기존 프로젝트를 모노레포 구조로 재구성하는 형태였기에 별도의 Git 히스토리 병합이 필요하지 않았고, 결국 이 도구를 사용하지 않고도 원활하게 모노레포를 구축할 수 있었습니다.
마지막으로, 통합된 CI/CD 파이프라인을 구축하여 하나의 파이프라인 파일로 여러 서비스의 빌드 및 배포를 관리할 수 있게 되었습니다. 이는 배포 프로세스의 효율성을 크게 향상시켰습니다.
회고: 배운 점과 5개월 후의 개선점들
모노레포 전환은 기술적인 깊이와 넓은 시야를 요구하는 복잡한 작업이었습니다. npm workspaces의 특성을 이해하고, TypeScript, ESLint, Tailwind CSS 등 다양한 도구의 설정을 통합하는 과정에서 많은 시간을 들였지만, 그만큼 팀원들의 문제 해결 능력과 기술 역량을 크게 향상시키는 계기가 되었습니다.
이번 여정을 통해 얻은 가장 큰 교훈은 점진적인 접근과 충분한 테스트, 그리고 명확한 의존성 관리의 중요성이었습니다. 초기에는 예상치 못한 문제들이 속출했지만, 매일매일 해결책을 찾아나가면서 목표를 달성할 수 있었습니다. 특히, 어떤 도구가 어떤 상황에 필요한지에 대한 정확한 이해(예: git-filter-repo 불필요)는 불필요한 공수를 줄이는 데 중요함을 깨달았습니다.
모노레포 도입 후 5개월여가 지났습니다. 큰 구조적 변화는 없었지만 몇 가지 작업이 더 진행 됐습니다.
build설정을 각 프로젝트에 보다 체계적으로 배치하는 작업shared컴포넌트들의 폴더 트리를 더욱 효율적으로 구성하는 작업firebase,react-gtm-module,tanstack/react-query등 주요 라이브러리들을shared영역으로 완전히 이전하는 작업
이번 프로젝트에서 모노레포를 적용한 건 서비스 확장 전략에 있어 핵심적인 기술 선택이 됐고 많은 유익이 있었습니다.
하지만 이 글에서 언급됐던것처럼 다양한 설정들을 조정해야 될 수 있으니 트레이드 오프를 고려하여 적용되야 할 기술이라고 생각합니다.


