Nx (Mono repo)

NAVER D2

모노레포에 대해서는 위 문서가 매우 자세하게 소개했습니다.
그 안에서 모노레포를 구성하는 여러 툴(yarn, Lerna, Nx, Turborepo)이 소개되는데 그 중 팀에서 택한 Nx에 대해 간단히 소개하고 겪은 문제에 대해 기술합니다.

Nx 기본 디렉토리 구조

.
├── angular.json
├── apps/
│   ├── api/
│   ├── client/
│   └── client-e2e/
├── libs/
│   └── api-interfaces/
├── nx.json
├── package.json
├── tools/
└── tsconfig.base.json
  • apps: 어플리케이션 소스
  • libs: 어플리케이션 공통 소스
  • tools: 어플리케이션 관련 소스를 제외한 나머지 (helm 차트, docker 파일 등)
  • angular.json : 각 프로젝트 네임과 path를 매핑 (version 2)
  • 파일 내용에 version 1은 기존에 사용하던 앵귤러 빌드 설정 포맷
  • version 2 는 angular.json 파일을 각 프로젝트로 쪼갠 형태 https://nx.dev/getting-started/nx-and-angular#angular.json
  • nx.json : nx cli 옵션 설정 https://nx.dev/configuration/packagejson#cli-configuration
  • tsconfig.base.json : 타입스크립트 빌드 옵션 베이스 파일

각 프로젝트 (Library, Apps) 하위 디렉토리 구조

│   ├── api/
│   │   ├── project.json
│   │   ├── src/
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.editor.json
│   │   ├── tsconfig.json
│   │   └── tsconfig.spec.json
  • project.json : 프로젝트 설정(빌드, 테스트, 린트 등) 파일 https://nx.dev/configuration/projectjson#project-configuration
  • 기존에 앵귤러 빌드 설정 파일(angular.json)과 형태가 비슷
  • tsconfig.json : 레포지토리 루트 경로의 tsconfig.base.json 파일을 확장하여 사용
  • tsconfig.app.json : 애플리케이션 빌드 설정
  • tsconfig.editor.json : IDE에서 타입 오류를 잡을 때 해당 파일 설정을 참고
  • tsconfig.spec.json : 테스트 설정

문제점

프로젝트 의존성 관리

모노레포로 멀티 프로젝트를 관리하면서 각 프로젝트의 의존성 관리가 중요하다.
잘못 관리하는 경우

  • 클라이언트 프로젝트가 서버의 디비 스키마 파일을 import 하게 되어서 보안적인 문제가 생긴다.
  • 서스테이닝 단계에서 클라이언트의 공통 소스 파일을 고쳤는데 서버 프로젝트가 영향을 받는다.
  • 클라이언트에서 서버 라이브러리를 호출하게 되어서 프로젝트가 알 수 없는 에러를 내뱉는다.
  • 특히 Nestjs, Angular이 모노레포에 같이 존재하면 매우 빈번히 교차 import가 발생한다. (Nestjs 설계가 Angular에서 많은 영감을 받아... 기본적인 모듈 네이밍이 같다.)

등 여러 경우가 발생할 수 있으니 nx에서는 프로젝트 의존성 관리를 위해서 eslint 룰을 적용하여 강제하기도 하고 nx graph를 지원하여 프로젝트 의존성 시각화 툴도 제공한다.

eslint rule(@nrwl/nx/enforce-module-boundaries)

github : https://github.com/nrwl/nx/blob/master/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts

docs
- https://nx.dev/structure/monorepo-tags
- https://blog.nrwl.io/mastering-the-project-boundaries-in-nx-f095852f5bf4

각 프로젝트마다 태그를 정의하고 ex) library:api-interface, apps:server ...
project.json > tags에서 프로젝트의 태그를 정의 (https://nx.dev/structure/monorepo-tags#tags)

eslint에서 해당 프로젝트에서 import하거나 ban할 태그들의 바운더리를 정할 수 있다.

아래는 NestJs를 사용하는 서버 프로젝트에서 바운더리를 정하는 예시다.

  • api-interfaces와 서버 공통소스는 사용, 클라이언트 공통소스는 막고, 외부 라이브러리 중 앵귤러도 막는다.
  • sourceTag: 프로젝트의 태그 네임 (에러 메시지에 넣기 위해서만 사용하지 다른 기능 없다. 태그 정의 자체는 project.json 파일에서 한다)
  • onlyDependOnLibsWithTags: 해당 태그들의 소스만 import 하겠다는 내용, 자신의 태그도 넣어야함
  • notDependOnLibsWithTags: import하지 않는 소스의 태그들
  • bannedExternalImports: 모노레포 하위의 프로젝트 말고 외부 라이브러리도 체크 ex) 서버 프로젝트에서 앵귤러를 가져오는걸 막음
{
  "root": true,
  "ignorePatterns": ["**/*"],
  "plugins": ["@nrwl/nx"],
  "overrides": [
    {
      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "rules": {
        "@nrwl/nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            "allow": [],
            "depConstraints": [
              {
                "sourceTag": "apps:api",
                "onlyDependOnLibsWithTags": ["apps:api","library:api-interfaces", "library:server-shared"],
				"notDependOnLibsWithTags": ["library:client-shared"],
				"bannedExternalImports": ["@angular/*"]
              }
            ]
          }
        ]
      }
    }
  ]
}

중요한 설정은 depConstraints에 정의하고 있고 그 외 다른 옵션들도 존재한다 참고 (https://github.com/nrwl/nx/blob/3b1ea739f911b4655e33fdced5708140ebadc59c/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts#L74-L106)


tsconfig paths

위의 eslint로 룰을 정의하고 그대로 프로젝트에서 상대 혹은 절대경로로 다른 프로젝트의 소스를 import하면 오류가 발생한다.

@nrwl/nx/enforce-module-boundaries 룰에서 해당 설정만 on off 할 수 없다. 무조건 npm scope를 통해서 가져와야한다. (아니면 그냥 해당 eslint 룰을 가져오지 말아야함)

npm scope 정의는 tsconfig > paths에서 한다.

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "importHelpers": true,
    "target": "es2017",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      // 클라이언트 공통 컴포넌트
      "@client/components": ["libs/client/src/components/index.ts"]
    }
  },
  "exclude": ["node_modules", "tmp"]
}

Etc

nx graph

모노레포에 있는 프로젝트들 사이의 의존성 시각화

nx graph # 브라우저에서 오픈
nx graph --file=output.json # json 파일로 추출
nx graph --file=output.html # static 파일들 생성, nx graph 와 동일한 뷰

아래와 같은 구조라 하면

.
├── apps
│   ├── app1
│   │   ├── app1-client
│   │   │   └── tsconfig.json (extends tsconfig.client-base.json)
│   │   └── app1-server
│   │       └── tsconfig.json (extends tsconfig.server-base.json)
│   └── app2
│       ├── app2-client
│       │   └── tsconfig.json (extends tsconfig.client-base.json)
│       └── app2-server
│            └── tsconfig.json (extends tsconfig.server-base.json)
├── libs
│   ├── client
│   │   ├── src
│   │   └── tsconfig.json (extends tsconfig.base.json)
│   ├── data
│   │   ├── src
│   │   └── tsconfig.json (extends tsconfig.base.json)
│   ├── server
│   │   ├── src
│   │   └── tsconfig.json (extends tsconfig.base.json)
│   └── utils
│       ├── src
│       └── tsconfig.json (extends tsconfig.base.json)
├── nx.json
├── nx.md
├── package-lock.json
├── package.json
├── tree.md
├── tsconfig.base.json
├── tsconfig.client-base.json
└── tsconfig.server-base.json

위와 같이 시각화하여 모노레포 구조를 쉽게 파악할 수 있도록 한다. (README.md에 같이 넣으면 좋을듯)