Turborepo로 기존의 프로젝트들 한번에 관리하기
2023.12.02
목차
Turborepo? Monorepo?
Turborepo로 모노레포 개발 경험 향상하기
LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech BlogLINE+ UIT 조직에서 프론트엔드 개발을 하고 있습니다.안녕하세요. 저는 LINE+ UIT 조직에서 프런트엔드 개발을 하고 있는 이상철입니다. 저는
관련 내용은 해당 링크들 참조. 상당히 잘 쓰여져 있다.
사용을 결정한 이유
Monorepo의 목적은 "의존성이 있는 코드들을 효율적으로 관리" 하는 것 이다. remote fetching을 위한 기본 axiosInstance, Logging을 위한 errorReport 함수등, 여러 엡에서 반복해서 사용하는 공통함수들을 지속적으로 개선하면서 사용하기 위해서 monorepo를 사용하기로 결정했다.
또한 node기반 백엔드를 사용하는 풀스택 개발을 하면서, Front-Back 둘다 동일한 타입스크립트를 사용하고 있다. 이때, 사용하는 타입을 공유한다면 매우 편하게 개발할 수 있기 때문에 monorepo의 장점은 더 매력적이였다.
마지막으로 Lambda나 Worker를 주로 사용하는 서버리스 환경에서 작은 단위기능을 개발했을때, 하나의 공통 배포설정을 통해 빠르고 쉽게 배포하기 위함이다.
Turborepo 도입 과정
Turborepo 초기 세팅
Installation | Turborepo
Learn how to get started with Turborepo.
나는 Turborepo의 tailwindcss template로 시작, 아래의 파일들과 같이 작성했다.
my-turborepo-tailwindcss
apps
docs
web
packages
eslint-config-custom
tailwind-config
tsconfig
ui
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
// apps에 있는 각 package.json의 scripts.build를 실행한다.
"dependsOn": ["^build"],
// 그리고 그 빌드 결과를 캐싱한다.
"outputs": [".next/**", "!.next/cache/**"]
},
"build:pages": {
"dependsOn": ["^build:pages"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
<의존성 관리 툴은 pnpm을 기반으로 세팅했다. pnpm이 tureborepo의 추천사항이다.>
turborepo를 다루는 큰 컨셉은 다음과 같다.
- apps: 서비스 로직 구현
- packages: Apps에서 공통적으로 사용하게 될 설정, 컴포넌트, 함수들 작성
- turbo.json: monorepo의 스크립트를 실행하는 파이프라인 구성
turbo.json의 파이프라인 구성이 생소할수있다. 이는 여러 레포지토리들을 한번에 관리하기 위해 만들어진 기능이다. 요약하자면, root workspace의 package.json의 특정 스크립트를 실행할때, 그 스크립트의 키에 설정한 dependsOn
은 각 하위 app들에 있는 package.json에서 선언한 스크립트들의 실행에 의존하고, outputs
는 실행결과를 캐싱하는 역할을 한다.
기존의 NextJS 프로젝트 적용 & ui 컴포넌트 분리
다음은 기존의 NextJS 프로젝트를 본 monorepo로 가져오는 과정이다.
현재 apps/docs, apps/web
이 있는데, docs는 지우고, web 디랙토리의 내용은 비운다음 기존의 NextJS 래포지토리를 git clone
을 통해 가져온다.
my-turborepo-tailwindcss
apps
web
public
src
packages
eslint-config-custom
tailwind-config
tsconfig
ui
src
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
// monorepo에서 배포를 위해 추가된 스크립트. turbo.json의 파이프라인을 통해 실행된다.
"build:pages": "pnpm dlx @cloudflare/next-on-pages"
},
"dependencies": {
// ... 기존의 dependencies
"ui": "workspace:*"
},
"devDependencies": {
// ... 기존의 devDependencies
"eslint-config-custom": "workspace:*",
"tailwind-config": "workspace:*",
"tsconfig": "workspace:*",
}
}
<위와같이 web app에서의 package.json, tsconfig.json 일부를 수정해준다.>
apps/web/package.json
을 통해 알수있듯이, ui관련 컴포넌트들을 분리하여 사용하게끔 했다.
분리한 ui관련 컴포넌트들은 packages/ui
의 구조로 작성하여 사용한다. 이때 유의할점은, 빌드 후 참조하는 대상이 ./dist
아래의 컴파일된 코드들이므로, 이를 참조하면서 사용해야한다.
또한 직접 모듈형으로 ui component들을 작성해야하기에, 통상적으로 바로 front-client단에서 사용하는 컴포넌트 작성보다 좀 더 strict한 형태로 작성해야한다. 이를테면 함수의 내보내는 함수의 형식을 정해주거나(./packages/ui/eslintrc.js
), type
대신 interface
를 사용해야하는등의 제약이 있다. 너무 빡빡하다고 생각된다면, eslint-config-custom에서 이를 느슨하게 조정할 수 있다.
Error: Function component is not a function declaration eslint (eslint - 함수형 컴포넌트 선언지정)
reactjs - Struggling with TypeScript, React, Eslint and simple Arrow Functions components - Stack Overflow
Communities for your favorite technologies. Explore all Collectives Ask questions, find answers and collabora
기존의 AWS Lambda 프로젝트 적용
다음으로 web app이 사용하는 aws lambda 코드들을 옮겨준다.
my-turborepo-tailwindcss
apps
web
public
src
web-lambda
__tests__
src
packages
eslint-config-custom
tailwind-config
tsconfig
ui
src
{
"private": true,
"scripts": {
"build": "FORCE_COLOR=1 turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"clean": "turbo clean",
"test": "FORCE_COLOR=1 turbo run test --concurrency=1",
"format": 'prettier --write "**/*.{ts,tsx,md}"',
"build:pages": "turbo build:pages"
},
"devDependencies": {
// monorepo의 apps에서 jest를 사용하기 위해 workspace단에서 설치한다.
"@types/jest": "^29.5.10",
"@types/node": "^20.10.4",
"eslint": "^8.54.0",
"husky": "^8.0.3",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"tsconfig": "workspace:*",
"turbo": "latest"
},
"packageManager": "[email protected]"
}
<과정은 NextJS 프로젝트 코드를 가져오는 과정과 동일하다.>
Jest Test 설정도 동일하게 가져오면된다. 한가지 참고해야하는 부분은 IDE에서 jest에 대한 타입 추론을 위한 세팅을 root workspace단에서 해야한다는 것 이다.
pnpm add -D @types/node @types/jest --workspace-root
커맨드를 통해 root workspace에 타입모듈을 설치하면 된다.
<app내에서 지엽적으로 설치할경우, Jest를 추론을 못하는 문제가 있다. (2023.12.12)>
Front - CloudFlare Pages에 배포하기
CloudFlare Page에서 github 레포지토리 연동을 통해 간단히 배포 자동화가 가능하다.
CloudFlare의 NextJS app 배포를 위해 @cloudflare/next-on-pages
라이브러리를 사용하는데, 다만 주의할 점은 단일 web app을 클론하는것이 아닌, monorepo 전체를 클론해서 사용하기때문에, CloudFlare에서 배포설정을 조금 수정해야한다.
my-turborepo-tailwindcss
apps
web
public
src
web-lambda
__tests__
src
packages
eslint-config-custom
tailwind-config
tsconfig
ui
src
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
// monorepo에서 배포를 위해 추가된 스크립트. turbo.json의 파이프라인을 통해 실행된다.
"build:pages": "pnpm dlx @cloudflare/next-on-pages"
},
"dependencies": {
// ... 기존의 dependencies
"ui": "workspace:*"
},
"devDependencies": {
// ... 기존의 devDependencies
"eslint-config-custom": "workspace:*",
"tailwind-config": "workspace:*",
"tsconfig": "workspace:*",
}
}
<./apps/web/package.json의 build:pages 스크립트와 ./turbo.json의 build:pages 파이프라인을 확인하자.>
<turborepo 의존성 설치와 turborepo의 파이프라인 트리거 스크립트를 작동시킨다. output path도 참조.>
Back - Github action을 이용한 배포 자동화
github action에서 lambda handler 배포를 위한 스크립트를 다음과 같이 작성했다.
my-turborepo-tailwindcss
.github
workflows
.husky
apps
web
public
src
web-lambda
__tests__
src
packages
eslint-config-custom
tailwind-config
tsconfig
ui
src
name: Deploy to Amazon Lambda
on:
push:
# main 브랜치에 push가 발생할 때만 실행한다.
# 이때, web-lambda 폴더와, 본 스크립트 파일이 변경되었을 때만 실행한다.
branches: ['main']
paths:
- 'apps/web-lambda/**'
- '.github/workflows/aws-lambda-deploy.yml'
env:
AWS_REGION: ap-northeast-2
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
jobs:
deploy:
name: Deploy lambda function
timeout-minutes: 15
runs-on: ubuntu-latest
environment: production
defaults:
run:
working-directory: './apps/web-lambda'
strategy:
matrix:
node-version: [18.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/[email protected]
with:
version: 6.32.2
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: install dependencies
run: pnpm install
- name: handler build & bundle
run: pnpm build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{ env.secrets.DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy
run: pnpm deploy
<./.github/workflows/aws-lambda-deploy.yml은 직접 web-lambda 디랙토리의 스크립트를 사용함에 주목.>
aws lambda 배포는 Serverless프레임워크를 사용해서 배포했다.
husky를 이용한 CI/CD 설정
GitHub - typicode/husky: Git hooks made easy 🐶 woof!
Git hooks made easy 🐶 woof! Contribute to typicode/husky development by creating an account on GitHub.
husky를 사용하여 pre-commit 훅을 작성했다. 설치 및 적용은 cli로 진행하며, 위의 링크의 github 레포지토리에 쉽게 설명이 쓰여있다.
본 기능을 사용하여 커밋 전 test를 진행하여, 원격저장소 push되는 코드의 안정성을 체크하고, 실제 배포하는 환경에서는 빠르게 배포를 할 수 있도록 했다.
적용 이후 장점과 단점
Monorepo 자체로서의 장점: 서비스 하나에 관련되어있던 각각 분산되어 있던 front, back 코드들이 한 곳으로 관리하기 편하게 모아놓을 수 있다는 것이다. 기존에는 서비스에서 기능을 추가하고자 하면 관련 작업을 수행해야할 레포지토리가 물리적으로 분산되어있어 집중적으로 작업하기가 어려웠다. 또한 lambda 특성상 작은 단위의 기능을 개발하게 되는데, 이때마다 배포를 위한 설정을 하지 않고 공유되는 설정을 통해 빠르게 배포할 수 있다는 점이 매우 편했다.
Turborepo로서의 장점: 제일 좋은것은 역시 캐싱기능이다.
< 캐시 miss시 >
< 캐시 hit시 >
위와 같이 동일한 코드상태에 대해서 Test를 진행할경우, 매우 빠르게 테스트를 건너 뛰듯이 진행할 수 있다.
만약 web에 대해서만 작업하고, web-lambda에 대해서는 작업하지 않아서 변하지 않았다고 할때, 커맛하면서 CI/CD과정 중 web-lambda에 대해서 테스트를 또 진행하는 것은 매우 불필요한 행위가 될 것이다. 하지만, turborepo에서는 이러한 불필요한 시간낭비를 캐싱을 통해 줄여줄 수 있다.
1. Monorepo 디자인에 대한 러닝커브가 길다.
=> 생각보다 Monorepo로 옮기면서 apps, packages로 관리한다는 디자인 시스템에 대해 이해하고 적응하는데 시간이 걸렸다. 가장 큰 문제는 초기에 '굳이 이걸 도입하지 않는다고 문제가 생기지 않는다'라는 생각이 지배적이여서 좀 보수적으로 접근한 이유가 크다.
2. Turborepo configration을 위해 작성해야하는 코드양이 꽤 많다.
=> turbo.json의 파이프라인 구성 뿐만 아니라, apps, packages의 하위 항목들에 대해 package.json을 연동성있게 작성해야한다는 점에서 작업량이 매우 많다. 나의 경우에는 작은 개인 프로젝트수준이라서 그나마 작업량이 적었지만, 상용화된 서비스라면 작업량이 상당히 많아서 도입에 상당히 부담이 될 수 있다.
3. 유지보수가 쉬워지면서도 어려워진다.
=> 코드를 한곳에 모아놓으면서, 코드의 재사용성이 엄청나게 높아지고, 다른 의존받는 app에 대한 변화에 기민하게 반응할 수 있다는것은 분명 유지보수의 난이도를 크게 낮추는 요인이다. 하지만, 이를 위해서는 필수적으로 package아래의 코드를 관리하게 되는데, 이는 app에서 서비스로직을 작성하는 것보다 더 까다로운 작업이 될 수 있다. 약간 일장일단이 갈리는 부분인데, 나의 경우에는 모듈화를 염두에 둔 코드를 작성한 경험이 적어, 혹을 하나때고 다시 한개를 받은 기분이다.
4. Front의 배포는 현재 app 한개만 가능하다. (cloudflare)
=> 이건 cloudflare의 한계이긴 하지만, 현재 cloudflare pages에서는 하나의 레포지토리에 대해 하나의 pages 앱만 생성할 수 있다. 이는 web1, web2, web3 등등 여러 프론트앱들을 만들어내도, cloudflare pages에서는 하나의 pages만 자동으로 배포되고, 나머지는 수동으로 배포해야한다는 것이다. 이 문제 때문에 Vercel로 이전을 고려했었고, 시도도 해봤으나, 현재 cloudflare의 CDN을 통해서 배포중인 여러 에셋 세팅까지 vercel로 모두 이동하는것은 상당히 무리가 있다. 때문에 현재까지는 제 2의 web을 배포할때는 그 내부에 다른 하위 git 레포지토리를 생성해서, husky를 통해 pages에 배포하는 방식을 시도해볼까 고려중이다.
정리
monorepo를 도입하면서 동시에 이 글을 작성했는데, 2주가까이 긴 시간이 걸렸다.
물론 한번도 해본적 없는것을 해본다는 점에서 꽤 시간이 걸릴 수 있을것이라 예상은 어느정도 했었지만, NextJS 14버전 이슈와 번들링 설정관련 이슈등 다리를 잡는 생각치 못한부분들이 많았다.
하지만, 이번 Monorepo설정을 통해, 향후 개인 프로젝트 생성시에는 더 기민하게 추가 기능을 만들 수 있을것이라 예상되어 이득인 부분이 크다고 생각한다.
특히 lambda로직의 경우, 이것저것 더 작은단위로 만들어 붙일것들이 있는데, 이것들을 가져와서 테스트 및 배포에 대한 자동화가 단단하게 이루어지고, front와의 더 편리한 연동이 가능할 것을 생각하면 충분히 갈아타면서 이득이 된 부분이 많았다.
근 시일내에 개인적으로 clone해서 작업하기 위한, 아래와 같이 초기세팅이 되어있는 레포지토리를 만들 계획이다
App-Front: Nextjs, Vite-react
App-Back: worker, lambda
packages:config: - ts-config, tailwind-config, eslint-config
packages: ui, utils, types
위의 구성을 기본으로 하는 빈 프로젝트를 만들어두고, 필요한 부분만 살려서 쓰는식으로 clone해서 full-stack 개발용으로 사용할 빈 monorepo를 만들어 두려한다.