# 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`](http://tsconfig.app)`.json` 및 `tsconfig.node.json`에서 이를 확장(extends)하는 방식을 사용했습니다. 이로써 모든 프로젝트가 일관된 타입 검사 규칙을 따르게 되었습니다.
    
    ```mermaid
    graph TD
        A[tsconfig.base.json] --> B[tsconfig.app.json]
        A --> C[tsconfig.node.json]
        B --> D[React App 1]
        B --> E[React App 2]
        C --> F[Build Scripts]
        C --> G[Node.js Utilities]
    ```
    
* **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` 영역으로 완전히 이전하는 작업
    

이번 프로젝트에서 모노레포를 적용한 건 서비스 확장 전략에 있어 핵심적인 기술 선택이 됐고 많은 유익이 있었습니다.  
하지만 이 글에서 언급됐던것처럼 다양한 설정들을 조정해야 될 수 있으니 트레이드 오프를 고려하여 적용되야 할 기술이라고 생각합니다.
