본문 바로가기

테스트 플렛폼

Vinci 모노레포: pnpm과 터보레포를 활용한 다중 스택, 다중 버전 개발 지침서

반응형

요약

pnpm과 Turborepo를 활용하여 "Vinci" 시스템의 복잡한 모노레포 아키텍처를 성공적으로 구축하고 운영하기 위한 포괄적인 지침을 제공합니다. 이 아키텍처는 React 17 및 React 19와 같은 여러 프런트엔드 애플리케이션 버전을 단일 저장소 내에 공존시키면서, Spring, MyBatis(iBatis의 후속 버전), 그리고 MariaDB를 사용하는 자바 백엔드와 원활하게 통합하는 것을 목표로 합니다.

pnpm의 고유한 콘텐츠 주소 지정 저장소(content-addressable store)와 심링크(symlinks) 기반의 의존성 관리 방식은, 특히 여러 버전의 React를 관리할 때 발생하는 피어 의존성 충돌 문제에 대한 안정적이고 공간 효율적인 해결책을 제공합니다. 이를 위해 pnpm의 카탈로그(catalogs) 프로토콜을 사용하여 각 프런트엔드 애플리케이션이 필요한 정확한 React 버전을 명시적으로 참조하도록 구성합니다.

한편, Turborepo는 JS와 Java 생태계를 아우르는 통합 빌드 시스템으로 기능합니다. 개별 프로젝트의 네이티브 빌드 도구(예: Maven)를 쉘 커맨드(shell command)로 추상화하고, 이를 병렬 및 증분 빌드 파이프라인으로 통합합니다. 이 접근 방식은 개발 워크플로우를 극적으로 가속화하고, CI/CD(지속적 통합/배포) 환경에서의 불필요한 작업을 제거하여 팀 생산성을 극대화합니다. 이 보고서에 제시된 아키텍처는 복잡한 다중 기술 스택 프로젝트를 위한 확장 가능하고 유지보수 용이한 기반을 제공합니다

 

1. Vinci 모노레포의 아키텍처 기반

이 섹션은 "Vinci" 시스템을 위한 모노레포 아키텍처의 이론적 토대를 구축하고, 핵심 기술 선택의 이유를 설명합니다.

1.1. 엔터프라이즈 시스템을 위한 모노레포 패러다임

Vinci 시스템은 CRM, WMS, 학사, 테스트 통합 등 여러 상호 연결된 애플리케이션으로 구성됩니다. 이러한 프로젝트를 단일 저장소, 즉 모노레포에 통합하는 것은 여러 전략적 이점을 제공합니다. 첫째, 의존성 관리가 대폭 간소화됩니다. 모든 프로젝트가 단일 pnpm-lock.yaml 파일을 공유하므로, 의존성 버전의 일관성을 쉽게 유지할 수 있습니다. 둘째, 코드 공유 및 재사용이 용이해집니다. 공통 UI 컴포넌트 라이브러리나 유틸리티 패키지를 만들고, 모든 애플리케이션에서 workspace: 프로토콜을 사용하여 이를 참조할 수 있습니다. 셋째, 코드베이스 전체에 걸친 원자적(atomic) 변경이 가능해집니다. 예를 들어, 공유 유틸리티 패키지를 업데이트하면서, 그 변경에 영향을 받는 모든 애플리케이션의 코드를 동일한 커밋 내에서 수정할 수 있습니다. 이는 여러 저장소에 분산된 코드베이스에서 발생할 수 있는 버전 불일치 문제를 방지합니다.

1.2. 현대적인 툴체인: pnpm과 터보레포

pnpm은 npm 또는 Yarn과 달리, 모노레포 환경에 최적화된 독특한 의존성 관리 방식을 사용합니다. pnpm은 패키지를 중앙의 콘텐츠 주소 지정 저장소에 단일 복사본으로 저장하고, 각 프로젝트의 node_modules 폴더에는 실제 패키지를 가리키는 심링크를 생성합니다. 이 방식은 의존성 중복을 근본적으로 방지하고 디스크 공간을 절약합니다. 또한,

pnpm의 심링크 기반 아키텍처는 npm/Yarn의 호이스팅(hoisting)으로 인해 발생할 수 있는 "팬텀 의존성(phantom dependencies)" 문제를 해결하는 데 도움이 됩니다. 이는 선언되지 않은 의존성에 접근하는 것을 방지함으로써 프로젝트의 의존성 관계를 보다 명확하고 안전하게 만듭니다.

Turborepo는 pnpm이 제공하는 견고한 의존성 관리 위에 고성능 빌드 오케스트레이션 계층을 추가합니다. Turborepo의 핵심 기능인 증분 빌드, 작업 병렬화, 그리고 원격 캐싱은 대규모 모노레포의 확장성 문제를 해결합니다. 이미 캐시된 작업은 다시 실행되지 않으므로, 개발 워크플로우 및 CI/CD 파이프라인의 빌드 및 테스트 시간을 획기적으로 단축할 수 있습니다. 이는 엔지니어링 시간을 절약하고 컴퓨팅 비용을 절감하는 중요한 이점을 제공합니다.

1.3. 폴리글랏 아키텍처: 자바스크립트와 자바의 연결

JS(Node.js)와 JVM(Java) 생태계는 근본적으로 다른 빌드 시스템과 의존성 관리 방식을 사용합니다. 이러한 두 생태계를 단일 저장소 내에서 원활하게 통합하는 것은 기술적인 도전 과제입니다. 이 문제를 해결하기 위한 전략은 각 프로젝트의 네이티브 빌드 도구(예: JS/TS의 경우 pnpm, Java의 경우 Maven 또는 Gradle)를 존중하되, Turborepo를 모든 작업 실행의 단일 진입점으로 사용하는 것입니다. Turborepo의 가장 중요한 강점 중 하나는 모든 쉘 명령어를 실행하고 캐싱할 수 있다는 점입니다. 이 특성을 활용하여

Turborepo는 JS 애플리케이션의 빌드 스크립트뿐만 아니라 자바 백엔드의 Maven 빌드, 테스트, 배포 등의 모든 태스크를 조율할 수 있습니다. 이를 통해 개발자는 기술 스택에 관계없이 turbo run <task>와 같은 일관된 명령어로 모든 작업을 수행할 수 있어, 통일된 개발 경험을 제공합니다.


2. 다중 버전 리액트를 위한 실용적 접근법

이 섹션은 사용자의 가장 복잡한 요구사항, 즉 React 17과 React 19를 동시에 관리하는 문제에 대한 구체적인 해결책을 제시합니다.

2.1. 리액트 피어 의존성의 도전

리액트(React)는 애플리케이션 번들 내에서 단일 복사본으로 존재해야 하는 "싱글턴(singleton)" 의존성으로 설계되었습니다. 한 페이지에 여러 버전의 리액트가 로드되면

Context나 useState 같은 훅이 올바르게 작동하지 않아 예측 불가능한 런타임 오류를 유발할 수 있습니다. 피어 의존성은 이러한 호환성 요구사항을 명시적으로 선언하는 용도로 사용되지만 , 복잡한 모노레포에서는 종종 해결하기 어려운 충돌을 야기합니다. 일부 라이브러리가 특정 React 버전에만 의존하는 경우, 다른 버전의 React를 사용하는 애플리케이션에서는 "UNMET PEER DEPENDENCY" 오류가 발생할 수 있습니다.

2.2. pnpm 카탈로그 프로토콜을 이용한 확실한 해결책

pnpm의 카탈로그(Catalogs) 프로토콜은 이러한 다중 버전 의존성 문제를 해결하기 위한 명확하고 안정적인 방법을 제공합니다. pnpm-workspace.yaml 파일에서 특정 버전 범위를 가진 명명된 카탈로그를 정의할 수 있습니다. 예를 들어,

react17 카탈로그는 React 17 버전을, react19 카탈로그는 React 19 버전을 정의합니다. 개별 프로젝트의 package.json 파일은 버전 범위 대신 catalog:<catalog_name> 프로토콜을 사용하여 필요한 카탈로그를 참조합니다.

이 방법은 pnpm의 아키텍처와 시너지 효과를 냅니다. pnpm의 콘텐츠 주소 지정 저장소는 각 React 버전의 단일 물리적 복사본을 유지하지만, 각 프로젝트의 node_modules 심링크는 package.json에 명시된 catalog: 프로토콜에 따라 정확히 필요한 버전의 물리적 복사본을 가리킵니다. 이 접근 방식은 각 애플리케이션이 React의 단일 복사본만 사용하도록 보장하는 동시에, 코드베이스 전체의 가독성과 유지보수성을 극대화합니다.

다음 표는 Vinci 모노레포에서 React 17과 19를 관리하기 위한 카탈로그 구성의 예시입니다.

표 1: 리액트 버전 카탈로그 구성(pnpm-workspace.yaml)

카탈로그 이름 의존성 및 버전 범위 (react, react-dom) 사용하는 애플리케이션
react17 ^17.0.2 CRM, 학사
react19 ^19.0.0 WMS, 테스트통합
default ^18.2.0 공유 패키지 등
Sheets로 내보내기

2.3. 고급 시나리오에서의 오버라이드 역할

pnpm의 overrides 필드는 카탈로그와는 별도로, 특정 의존성 버전을 강제하는 데 사용됩니다. 이는 주로 타사 라이브러리가 아직 React 19에 대한 피어 의존성 업데이트를 제공하지 않아 발생하는 충돌을 해결하기 위한 임시 방편으로 활용됩니다. 예를 들어, 특정 라이브러리가 React 18을 피어 의존성으로 요구하지만, 현재 애플리케이션이 React 19를 사용하는 경우

overrides를 통해 React 19 버전을 사용하도록 강제할 수 있습니다.

그러나 overrides의 남용은 권장되지 않습니다. 이는 근본적인 문제를 가릴 수 있으며, pnpm의 강력한 의존성 관리 체계를 우회하는 행위입니다. 오버라이드는 상위 라이브러리에 풀 리퀘스트(pull request)를 제출하는 동안 임시로 사용하는 것이 바람직하며, 이는 건강한 오픈 소스 생태계에 기여하는 성숙한 개발자의 자세입니다.

2.4. 리액트 17 vs. 19: 변화의 이해

Vinci 시스템에서 여러 React 버전을 사용하는 것은 각 버전이 제공하는 기능적 차이 때문일 수 있습니다. React 17은 새로운 사용자 지향 기능 없이, 내부적인 업그레이드 편의성과 JSX Transform을 개선하는 데 중점을 두었습니다. 반면, React 18은 동시성 렌더링(Concurrent Rendering)과 서스펜스(Suspense),

startTransition과 같은 새로운 API를 도입하며 근본적인 렌더링 방식에 변화를 주었습니다. 최신 버전인 React 19는 새로운 컴파일러, "Actions," 그리고 서버 컴포넌트(Server Components)와 같은 기능을 통해 성능 최적화와 서버와의 상호작용을 혁신하고 있습니다.

이러한 기술적 진화는 프로젝트마다 React 버전 선택의 이유가 다를 수 있음을 시사합니다. 예를 들어, 기존의 안정적인 CRM 앱은 React 17을 유지하고, 새로운 기능 개발에 초점을 맞춘 WMS 앱은 React 19의 최신 기능을 활용할 수 있습니다. 이러한 필요성이야말로 단일 모노레포 내에서 다중 React 버전을 지원해야 하는 아키텍처적 당위성을 제공합니다.


3. 자바 백엔드 통합

이 섹션은 프로젝트의 폴리글랏 특성을 다루고 자바 생태계를 모노레포에 통합하는 방법을 설명합니다.

3.1. 모노레포 내 백엔드 구조화

명확한 책임 분리를 위해 자바 백엔드는 프런트엔드 애플리케이션과 나란히 별도의 디렉터리(예: apps/backend)에 위치하는 것이 좋습니다. 이 구조는 표준 모노레포 관행을 따릅니다. 백엔드 빌드는 Maven과 같은 고유한 빌드 도구를 사용하여 관리되며,

Turborepo는 이 빌드 도구를 호출하는 오케스트레이터 역할을 수행합니다.

3.2. 현대적인 스택: 스프링 + MyBatis + MariaDB

사용자 질의에서 언급된 iBatis는 현재 MyBatis라는 이름으로 계승된 프레임워크입니다. 본 보고서는 현대적인 스택인

MyBatis를 기준으로 설명합니다. 스프링 부트와 MyBatis를 통합하는 가장 쉬운 방법은 mybatis-spring-boot-starter 의존성을 pom.xml에 추가하는 것입니다. 이 스타터는 스프링 부트 애플리케이션 내에서 MyBatis를 자동으로 구성합니다.

데이터베이스 연결 정보(MariaDB)와 MyBatis 매퍼 파일의 위치는 application.properties 파일에 정의합니다.

다음은 백엔드 설정에 필요한 핵심 구성 요소입니다.

표 2: 백엔드 통합 구성

구성 요소 예시 설명
Maven 의존성 <groupId>org.mybatis.spring.boot</groupId><br/><artifactId>mybatis-spring-boot-starter</artifactId> 스프링 부트와 MyBatis를 통합하는 필수 스타터
  <groupId>org.mariadb.jdbc</groupId><br/><artifactId>mariadb-java-client</artifactId> MariaDB 데이터베이스 드라이버
  <groupId>org.springframework.boot</groupId><br/><artifactId>spring-boot-starter-web</artifactId> REST API를 위한 스프링 부트 웹 스타터
application.properties spring.datasource.url=jdbc:mariadb://localhost:3306/vinci_dbspring.datasource.username=rootspring.datasource.password=passwordmybatis.mapper-locations=classpath:mappers/*.xml MariaDB 데이터 소스와 MyBatis XML 매퍼 위치 설정
Sheets로 내보내기

3.3. 애플리케이션 간 통신

프런트엔드와 백엔드 간의 기본 통신 인터페이스는 RESTful API가 될 것입니다. 모노레포 구조는 백엔드 API 계약(contract)을 명확하게 정의하고, 프런트엔드 팀이 이를 쉽게 참조할 수 있도록 공유 타입스크립트 인터페이스나 API 클라이언트 코드를 패키지로 관리할 수 있는 이점을 제공합니다.


4. 터보레포를 이용한 빌드 오케스트레이션

이 섹션은 Turborepo가 어떻게 모든 프로젝트를 하나의 통합된 빌드 파이프라인으로 묶는지를 설명합니다.

4.1. turbo.json 마스터 플랜

turbo.json 파일은 모노레포의 모든 빌드 및 테스트 태스크를 정의하는 중앙 집중식 구성 파일입니다. 이 파일의 pipeline 객체는 각 태스크의 의존성과 캐싱 전략을 선언합니다.

dependsOn 키는 태스크 실행 순서를 보장하는 데 사용됩니다. 예를 들어, web#build가 backend#build에 의존하도록 설정하여, 프런트엔드 빌드 전에 자바 백엔드가 항상 먼저 빌드되도록 할 수 있습니다.

outputs 키는 태스크가 성공적으로 완료되었을 때 캐시할 파일과 디렉터리를 지정합니다. Turborepo는 캐시된 출력을 기반으로 변경되지 않은 태스크를 건너뛰어 빌드 시간을 크게 단축합니다. 이는 JS 프로젝트의

dist/ 폴더뿐만 아니라 자바 프로젝트의 target/ 폴더에도 적용되어, JS와 Java의 빌드 캐싱을 통합합니다.

다음 표는 Vinci 모노레포의 turbo.json 파이프라인 구성의 예시입니다.

표 3: 터보레포 태스크 파이프라인(turbo.json)

태스크 이름 dependsOn outputs 설명
build (프런트엔드) ^build, backend#build dist/** 루트에서 모든 프런트엔드 앱을 빌드합니다. backend 태스크에 의존합니다.
dev (프런트엔드) backend#dev - 개발 환경을 실행하며 백엔드 개발 서버에 의존합니다.
lint - - 모든 JS/TS 프로젝트에 대해 린트를 실행합니다.
build:java - target/** 자바 백엔드를 빌드합니다.
dev:java - - 자바 백엔드 개발 서버를 실행합니다.
test:java build:java - 자바 백엔드 테스트를 실행합니다.
Sheets로 내보내기

4.2. 쉘 커맨드를 통한 생태계 연결

Turborepo는 자체적인 빌드 시스템이 아니라, 태스크를 오케스트레이션하는 도구입니다. Turborepo의 가장 큰 강점은 각 프로젝트의 고유한 빌드 명령어를 추상화 계층으로 취급한다는 점입니다. 예를 들어, turbo build 명령은 JS 프로젝트의 경우 pnpm run build를 호출하고, 자바 프로젝트의 경우 mvn clean install을 호출하는 식입니다.

이 접근 방식은 JS와 Java라는 두 개의 상이한 기술 스택을 단일 turbo.json 파일 아래에 통합할 수 있게 합니다. turbo.json은 전체 모노레포의 빌드 프로세스를 선언하는 단일 진실 공급원(single source of truth)이 됩니다. 이를 통해 개발자는 백엔드 코드를 변경한 후, turbo run build 한 번만 실행하면 모든 종속 프런트엔드 앱이 자동으로 재빌드되고, 변경된 백엔드에 맞게 업데이트됩니다. 이 단순하지만 강력한 추상화는 개발자 경험을 획기적으로 개선합니다.

4.3. 태스크 병렬화 및 캐싱을 통한 성능 극대화

Turborepo는 태스크 간의 의존성 그래프를 분석하여 병렬 실행 가능한 모든 작업을 동시에 실행합니다. 예를 들어,

turbo run build test lint와 같은 명령을 실행하면, build 태스크가 완료된 후 test와 lint 태스크를 동시에 실행하여 시간을 절약합니다. 또한, turbo prune 기능은 배포를 위해 필요한 패키지만 포함하는 경량화된 Docker 이미지를 생성하는 데 유용합니다.


5. Vinci 모노레포: 단계별 구현 가이드

이 섹션은 Vinci 모노레포를 구축하기 위한 실질적인 단계별 지침과 예시 코드를 제공합니다.

5.1. 프로젝트 초기화

먼저 프로젝트 루트에 pnpm과 Turborepo를 설치하고 모노레포를 초기화합니다.

Bash
 
# 빈 디렉터리 생성 및 진입
mkdir vinci-monorepo
cd vinci-monorepo

# pnpm 초기화 및 workspace 설정
pnpm init
touch pnpm-workspace.yaml
touch turbo.json

# pnpm과 turborepo 설치
pnpm add -D turbo

pnpm-workspace.yaml 파일에 워크스페이스(workspace)를 정의합니다.

YAML
 
packages:
  - 'apps/*'
  - 'packages/*'

5.2. 다중 리액트 환경 설정

pnpm-workspace.yaml 파일에 React 17과 React 19를 위한 카탈로그를 정의합니다.

YAML
 
catalogs:
  react17:
    react: ^17.0.2
    react-dom: ^17.0.2
  react19:
    react: ^19.0.0
    react-dom: ^19.0.0

각 프런트엔드 앱의 package.json에서 catalog: 프로토콜을 사용하여 React 버전을 지정합니다.

JSON
 
// apps/crm-app/package.json
{
  "name": "crm-app",
  "dependencies": {
    "react": "catalog:react17",
    "react-dom": "catalog:react17"
  }
}

// apps/wms-app/package.json
{
  "name": "wms-app",
  "dependencies": {
    "react": "catalog:react19",
    "react-dom": "catalog:react19"
  }
}

5.3. 자바 백엔드 구축

apps/backend 디렉터리를 생성하고 Maven 프로젝트를 초기화합니다. pom.xml 파일에 필요한 의존성을 추가합니다.

XML
 
<project>
 ...
  <dependencies>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>
    <dependency>
      <groupId>org.mariadb.jdbc</groupId>
      <artifactId>mariadb-java-client</artifactId>
      <version>3.3.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
   ...
  </dependencies>
 ...
</project>

src/main/resources/application.properties 파일에 데이터베이스 연결 정보를 설정합니다.

Properties
 
# apps/backend/src/main/resources/application.properties
spring.datasource.url=jdbc:mariadb://localhost:3306/vinci_db
spring.datasource.username=root
spring.datasource.password=mypassword
mybatis.mapper-locations=classpath:mappers/*.xml

5.4. 터보레포 파이프라인 구성

루트 디렉터리의 turbo.json 파일을 사용하여 모든 프로젝트의 태스크 파이프라인을 정의합니다. 다음은 자바 백엔드와 프런트엔드 앱 간의 의존성을 포함한 전체 구성의 예시입니다.

표 4: 모노레포 폴더 구조

/vinci-monorepo
├── apps/
│   ├── crm-app/             # React 17 애플리케이션
│   ├── wms-app/             # React 19 애플리케이션
│   └── backend/             # Spring Boot, MyBatis, MariaDB 백엔드
├── packages/
│   └── ui-kit/              # 공유 React 컴포넌트 라이브러리
├── pnpm-workspace.yaml
├── package.json
└── turbo.json
JSON
 
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": [
        "^build",
        "backend#build:java"
      ],
      "outputs": [
        "dist/**"
      ]
    },
    "dev": {
      "persistent": true,
      "cache": false,
      "with": [
        "backend#dev:java"
      ]
    },
    "lint": {},
    "test": {
      "dependsOn": [
        "build"
      ]
    },
    "build:java": {
      "cache": true,
      "outputs": [
        "target/**"
      ]
    },
    "dev:java": {
      "persistent": true,
      "cache": false
    }
  }
}

5.5. 샘플 소스 코드

백엔드에서 간단한 MyBatis 매퍼 인터페이스와 스프링 컨트롤러를 구현하여 프런트엔드와의 통신을 시연할 수 있습니다. 예를 들어, apps/backend/src/main/java/.../UserMapper.java 인터페이스에 @Mapper와 @Select 어노테이션을 사용하여 데이터베이스 쿼리를 정의하고 ,

UserController.java에서 이 매퍼를 호출하는 REST 엔드포인트를 노출할 수 있습니다.


6. 고급 고려사항 및 모범 사례

6.1. 공유 라이브러리 및 유틸리티 관리

모노레포의 핵심 이점은 코드 재사용입니다. packages/ 디렉터리에 공유 컴포넌트나 유틸리티를 위한 패키지(예: ui-kit 또는 configs)를 생성하는 것이 모범 사례입니다. 이러한 내부 패키지는 workspace: 프로토콜을 사용하여 다른 프로젝트의 package.json에서 직접 참조할 수 있습니다. 이 방식은 의존성을 중앙 집중화하고 불필요한 코드 중복을 방지합니다.

6.2. CI/CD 통합

Turborepo는 CI/CD 파이프라인에서 엄청난 효율성을 제공합니다. turbo run build --filter=와 같은 명령어를 사용하면, HEAD 커밋과 그 이전 커밋(HEAD^1) 간에 변경된 패키지와 그 패키지에 종속된 프로젝트만 빌드하고 테스트할 수 있습니다. 이를 통해 CI 런타임을 획기적으로 줄이고, 클라우드 빌드 비용을 절감할 수 있습니다.

6.3. 일반적인 모노레포 문제 해결

pnpm은 의존성 설치 시 순환 의존성(cyclic dependencies)을 감지하고 경고를 출력하여 잠재적인 문제를 미리 알려줍니다. 문제가 발생하면

pnpm ls와 같은 명령어를 사용하여 의존성 트리를 시각적으로 확인하고, overrides 또는 packageExtensions를 통해 해결할 수 있습니다.

pnpm의 엄격한 의존성 관리 방식은 개발자가 명시적으로 문제를 해결하도록 유도하여 코드베이스의 안정성을 높입니다.

 

반응형