상세 컨텐츠

본문 제목

Monorepo이거 진짜 쓰면 안되겠네요.

기타

by 리액트바오 2024. 1. 18. 22:33

본문

Monorepo바로 쓰면 안되겠네요

Monorepo에 대해 먼저 좀 알아보고 써야겠네요.

 

이사님께서 지금까지 해오던 프로젝트를 모노레포로 바꾼다고 말씀하셨고, 나는 모노레포가 무엇인지 궁금해서 공부해보았다. 또, 모노레포로 작업을 하기 위한 세팅을 해보았다.

 

모노레포가 뭘까? 

모노레포는 한마디로..

하나의 레포지토리에서 독립적으로 여러 프로젝트를 관리하는 방법이다.

 

 

멀티 레포(Multi-repo)와 모노 레포(Monorepo)의 차이점

멀티 레포 Multi-repo

특징: 각 프로젝트 또는 라이브러리가 별도의 저장소에 저장된다.

버전 관리: 각 프로젝트는 독립적으로 버전 관리된다.

의존성: 프로젝트 간 의존성은 외부 패키지로 관리되며, 버전 충돌이 발생할 수 있다.

배포 및 빌드: 각 프로젝트는 별도로 빌드 및 배포된다.

프로젝트 격리: 프로젝트 간 격리가 잘 되어있어, 한 프로젝트에서 발생한 문제가 다른 프로젝트에 영향을 미치지 않는다.

설정 및 유지 관리: 각각의 저장소에 대한 설정과 유지 보수가 필요하다.

 

 

모노 레포 Monorepo

특징: 여러 프로젝트와 라이브러리가 하나의 저장소에 함께 저장된다.

버전 관리: 모든 프로젝트가 하나의 저장소에서 통합적으로 관리된다.

의존성: 공유된 의존성을 통해 프로젝트 간의 호환성을 유지하기 쉽다.

배포 및 빌드: 중앙집중식으로 빌드 및 배포 프로세스를 관리할 수 있다.

프로젝트 상호 작용: 프로젝트 간 코드 공유 및 재사용이 용이하다. (다른 프로젝트에 있는 컴포넌트도 쉽게 불러올 수 있다는것이다.)

설정 및 유지 관리: 하나의 저장소로 모든 프로젝트를 관리하기 때문에, 일관된 설정과 유지 보수가 가능하다.

 

 

멀티 레포와 모노 레포 외의 다른 접근 방식

하이브리드 접근법

특정 프로젝트나 서비스는 개별 저장소에서 관리하고, 일부 공유 라이브러리나 서비스는 모노 레포에서 관리하는 방식으로 이 접근법은 멀티 레포와 모노 레포의 장점을 섞을 수 가 있다. 

 

서브모듈(Submodules) 또는 서브트리(Subtrees)

Git 서브모듈이나 서브트리를 사용하여, 별도의 저장소에 위치한 코드를 메인 프로젝트에 통합할 수 있다.

이 방식은 멀티 레포의 독립성을 유지하면서 코드를 일부 공유할 수 있는 방법을 제공한다.

 

패키지 매니저를 통한 의존성 관리

공유 라이브러리나 모듈을 별도의 패키지로 배포하고, 패키지 매니저(NPM, NuGet 등)를 통해 각 프로젝트에서 해당 패키지를 의존성으로 추가한다. 이 방식은 멀티 레포 환경에서 흔히 사용되며, 코드 재사용성을 높이는 데 도움이 된다.

 

 

각 방식은 고유한 장단점을 가지고 있기에 프로젝트의 특성, 팀의 작업 방식, 유지 보수 요구 사항 등에 따라 적합한 방식을 선택하는 것이 중요하다.

 

 

모노 레포의 장점

통합된 버전 관리: 모든 프로젝트와 라이브러리가 하나의 저장소에 있어 버전 관리가 통합된다. 이는 의존성 관리를 간소화하고, 코드베이스 전반에 걸친 변경사항을 쉽게 추적할 수 있게 해준다.

의존성 관리 용이성: 프로젝트 간 공유되는 코드 및 라이브러리에 대한 의존성 관리가 용이해진다. 코드 변경 시 관련된 모든 프로젝트에 대한 영향을 즉시 파악하고, 필요한 조치를 취할 수 있다.

일관된 도구 및 스크립트 사용: 공통된 빌드, 테스트, 배포 스크립트 및 도구를 전체 저장소에 걸쳐 일관되게 사용할 수 있다. 이는 개발 프로세스의 표준화에 도움이 된다.

코드 재사용 및 협업 촉진: 다양한 프로젝트 간 코드 재사용이 용이해지고, 팀원 간 협업이 촉진된다. 한 프로젝트에서의 개선 사항이 다른 프로젝트에도 쉽게 적용될 수 있다.

효율적인 CI/CD 통합: Continuous Integration (CI) 및 Continuous Deployment (CD) 파이프라인을 한 곳에서 관리할 수 있어, 전체 프로젝트에 걸쳐 일관된 테스트 및 배포 프로세스를 구축할 수 있다.

CI (Continuous Integration): 지속적인 통합

CD(Continuous Deployment): 지속적인 배포

 

 

모노 레포 사용 방법

저장소 초기화:

새로운 Git 저장소를 생성하고, 모든 프로젝트와 라이브러리를 이 저장소에 통합한다.

 

디렉토리 구조 설계:

일반적으로 packages 또는 projects 디렉토리 내에 개별 프로젝트를 위치시킨다. 각 프로젝트는 독립된 폴더를 갖는다.

도구 사용:

Lerna, Yarn Workspaces, Nx 등과 같은 도구를 사용하여 의존성 관리 및 프로젝트 간의 연결을 용이하게한다. (우리 프로젝트는 Lerna를 쓴다.)

Lerna는 여러 저장소의 의존성을 묶어주는 역할과 각 패키지별 버전 업데이트를 도와준다.

 

공통 의존성 관리:

공통 의존성은 루트 레벨의 package.json에 정의하여 모든 프로젝트에서 사용할 수 있게 한다.

 

빌드 및 배포 스크립트 통합:

루트 레벨에서 공통 빌드, 테스트, 배포 스크립트를 구성하여 프로젝트 전체에 적용한다.

CI/CD 파이프라인 설정:

저장소 전체에 걸친 CI/CD 파이프라인을 설정하여, 코드 변경 시 자동으로 빌드, 테스트, 배포가 진행되도록 한다.

 

문서화 및 가이드라인 제공:

프로젝트 구조, 사용 방법, 개발 규칙 등을 명확하게 문서화하여, 팀원들이 모노 레포 환경에서 효과적으로 작업할 수 있도록 지원한다.

 

 

모노 레포는 여러 프로젝트를 동시에 관리해야 하는 경우에 유용하다. 그러나 초기 설정과 관리에는 상당한 노력이 필요하기에, 프로젝트의 요구사항과 팀의 작업 방식에 맞는지 충분히 고려한 후 도입하는 것이 좋다.

 

 

Monorepo 환경 설치

## Monorepo 환경 설치

1. lerna 설치

`npm install -g lerna`

1. 프로젝트 생성
- [ ]  프로젝트 폴더 생성
- [ ]  프로젝트 폴더로 이동
- [ ]  프로젝트 초기화
- 방법1. 기본 프로젝트 생성 후 패키지 폴더 수작업 생성

`lerna init --independent`

- 방법2. 프로젝트 생성시 패키지 폴더 생성

`lerna init --packages="packages/*" --packages="shared/*" --independent`

1. 프로젝트 설정
- [ ]  package.json

`"workspaces": ["packages/*", "shared/*"]`

1. 프로젝트 공통 종속성 설치
- [ ]  Typescript

`npm install typescript @types/node — save-dev
npx tsc --init`

- [ ]  prettier
- 설치

`npm install --save-dev prettier`

- 설정
.prettierrc.js 파일 생성

`module.exports = {  
  arrowParens: 'avoid',
  bracketSameLine: true,
  bracketSpacing: true,
  singleQuote: true,
  trailingComma: 'all',
  endOfLine: 'auto',
};`

- [ ]  eslint

**자동**

`npm init @eslint/config`

**수동**

`npm install --save-dev eslint
npx eslint --init`

- 플러그인

prettier 연동

`npm install --save-dev eslint-plugin-prettier`

.eslintrc.* 파일의 extends에 prettier 추가

`{
  "extends": [
    ...
    "plugin:prettier/recommended",
  ]
}`

react 프로젝트일 경우

`npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y`

- package.json 수정

`{
  ...
  ...
  "scripts": {
     ...
     ...
     "lint": "eslint 'src/**/*.{js,ts,jsx,tsx}' --quiet --fix"
     ...
     ...
   }
   ...
   ...
}`

- [ ]  lint-staged / hustky

## 패키지 모듈

1. 패키지 모듈 생성
- [ ]  react 모듈
패키지 폴더로 이동

`npx create-react-app <모듈명> --template typescript --use-npm`

- [ ]  lerna 모듈
루트 폴더로 이동

`lerna create <모듈명> <패키지명>`

- [ ]  npm 모듈

`npm init -y`

1. 패키지 모듈별 종속성 설치
- [ ]  타입스크립트
Typescript 모듈이 아닐 경우 별도 추가

`npm install --save-dev typescript
npx tsc --init`

- [ ]  웹팩

`npm install --save-dev webpack webpack-cli html-webpack-plugin webpack-dev-server webpack-merge ts-loader
npx webpack init`

위의 방법을 참고하여 프로젝트를 만들었다.

 

모노레포를 사용하는 대표적인 회사

Google: Google은 모든 소프트웨어 프로젝트를 하나의 거대한 모노레포에서 관리하는 것으로 유명하다. 이는 Google의 다양한 서비스와 프로젝트들이 상호 의존적이며, 통합된 도구와 프로세스를 통해 관리될 수 있도록 한다.

Facebook: Facebook도 자체 소스 코드를 모노레포로 관리한다. 이를 통해 페이스북의 다양한 서비스와 제품들이 원활하게 통합되고, 코드 재사용 및 협업이 용이해진다.

Microsoft: 특히, Microsoft의 Windows는 거대한 모노레포에서 관리된다. 이는 Windows의 많은 컴포넌트와 의존성들을 효과적으로 관리하기 위함이다.

Twitter: Twitter 또한 자신들의 서비스를 모노레포를 통해 관리하는 것으로 알려져 있다. 이는 대규모 팀과 프로젝트 간의 협업을 촉진한다.

Airbnb: Airbnb는 자신들의 웹 애플리케이션과 관련 서비스들을 모노레포로 관리하여, 개발 프로세스를 통합하고 효율성을 높이고 있다.

 

 

이러한 기업들은 모노레포를 사용하여 코드 통합, 팀 간 협업 강화, 개발 프로세스의 표준화 등 다양한 이점을 얻고 있습니다. 그러나 모노레포는 대규모 조직과 복잡한 프로젝트에 특히 적합하며, 중소 규모의 프로젝트나 팀에서는 멀티 레포 방식이 더 적합할 수 있습니다.

 

왜냐?

모노레포가 많은 장점을 제공하긴 하지만, 모든 상황에 완벽하게 적합한 것은 아니기 때문이다. 모노레포 방식의 단점 또는 고려해야 할 점은 다음과 같다.

 

 

규모의 복잡성: 거대한 코드베이스를 한 곳에 관리하게 되면, 시스템의 복잡성이 증가한다. 이로 인해 프로젝트 관리, 코드 탐색, 문서화 등이 어려워질 수 있다.

빌드 시간: 프로젝트의 규모가 커질수록 빌드 시간이 길어질 수 있다. 특히, 관련 없는 부분까지 불필요하게 빌드되는 경우가 발생할 수 있다.

CI/CD 파이프라인 복잡성: 모든 코드와 프로젝트가 한 곳에 있기 때문에, Continuous Integration (CI) 및 Continuous Deployment (CD) 파이프라인이 복잡해질 수 있다. 이는 테스트 및 배포 프로세스를 관리하는데 더 많은 리소스와 노력이 필요하게 된다.

저장소 크기: 모든 코드와 자산이 하나의 저장소에 있기 때문에, 저장소의 크기가 매우 커질 수 있다. 이는 클론, 풀, 푸시 등의 Git 작업에 영향을 줄 수 있다.

보안 문제: 단일 저장소에서 다양한 프로젝트를 관리하는 경우, 보안 관리가 복잡해질 수 있다. 잘못된 권한 설정으로 인해 민감한 코드가 노출될 위험이 있다.

팀 협업의 어려움: 모든 프로젝트와 팀이 같은 저장소를 공유함으로써, 협업 시 충돌이나 의존성 문제가 발생할 수 있습니다. 또한, 큰 프로젝트에서는 변경 사항을 추적하기 어려울 수 있다. (git pull 받을때마다 엄청난 에러가 쏟아지고 있다.)

pull하는게 두려워..

따라서 모노레포를 도입하기 전에는 프로젝트의 규모, 팀의 구조, 개발 및 배포 프로세스 등을 고려하여 이 방식이 적합한지 신중히 평가해야 한다. 때때로 멀티 레포 또는 하이브리드 접근법이 더 나은 선택일 수도 있다.

 

 

처음 프로젝트를 생성할때는 뭐가 기본일까?

그러니깐.. 처음 개발프로젝트를 생성하는 사람이 ‘나는 모노레포 방식으로 개발할거야’라던지 ‘난 멀티레포로 개발하겠어’라고 정해두진 않는단 말이다. 모르니깐.

처음 간단한 프로젝트를 생성하는 경우, 일반적으로 '멀티 레포(Multi-repo)' 방식을 따르게 된다. 이때 멀티 레포라는 용어를 명시적으로 사용하지 않더라도, 기본적인 접근 방식이 그러하다.

이는 단일 프로젝트가 자체적인 저장소(repository)를 가지고, 그 안에서 독립적으로 관리되는 구조를 의미한다. 예를 들어, 한 개발자가 GitHub에 '내 첫 번째 웹사이트'

 

일반적으로 처음 코딩을 시작하는 사람들이 프로젝트를 생성할 때는, 별도의 모노레포나 멀티레포 전략을 고려하지 않고 단순히 하나의 프로젝트를 위한 단일 저장소를 생성하는 것이 일반적이다. 이러한 접근 방식은 기본적으로 멀티레포(Multi-repo) 방식에 가깝다.

 

 

기본 프로젝트 생성이 멀티레포에 해당하는 이유

독립된 저장소: 새 프로젝트를 생성할 때, 보통 한 개의 저장소(repository)에 해당 프로젝트의 모든 코드를 넣는다. 이는 멀티레포 접근법의 기본 특성인 각 프로젝트의 독립된 저장소를 의미한다.

개별 관리: 프로젝트는 독립적으로 관리되며, 별도의 빌드, 테스트, 배포 과정을 갖는다. 이는 멀티레포 방식에서 각각의 레포지토리가 자체적으로 관리되는 것과 유사하다.

단순성: 초기 프로젝트는 단순하며, 다른 프로젝트와의 복잡한 상호 의존성이나 연결이 없다. 멀티레포 방식이 제공하는 단순한 구조와 일치한다.

 

 

즉, 프로젝트를 처음 만드는 경우, 대부분의 개발자는 멀티 레포(Multi-repo) 방식을 기본으로 사용한다.

 

 

모노레포(Monorepo)는 대규모 프로젝트, 복잡한 의존성을 가진 프로젝트, 또는 여러 팀이 협업하는 환경에서 더 유리할 수 있다. 그러나 모노레포는 관리의 복잡성, 필요한 도구 및 설정의 고급 수준 때문에 초기 학습자에게는 다소 부담이 될 수 있다.

따라서 처음 프로젝트를 시작할 때는 멀티 레포 방식으로 시작하는 것이 일반적이며, 프로젝트의 규모나 팀의 필요에 따라 나중에 모노레포로 전환하는 것을 고려할 수 있다.

 

레르나의 필요성과 npm과의 차이점

모노레포에 대해 알아보다가 왜 갑자기 레르나냐고? 

단일 리포지토리 내에서 여러 패키지 또는 프로젝트가 관리되는 설정인 모노레포를 개발할 때 Lerna 및 NPM과 같은 도구는 중요한 역할을 하지만 다소 다른 방식으로 수행된다. 모노레포를 효과적으로 관리하려면 이들의 목적과 차이점을 이해하는 것이 중요하다.

 

 

레르나로 모노레포 구조를 실제 프로젝트에 적용하는법

Lerna는 여러 패키지가 포함된 JavaScript 프로젝트를 관리하기 위해 특별히 설계된 도구로, 모노레포 설정에 널리 사용된다. 주요 기능은 다음과 같다.

 

자동 버전 관리 및 게시: Lerna는 저장소 내 여러 패키지의 버전 관리 프로세스를 자동화할 수 있다. 마지막 릴리스 이후 어떤 패키지가 변경되었는지 확인하고 그에 따라 버전을 늘릴 수 있다. 또한 이러한 패키지를 npm과 같은 레지스트리에 게시하는 프로세스를 단순화한다.

 

종속성 관리: Lerna는 공유 종속성을 모노레포의 루트로 끌어올려 종속성 설치를 최적화한다. 이렇게 하면 중복이 줄어들고 설치 시간이 단축된다.

 

패키지 전체에서 스크립트 실행: Lerna를 사용하면 모든 패키지에서 또는 선택적으로 스크립트를 실행할 수 있다. 이는 모노레포의 모든 패키지를 동시에 빌드, 테스트 또는 린팅하는 데 특히 유용하다. (Linting은 코드에서 잠재적인 오류, 문체 문제 또는 기타 코딩 표준을 분석하는 프로세스다)

 

독립 또는 고정/잠금 모드: Lerna는 두 가지 기본 작동 모드, 즉 모든 패키지가 동일한 버전 번호를 공유하는 고정(잠금) 모드와 각 패키지가 자체 버전 번호를 가질 수 있는 독립 모드를 지원한다.

 

 

lerna.json

{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "independent"
}

그렇다면, 모노레포 프로젝트에서 볼 수 있는 React.lazy() 정체는 뭘까

React.lazy()란 무엇인가?

React.lazy()는 React에서 제공하는 유틸리티 함수로, 동적 임포트를 통해 컴포넌트를 필요할 때 로드하는 방법이다. 이 기능은 React 16.6에서 도입된 React의 코드 분할 기능의 일부다. 동적으로 임포트된 컴포넌트를 일반 컴포넌트처럼 렌더링할 수 있게 해주며, 이는 애플리케이션의 성능을 크게 향상시킬 수 있다.

 

React.lazy()의 작동 방식은?

동적 임포트:  React.lazy()는 동적 import() 구문과 함께 작동하여 필요할 때 컴포넌트를 임포트한다.  초기 앱 로드 시 모든 컴포넌트를 불러오는 대신, React.lazy()는 런타임에 컴포넌트를 동적으로 임포트한다.

코드 분할: React.lazy()를 컴포넌트에 사용하면, 웹팩(또는 사용하는 번들러)이 해당 컴포넌트에서 애플리케이션의 번들을 분할한다. 이로 인해 해당 컴포넌트의 별도 청크 파일이 생성되며, 이는 컴포넌트가 렌더링될 때만 로드된다. 이 분할은 초기 로드 크기를 줄여 앱이 더 빨리 로드되도록 한다.

 

Suspense 통합: React.lazy()는 Suspense 컴포넌트와 함께 사용된다.  Suspense를 사용하면 지연 로드되는 컴포넌트가 완전히 로드되고 렌더링될 준비가 될 때까지 표시할 대체 UI를 지정할 수 있다. 이 대체 UI는 일반적으로 로딩 인디케이터 또는 다른 적절한 플레이스홀더이다.

import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

이렇게 각각의 프로젝트에 Suspense로 감싸주었다.

 

// src/utils/customLazyLoader.js
import React from 'react';
import FallbackComponent from './FallbackComponent';

export function LazyLoader(importFunction) {
  return React.lazy(() =>
    importFunction().catch(error => {
      console.error('Failed to load the component', error);
      return { default: FallbackComponent };
    }),
  );
}

//동적 가져오기 오류가 오류 경계에 도달하기 전에 이를 포착하고 처리하는 함수

//모듈을 동적으로 가져오는 중에 발생하는 모든 오류를 포착한다.

결과물이다.

packages폴더에서 가장 root가 되는 host프로젝트에 각각 A프로젝트와 B프로젝트를 불러왔고, B프로젝트는 서버를 꺼보았다.

원래는 이런 에러가 났지만, ErrorBoundary로 감싸주었기 때문에 B프로젝트 영역에는 '존재하지 않습니다'라는 글씨로 나타나고 있다. 

 

 

이렇게 말이다.

 

 

모노레포로 진행하면서 부딪힌 문제중 하나는 context가 안먹는것이다. 검색을 해보니 여러가지 이유가 있다지만  내가 가장 의심이가는 부분은 모노레포에서 사용하는 lazy()이다. 모노레포에서 lazy()를 사용하면 Context가 차단될 수 있다고 한다.  lazy()는 컴포넌트를 렌더링하기 전에 컴포넌트의 코드를 로드하고 인스턴스화하는데, 이 과정에서Context.Consumer를 사용하지 않으므로, Context를 차단할 수 있다는것이다.

여기서 잠깐! (인스턴스화는 클래스의 정의에 따라 객체의 메모리 공간을 할당하고, 객체의 속성과 메서드를 초기화하는 과정을 말한다.)

 


의심이 되는 이부분을 테스트해볼계획이다.

 

 

 

관련글 더보기