현재 진행하고 있는 프로젝트는 서비스 레포지토리와 디자인 시스템 레포지토리를 따로 구성하여 관리하고 있었습니다. 원래는 서비스 레포지토리에서 함께 관리하다가 프로젝트 코드량이 많아짐에 따라 같이 관리하기에는 너무 거대하기에 UI 디자인 시스템 레포를 멀티레포로 운용하고 있었습니다.
분리해서 사용하다보니 코드량의 문제점은 해결했지만 유지보수 및 관리 측면에서 불편함을 크게 느끼게 되었습니다. 서비스를 개발하다 디자인 시스템을 수정하거나 추가할 일이 있으면 디자인 시스템 레포로 다음과 같은 단계를 거쳐야 했습니다.
코드 수정 -> PR 작성 -> Merge -> npm 배포 -> 서비스에서 업데이트 후 사용
이러다보니 코드량 개선에 비해 유지 보수 및 관리는 오히려 불편함을 겪는 상황이 발생했습니다. 그러다 모노레포
에 대해 알게 되었고 마이그레이션의 과정에서 어려움을 조금 겪겠지만 현재 느끼는 불편함은 확실히 개선할 수 있다고 판단, 리팩토링을 진행하기로 결정했습니다.
해당 과정 및 모노레포 개념, 마이그레이션 과정에서 겪은 트러블 슈팅을 기록하기 위해 글을 작성했습니다.
모노레포
란 많은 프로젝트를 단일 저장소에서 관리하는 방식으로 같은 레포지토리에서 서로 다른 프로젝트들을 관리하는 소프트웨어 개발 전략을 말합니다.
코드 재사용
, 의존성 관리 용이
, 프로젝트 유지 보수 및 관리 용이
등의 장점이 존재합니다.
React의 가장 큰 장점 중 하나가 컴포넌트 단위로 코드를 작성하고 재사용할 수 있다는 부분이 있습니다. 해당 프로젝트 단위가 기업 서비스급 규모는 아니기에 디자인 시스템, 서비스정도만 모노레포로 구성했지만 실제로 대부분의 회사들이 서비스 코드들을 모노레포로 구성해서 여러곳에서 재사용할 수 있게 나눠서 작성되어있다고 합니다.
모노레포를 Yarn workspace로 결정한 이유는 결론부터 말씀드리면 프로젝트 패키지 매니저인
Yarn berry 효율 극대화
, 많은 레퍼런스 및 문서
, 낮은 러닝커브
였습니다.
다른 좋은 도구들도 존재하지만 Yarn workspace 사용시 프로젝트에 많은 수정을 거치지 않아도 충분히 모노레포로 구성할 수 있기 때문입니다.
물론 다른 도구 또한 많기 때문에 종류에 대해 간단히 설명드리겠습니다.
yarn
은 패키지 매니저중 하나로 npm
과 더불어 가장 많이 사용되는 패키지 매니저입니다. Yarn berry에서는 workspace로 어렵지 않게 모노레포를 구성할 수 있습니다.
또한 Yarn berry의 장점인 zero-install과 pnp는 프로젝트를 제작하면서 이미 많이 느낀 부분이기도 했습니다.
그리고 Yarn berry에 workspace 기능이 내장되어 있기 때문에 러닝 커브가 현저히 낮은 것도 사실입니다. 위에서 언급했듯 장점들이 존재했기에 yarn workspace로 결정할 수 있었습니다.
Yarn berry에 대한 자세한 내용은 해당 글을 참고하시기 바랍니다.
Lerna는 레포지토리에서 다양한 package 구성을 도와주는 라이브러리입니다.
workspace의 버전 관리, 테스트, 빌드, 배포 등의 작업을 최적화해서 구성이 가능합니다만 높은 러닝 커브가 가장 큰 문제여서 배제하게 되었습니다.
팀원도 그렇고 본인도 Lerna에 대한 개념이 전혀 없기 때문에 하나하나 찾아보고 테스트해보면서 마이그레이션을 진행해야 했기에 어쩌면 배보다 배꼽이 커질 수 있다는 문제점이 존재했습니다.
Nx는 구글에서 만든 오픈소스 프로젝트로 모노레포 구성을 위한 다양한 개발 도구를 제공하고 Angular, React 등 프론트엔드 뿐만 아니라 Express, Nest.js등 백엔드 기반도 폭넓게 지원합니다.
workspace 생성 시 Cypress, Jest 기반 테스트 환경까지 설정해주어서 초기 개발 환경 구축 비용을 크게 줄여줍니다.
다만 마찬가지로 높은 러닝 커브와 적은 레퍼런스로 인해 배제하게 되었습니다.
TurboRepo는 Vercel에서 개발 및 운영하고 있는 JavaScript, TypeScript 모노레포 빌드 시스템입니다.
Turborepo는 증분 빌드 원격 캐싱, 병렬 처리 기법을 통해 빌드 성능을 끌어올리고, Pipeline의 쉬운 설정과 profiling, trace 등 다양한 시각화 기능을 제공해 많은 편의성을 제공하고 있습니다.
TurboRepo가 큰 장점을 보유하고 있지만 기본적으로 Next.js
설정되기 때문에 현재 Next.js가 아닌 React를 사용하고 있는 현재 프로젝트 설정으로서는 아쉬움이 컸지만 배제할 수 밖에 없었습니다.
구성은 가능하겠지만 프로젝트의 변동이 너무나도 커지는 부분을 분명 예상할 수 있었기 때문입니다. 이후에는 꼭 한번 사용해보고 싶은 욕심이 나기도 했습니다.
모노레포 마이그레이션은 Yarn workspace로 결정했고 참고할 수 있는 레퍼런스들도 많았기에 바로 마이그레이션 작업에 들어갔습니다.
❗️ 본 프로젝트는 Vite, TypeScript, Yarn 기반 프로젝트입니다.
이미 서비스와 디자인 시스템 모두 yarn berry 기반으로 설정이 되어있기 때문에 yarn berry 작업은 넘어가도록 하겠습니다.
관련 작업 내용은 해당 글을 참고하시기 바랍니다.
루트에서 yarn 설정을 다시 진행해준 후 package.json
을 수정해주었습니다.
"name": "frontend-monorepo",
"packageManager": "yarn@4.1.1",
"workspaces": {
"packages": [
"packages/*"
]
},
그리고 packages
디렉토리를 만들어 서비스와 디자인 시스템 프로젝트를 옮겨 줍니다.
이후 각 프로젝트에 가서 install
을 진행해줍니다. 그리고 sdk 적용 또한 새로 해줬습니다.
yarn dlx @yarnpkg/sdks vscode
yarn berry 글에서 언급했었지만 yarn berry에서 typescript, eslint, prettier 등을 쓰기 위한 작업입니다.
다음으로는 workspace 관련 설정을 해주었습니다.
프로젝트 루트에 frontend-monorepo.code-workspace
파일을 새로 생성하고 다음과 같이 작성해주었습니다.
{
"folders": [
{
"path": "packages/waggle-design-system"
},
{
"path": "packages/waggle-service"
}
],
"settings": {
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true,
"**/dist": true,
"**/node_modules": true,
"tsconfig.tsbuildinfo": true
},
"eslint.nodePath": "../../.yarn/sdks",
"typescript.tsdk": "../../.yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"prettier.prettierPath": "../../.yarn/sdks/prettier/index.js",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"extensions": {
"recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
}
prettier
의 경우 프로젝트마다 설정을 다르게 가져가야 한다면 설정을 통해 다르게 사용할 수 있지만 해당 프로젝트 같은 경우 prettier 설정이 동일하기 때문에 루트에서 설정을 해주었습니다.
동일하게 .prettier.rc
파일에 작성해줍니다.
{
"endOfLine": "auto",
"singleQuote": false,
"tabWidth": 2,
"printWidth": 100,
"semi": true,
}
eslint
도 prettier랑 동일하게 설정이 같게 가져가고 있기 때문에 루트에 작성해주었습니다.
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint","import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-imports": "error",
"import/order": "off"
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx", ".js"]
},
"import/resolver": {
"typescript": "./tsconfig.json"
}
}
}
만약 프로젝트별로 다르게 설정을 해주어야 한다면 overrides
를 통해 설정을 다르게 가져갈 수 있습니다. 저는 사용하지 않았지만 예시 방법을 알려드리겠습니다.
overrides 필드를 아래와 같이 사용하시면 됩니다. 이름은 제가 예시로 작성한거기 때문에 files 필드의 경로는 해당 프로젝트 네임을 사용하시기 바랍니다.
overrides: [
{
files: ['**/*.ts?(x)'],
parser: '@typescript-eslint/parser',
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'react/prop-types': 'off',
'react/require-default-props': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-floating-promises': 'off',
},
parserOptions: {
project: ['./tsconfig.json', './packages/**/tsconfig.json'],
},
},
{
files: [
'packages/components-package/**/*.ts?(x)',
'packages/components-package/**/*.js?(x)',
],
settings: {
'import/resolver': {
typescript: {
project: path.resolve(
`${__dirname}/packages/components-package/tsconfig.json`
),
},
},
},
},
{
files: [
'packages/design-packages/**/*.ts?(x)',
'packages/design-packages/**/*.js?(x)',
],
settings: {
'import/resolver': {
typescript: {
project: path.resolve(
`${__dirname}/packages/design-packages/tsconfig.json`
),
},
},
},
},
],
해당 프로젝트는 husky와 lint-staged를 통해 커밋 시 eslint와 prettier를 자동으로 검사해주고 있었기 때문에 해당 설정 또한 새로 해주었습니다.
루트에 lint-staged.config.js
파일을 만들고 아래와 같이 작성해주었습니다.
module.exports = {
"*.+(ts|tsx)": [() => "yarn tsc -p tsconfig.json --noEmit"],
"packages/waggle-service/**/*.+(ts|tsx)": [
() => "yarn tsc -p packages/waggle-service/tsconfig.json --noEmit",
],
"packages/waggle-design-system/**/*.+(ts|tsx)": [
() => "yarn tsc -p packages/waggle-design-system/tsconfig.json --noEmit",
],
"**/*.+(ts|tsx|js|jsx)": ["eslint --fix --cache", "prettier --write"],
};
최종적으로 다음과 같은 프로젝트 구조로 마이그레이션을 완료했습니다.
우선 도입 목적이였던 문제점은 해결할 수 있었습니다.
코드 수정 -> PR 작성 -> Merge -> npm 배포 -> 서비스에서 업데이트 후 사용
위 과정이 아래 과정으로 간소화될 수 있었습니다.
코드 수정 -> 디자인 시스템 빌드 -> 서비스에서 사용
모노레포에서 끌어다 사용하기 때문에 npm에 배포할 이유도 사라졌고 서비스에서 한번 업데이트를 진행하는 과정 또한 필요 없었습니다.
이러면 npm 배포 시도할 시간에 진작 모노레포할껄
npm 배포 관련은 해당 글에서 확인 가능합니다..
다만 같은 레포지토리에서 관리되기 때문에 PR
및 ISSUE
관리를 좀 더 상세하게 할 필요가 생겼습니다. 디자인 시스템 코드와 서비스 코드가 같이 커밋에 올라가면 조금 곤란하기 때문이죠.
개발 프로세스 간소화가 목적이였기에 목적은 이루었지만 아직 개발 과정이라 배포가 이루어지지 않아서 CI/CD 과정에 대해서는 의문점으로 남아있습니다. 개발이 완료되고 배포까지 이루어지면 빌드 과정 타임은 다시 알아봐야할 거 같습니다.
프로젝트가 기업에서 쓰이는 서비스급 규모가 아니기에 중간에 이런 마이그레이션 과정이 크게 어렵지는 않았지만 만약 대형 규모 프로젝트라 생각하면 이 마이그레이션 과정이 벌써부터 끔찍해 보입니다.
항상 새로운 기술을 도입하고 리팩토링하는 과정에서 느끼는거지만 초기 계획 및 설계가 중요하다는 것
과 더불어 그 설계가 완벽하려면 많은 기술들을 알아야 비교를 하고 도입을 할 수 있다는 것을 뼈저리게 한번 더 느낀 거 같습니다.
이렇게 하나 더 배워가는건 좋지만 아직도 배울 점이 너무나도 많다는 부분에 조금은 슬픈 감정을 느끼는 마이그레이션 과정이였던 거 같습니다.
마이그레이션된 전체 코드는 해당 레포에서 확인이 가능하지만 저희 서비스 기반으로 만들어진 코드이기 때문에 설정이 다른 프로젝트에서는 참고만 하시기 바랍니다. 아니면 끔찍한 에러들을 경험할지도..
감사합니다.
모노레포 적용부터 yarn berry까지 (화해 기술 블로그)
https://blog.hwahae.co.kr/all/tech/11962
리멤버 웹 서비스 좌충우돌 Yarn Berry 도입기 (리멤버 기술 블로그)
https://blog.dramancompany.com/2023/02/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A2%8C%EC%B6%A9%EC%9A%B0%EB%8F%8C-yarn-berry-%EB%8F%84%EC%9E%85%EA%B8%B0/
node_modules로부터 우리를 구원해 줄 Yarn Berry (토스 기술 블로그)
https://toss.tech/article/node-modules-and-yarn-berry
Yarn berry workspace를 활용한 프론트엔드 모노레포 구축기 (우아한 형제들 기술 블로그)
https://techblog.woowahan.com/7976/
모던 프론트엔드 프로젝트 구성 기법 - 모노레포 개념 편 (네이버 d2 기술 블로그)
https://d2.naver.com/helloworld/0923884
모던 프론트엔드 프로젝트 구성 기법 - 모노레포 도구 편 (네이버 d2 기술 블로그)
https://d2.naver.com/helloworld/7553804#ch3