본문으로 건너뛰기

오픈소스 ABC User Feedback에 적용한 모노리포 구조 소개

정치영techblog.lycorp.co.jp ↗

개요

  • ABC User Feedback: LINE+ ABC Studio에서 개발한 오픈소스 풀 스택 웹 애플리케이션
  • 온프레미스(on-premise) 설치형 솔루션
  • 제공 기능: 사용자 피드백 수집 API + 피드백 시각화 관리자 웹 화면
  • 전체 스택을 TypeScript 단일 언어로 개발 → 모노리포 구조 채택

모노리포 구조 선택

구조 비교

구조 설명 장점 단점
개별 프로젝트 구조 프론트엔드/백엔드를 각각 독립 패키지로 단순히 한 저장소에 배치 프로젝트 간 명확한 분리 프로젝트 간 코드 공유 어려움
단일 프로젝트 구조 하나의 프로젝트 안에 여러 서브 패키지 구성 (apps/, packages/) 코드 일관성, 재사용성 향상, 관심사 분리, 효율적 빌드/배포 패키지 간 의존성 관리 필요
단일 프로젝트 + tooling 디렉터리 apps, packages, tooling 세 디렉터리로 분리 설정 중앙화, 일관성 극대화 초기 구조 설계 비용

채택된 구조 (ABC User Feedback)

apps
 ㄴ web
 ㄴ api
packages
 ㄴ ufb-shared    // @ufb/shared
 ㄴ ufb-tailwind  // @ufb/tailwind
 ㄴ ufb-ui        // @ufb/ui
tooling
 ㄴ eslint        // @ufb/eslint-config
 ㄴ prettier      // @ufb/prettier-config
 ㄴ typescript    // @ufb/tsconfig

각 디렉터리 상세 소개

tooling 디렉터리 — 공통 설정 패키지

모든 패키지에 공통 적용되는 설정을 한 곳에 모아 유지보수성과 일관성 확보

  • @ufb/eslint-config: 프레임워크별 ESLint 규칙 분리 관리
    • base.js: 공통 ESLint 설정
    • nest.js: NestJS 전용
    • next.js: Next.js 전용
    • react.js: React 전용
  • @ufb/prettier-config: 전체 공통 Prettier 설정
    • 추가 플러그인: @ianvs/prettier-plugin-sort-imports (import 정렬), prettier-plugin-tailwindcss (Tailwind 클래스 정렬)
  • @ufb/tsconfig: 공유 TypeScript 설정 파일

tooling 사용 방법 (각 패키지 package.json)

{
  "prettier": "@ufb/prettier-config",
  "eslintConfig": {
    "extends": [
      "@ufb/eslint-config/base",
      "@ufb/eslint-config/react"
    ],
    "root": true
  }
}
  • 각 패키지에 별도 설정 파일 없이 package.json에서만 설정 가능
  • ESLint/Prettier 플러그인 버전을 한 곳에서만 관리

packages 디렉터리 — 코드 공유 패키지

코드 재사용성 향상 및 관심사 분리를 위한 공유 패키지 모음

  • @ufb/ui: 관리자 웹 UI 컴포넌트 (웹 패키지 전용)
  • @ufb/tailwind: Tailwind CSS 설정 (관리자 웹 전용)
  • @ufb/shared: API + 웹 패키지 공통 코드 공유

모듈 시스템에 따른 코드 공유 방식

JavaScript의 두 가지 모듈 시스템:

  • CJS (CommonJS): NestJS(API)가 사용
  • ESM (ECMAScript Modules): Next.js(웹)가 사용
  • 핵심 문제: 두 모듈 시스템은 서로 호환되지 않음
패키지 사용처 모듈 시스템 빌드 여부 비고
@ufb/ui 웹 전용 ESM 빌드 불필요 동일 모듈 시스템
@ufb/tailwind 웹 전용 ESM 빌드 필요 Turborepo 파이프라인 선행 빌드
@ufb/shared API + 웹 CJS + ESM 빌드 필요 tsup으로 CJS/ESM 각각 빌드

빌드 방식 장단점 비교

  • 빌드 불필요 (동일 모듈 시스템):
    • 장점: 빌드 의존성 불필요, 전체 빌드 시간 단축
    • 단점: 동일 모듈 시스템을 의도적으로 맞춰야 함
  • 빌드 필요 (다른 모듈 시스템):
    • 장점: 모듈 호환성 고민 없이 코드 개발에 집중 가능
    • 단점: 서브 패키지 별도 빌드 시간 추가 소요

apps 디렉터리 — 애플리케이션 패키지

실제로 빌드되어 배포되는 최종 애플리케이션 패키지

  • api: NestJS 기반 API 서버 (CJS 모듈 시스템)
  • web: Next.js 기반 관리자 웹 (ESM 모듈 시스템)
  • 다수의 서브 패키지를 임포트하므로 빌드 의존성 관리빌드 최적화 필수

Turborepo를 이용한 파이프라인 및 빌드 캐시 설정

Turborepo 핵심 특징

  • JavaScript/TypeScript 모노리포를 위한 고성능 빌드 시스템
  • 캐시 활용: 동일 작업 중복 실행 방지
  • 태스크 병렬 실행 지원
  • 파이프라인으로 태스크 간 의존성 정의 및 최적화

turbo.json 파이프라인 설정 예시

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "topo": {
      "dependsOn": ["^topo"]
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "next-env.d.ts", "!.next/cache/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "web#dev": {
      "dependsOn": ["@ufb/tailwind#build", "@ufb/shared#build"]
    },
    "api#dev": {
      "dependsOn": ["@ufb/shared#build"]
    },
    "lint": {
      "dependsOn": ["topo"],
      "outputs": ["node_modules/.cache/.eslintcache"]
    },
    "typecheck": {
      "dependsOn": ["topo"],
      "outputs": ["node_modules/.cache/tsbuildinfo.json"]
    },
    "test": {
      "dependsOn": ["topo", "@ufb/shared#build"]
    },
    "format": {
      "outputs": ["node_modules/.cache/.prettiercache"],
      "outputMode": "new-only"
    },
    "clean": { "cache": false }
  },
  "globalDependencies": ["**/.env"]
}

topo 태스크의 역할 (핵심)

  • 아무 작업도 수행하지 않는 가상 태스크
  • 서브 패키지 간 의존성 토폴로지(topology) 를 생성
  • 문제 상황 예시:
    • web 패키지가 ui 패키지를 임포트
    • 전체 typecheck 실행 후 → ui 패키지 수정 → web만 대상으로 typecheck 재실행
    • web 코드는 변경되지 않았지만, 의존하는 ui가 바뀌었으므로 캐시를 그대로 쓰면 안 됨
  • topo 태스크가 이 의존성 관계를 추적해 올바른 캐시 무효화 보장

캐시 파일 경로 설정

# 루트 package.json scripts
"format": "turbo format --continue -- --cache --cache-location='node_modules/.cache/.prettiercache'",
"lint":   "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache'",
// 각 패키지 tsconfig.json
{ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }

globalDependencies 활용

  • .env 파일을 globalDependencies에 등록
  • .env 파일 변경 시 전체 캐시 무효화 → 환경별(개발/운영) 재캐싱 보장
  • dev, build 태스크에 특히 유용

원격 캐시 (Remote Cache)

필요성

  • 로컬 캐시는 Docker 빌드 또는 CI 환경에서 사용 불가
  • 여러 머신에서 동일 코드를 반복 빌드하는 비효율 발생
  • 분산 계산 캐싱(Distributed Computation Caching) 으로 해결

설정 방법

  • Vercel 사용 시: 별도 설정 없이 배포 시 자동 원격 캐싱
  • Vercel 미사용 시: 오픈소스 turborepo-remote-cache 서버를 직접 운영
// .turbo/config.json
{
  "teamid": "team_myteam",
  "apiurl": "http://localhost:3000"
}
# Dockerfile
ENV TURBO_TOKEN=
COPY turbo.json ./
COPY .turbo/config.json ./.turbo/
RUN --mount=type=bind,source=.git,target=.git \
    pnpm turbo build

핵심 인사이트 요약

  • 디렉터리 3분할 전략: tooling(설정) → packages(공유 코드) → apps(애플리케이션) 순으로 의존 방향이 단방향으로 흐름
  • 모듈 시스템 인식 필수: CJS/ESM 혼용 시 빌드 전략을 패키지별로 명확히 결정해야 함
  • topo 태스크: Turborepo에서 빌드 없이 공유하는 내부 패키지의 의존성 캐시를 올바르게 유지하는 핵심 패턴
  • 캐시 파일 명시: lint/format/typecheck 모두 캐시 파일 경로를 outputs에 등록해야 Turborepo가 캐시를 재사용 가능
  • 원격 캐시: CI/Docker 환경에서 캐시 효율을 얻으려면 원격 캐시 서버 구성 필수