4 분 소요

꿈: “JavaScript 하나로 세 마리 토끼를 다 잡을 수 있지 않을까?”

[ AI_Todo 프로젝트 개발기 - DevOps #1 ] JavaScript로 세 마리 토끼 잡는 방법

React 로 웹 개발을 시작했을 때, React NativeElectron 의 존재는 엄청난 가능성으로 다가왔습니다.
“어차피 다 JavaScript 인데, 공통 로직 (core) 만 잘 분리하고 각 플랫폼의 UI 렌더링 방식만 처리해주면, 코드 하나로 웹, 모바일 앱, 데스크탑 앱을 모두 쉽게 만들 수 있는 거 아닐까? tailwindCSS ? 오잉? 이것도 그냥 모든 플랫폼에서 다 같이 쓸 수 있는거 아니야? 시간 벌었죠? 개이득이죠?”

그 생각은 지극히 합리적이었지만, 동시에 너무나 순진했습니다. 아이디어를 현실로 옮기자마자, 거대한 복잡성의 벽에 부딪혔습니다.

현실: 모노레포의 세 가지 난관

프로젝트를 모노레포로 구성하자 문제는 눈덩이처럼 불어났습니다.

1. 의존성의 지옥

apps/web, apps/desktop, packages/core, packages/ui… 패키지가 늘어날수록 node_modules는 비대해졌습니다.

더 큰 문제는 선별적 의존성이었습니다.
예를 들어, tailwindcsswebdesktop 에선 필요하지만, React Native 기반의 mobile 에선 styled-components를 써야 했습니다.

이 미묘한 차이가 패키지 관리와 빌드 구성을 지옥으로 만들었습니다.

2. 끝나지 않는 빌드

단순히 npm run build를 실행하면, 변경되지 않은 core 패키지까지 매번 새로 빌드를 해야만 했습니다.
웹과 core 패키지만 해도 이렇게 귀찮은데 모바일과 PC 개발에 대한 미래를 생각한다면 코드 한 줄 고치고 전체 빌드를 기다리는 시간은 생산성을 갉아먹는 주범이될 거라고 생각했습니다

3. 불투명한 의존성 그래프

webui에 의존하고, uicore에 의존합니다. 이 의존성 순서를 제가 직접 기억하고 순서대로 빌드해야 했습니다.
“분명히 이걸 자동화해주는 똑똑한 방법이 있을 텐데…” 라는 생각이 들었고 좀 자동으로 알잘딱으로 개발할 수 없을까 생각했습니다….

탐험: 수많은 도구들 속에서 최적의 조합을 찾아서

이 문제를 해결하기 위해, 최신 프론트엔드 툴체인을 깊게 파고들기 시작했습니다.
크게 패키지 매니저빌드 오케스트레이터(Orchestrator), 두 축으로 나눠 리서치를 진행했습니다.

1: 패키지 매니저 - 누가 디스크 공간과 시간을 아껴주는가?

항목 npm v10 yarn berry (v3+) pnpm v10
스토리지 구조 node_modules에 모두 복사 압축(.zip) 파일로 관리 (PnP) 글로벌 스토어 + 하드링크
설치 속도 Baseline 비슷하거나 약간 빠름 압도적으로 빠름 (최대 2배 이상)
디스크 사용량 1x (매우 큼) 0.8x (약간 작음) 0.2x (혁신적으로 작음)
모노레포 지원 workspaces 지원 강력한 workspaces 강력 + workspace:* 프로토콜

결론은 pnpm이었습니다.

디스크를 절약해주는 하드링크 구조도 매력적이었지만, 결정적인 이유는 예측 가능한 의존성 관리 때문이었습니다.
pnpm유령 의존성(Phantom Dependency) 문제를 원천 차단하여, 각 패키지가 package.json에 명시된 패키지에만 접근할 수 있도록 강제합니다.

이는 모노레포의 안정성을 극적으로 높여주었습니다.

2: 빌드 오케스트레이터 - 누가 ‘똑똑하게’ 빌드하는가?

후보 특징 꺼림직한 이유
Lerna 모노레포의 원조. 버전 관리 및 배포에 강점. 빌드 캐싱 기능 부재. 더 이상 적극적으로 쓰이지 않는 분위기.
Nx 강력한 캐싱과 풍부한 플러그인 생태계. 너무 많은 것을 해주려 함. 러닝커브가 높고 Nx 생태계에 대한 Lock-in 우려.
Turborepo 단순함. pipeline 문법이 직관적. 강력한 캐싱. 버전 관리 및 배포 기능은 직접 구현해야 함. (하지만 내겐 아직 불필요)

선택은 Turborepo였습니다.

Nx의 강력함은 분명 매력적이었지만, 1인 프로젝트인 제게는 과하다고 생각했습니다.
Turborepoturbo.json 파일 하나로 “무엇을, 어떤 순서로, 어떻게 캐시할지” 를 선언적으로 관리할 수 있다는 점이 좋았습니다.

복잡한 설정 없이, “변경된 것만 다시 빌드한다” 는 핵심 가치에 가장 충실한 도구였습니다.

발견: pnpm과 Turborepo, 완벽한 한 쌍

이 둘의 조합은 1 + 1 > 2의 시너지를 냈습니다.

  • pnpm이 하드링크로 node_modules의 구조를 예측 가능하고 효율적으로 만듭니다.
  • Turborepo는 이 구조 위에서 파일 내용과 pnpm-lock.yaml의 해시값을 기반으로 변경 사항을 정확히 감지하고, 단 한 줄의 코드라도 변경되지 않은 패키지의 빌드/테스트 결과는 원격 캐시(Remote Cache)에서 그대로 가져옵니다.

turbo.jsonpipeline 설정은 이 모든 것을 마법처럼 처리해줍니다.

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      //  패키지를 빌드하기 전에, 의존하는(^
      // 모든 패키지들의 'build'를 먼저 실행하라.
      "dependsOn": ["^build"],
      // 빌드 결과물인 dist 폴더를 캐싱 대상으로 지정하라.
      "outputs": ["dist/**"]
    },
    "test": {
      // 테스트는 빌드 후에 실행되어야 한다.
      "dependsOn": ["build"],
      // 테스트 결과는 캐싱하지 않는다.
      "outputs": []
    },
    "lint": {
      // 의존성 없음. 독립적으로 실행.
    }
  }
}

결과: 숫자는 거짓말을 하지 않는다

이론을 현실로 옮겼습니다.
npm에서 pnpm으로 마이그레이션하고, 모든 scriptsturbo run ...으로 교체했습니다.

결과는 놀라웠습니다.

지표 이전 (npm 단독) 현재 (pnpm + Turborepo)
최초 의존성 설치 2분 30초 40초
전체 빌드 (No Cache) 4분 10초 45초
변경된 파일 1개 빌드 (Cache Hit) 4분 10초 < 5초
CI 파이프라인 시간 18분 6분 (Cache Hit 시 3분)
node_modules 디스크 사용량 1.2 GB 260 MB

교훈: 길고 긴 삽질의 끝에서

모노레포는 단순히 코드를 한곳에 모으는 것이 아니었습니다.
복잡성을 ‘관리’하는 기술이었습니다.
이번 여정을 통해 얻은 가장 큰 교훈은 두 가지입니다.

  • 캐시가 왕이다: 빌드는 ‘변경된 것’만 해야 한다.
    • Turborepo는 이 원칙을 현실로 만들어주었고, 제 기다림의 시간을 코딩의 시간으로 바꿔주었습니다.
  • 도구는 짝이 있다
    • pnpm의 예측 가능한 의존성 구조와 Turborepo의 정교한 캐싱 전략은 서로를 위해 태어난 것처럼 완벽하게 맞물렸습니다.

하지만 이 조합만으로 모든 문제가 해결되지는 않았습니다.
코드 품질과 순환 참조 가 남아있었습니다.

백엔드를 개발할 때는 IntelliJ 가 제공하는 강력한 순환 참조 분석 기능에 익숙했지만, VSCode 환경에서는 패키지 간의 의존성이 꼬여도 미리 알기 어려웠습니다.
지금은 혼자 개발하기에 제 머릿속 의존성 그래프에 의존할 수 있지만, 만약 미래에 동료가 합류한다면 어떨까요?
기존에 일하던 사람은 문제가 없지만 문제가 복잡해질수록 새로운 사람이 작업을 하게될 때 실수를 하게 되는 경우가 많습니다. 또한 이런 실수는 그 사람 잘못으로 평가되기 쉽다고 생각합니다.

물론 어느정도 맞는 말이지만 개인적으로 이런 부분을 컴퓨터로 자동화해서 편하게 만드는거야말로 개발자로서의 진정한 특권이라고 생각합니다.
저는 이런 특권을 최대한 힘 안들이고 자동화하면서 어떻게하면 편하게 누릴 수 있을 까를 고민했고,
저는 HuskyESLint 가 있~다 는 거 슬…. 알아버렸습니다

다음 글에서는 이 도구들을 활용해 어떻게 커밋 단계에서 코드 품질을 자동으로 검사하고 잠재적인 오류를 차단하는지, 그 최적화 과정을 공유해 보겠습니다.

댓글남기기