NX monorepo에 Eslint, Prettier, Docker, Github Actions 설정하기

seoyeonpp·2024년 10월 15일

Monorepo

목록 보기
2/5
post-thumbnail

모노레포

여러 비슷한 성격의 비즈니스 서비스를 한 레포지토리에 넣기위해 모노레포로 구성하기로 했다.
모노레포의 장점은 Eslint, Prettier를 root에서 관리하고 각 앱들에 적용시킬수 있다는 점이다.
또, 공통된 CI/CD를 가져갈수있어서 계속 똑같은 코드를 여러 저장소에 복붙하지 않아도 되서 좋은것같다.👍

작업시작!

지금은 Nx monorepo 에 Next.js앱을 1개 생성한 상태이다.
npx create-nx-workspace@latest --preset=next

1. 패키지 매니저 변경

기존 서비스는 npm을 사용하고 있었는데 이번에 마이그레이션 하는김에 pnpm으로 변경도 같이 하기로 했다.

  • 기존에 pnpm이 global로 설치가 되지 않아서, 설치부터 했다.
    npm i -g pnpm

  • 그리고 node_modules 삭제 후, 최상단 package.json에 아래의 script를 추가했다.

"scripts": {
	"preinstall": "npx only-allow pnpm"
}
  • 기존에 있던 package-lock.json을 이용해서 pnpm-lock.yaml 파일을 생성하도록한다.
    pnpm import

  • 기존에 있던 package-lock.json 파일을 제거한다.
    rm ./package-lock.json

  • 의존성을 pnpm으로 다시 설치한다.
    pnpm install

  • 모노레포를 위해 최상단에 pnpm-workspace.yaml 파일을 생성한다.

  • 해당 파일에 사용할 package를 작성한다.

packages:
  - 'apps/**'
  - 'libs/**'
  • nx.json"packageManager": "pnpm" 를 추가한다.

2. 기존에 있던 .eslintrc.json 파일 변환

eslint가 업데이트가 되면서 eslint.config.js로 변환이 필요했다.
기존에 있는 json으로 변환이 가능해서 아래의 명령어를 입력해서 변환을 했다.
nx g @nx/eslint:convert-to-flat-config

  • 최상단에서만 관리할것이기 때문에, apps/ 하위에있는 앱의 eslint.config.js는 삭제한다.
  • apps/하위의 project.json에 아래와 같이 작성한다.
	"targets": {
      "lint": {
        "executor": "@nx/eslint:lint",
        "options": {
          "eslintConfig": "{workspaceRoot}/eslint.config.js"
        }
      },
  	}
최상단 eslint.config.js 코드
  const { FlatCompat } = require('@eslint/eslintrc')
const js = require('@eslint/js')
const nxEslintPlugin = require('@nx/eslint-plugin')
const typescriptEslintEslintPlugin = require('@typescript-eslint/eslint-plugin')
const eslintPluginReact = require('eslint-plugin-react')
const eslintPluginPrettier = require('eslint-plugin-prettier')
const eslintPluginSimpleImportSort = require('eslint-plugin-simple-import-sort')
const eslintPluginImport = require('eslint-plugin-import')
const typescriptEslintParser = require('@typescript-eslint/parser')
const globals = require('globals')

const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
})

module.exports = [
  js.configs.recommended,
  ...compat.extends('airbnb', 'airbnb-typescript', 'plugin:@typescript-eslint/recommended', 'prettier'),
  {
    plugins: {
      '@nx': nxEslintPlugin,
      '@typescript-eslint': typescriptEslintEslintPlugin,
      react: eslintPluginReact,
      prettier: eslintPluginPrettier,
      'simple-import-sort': eslintPluginSimpleImportSort,
      import: eslintPluginImport,
    },
  },
  {
    languageOptions: {
      parser: typescriptEslintParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        project: './tsconfig.json',
      },
      globals: { ...globals.browser, ...globals.node },
    },
  },
  {
    rules: {
      '@typescript-eslint/explicit-module-boundary-types': ['error'],
      'import/extensions': 'off',
      'import/no-unresolved': 'off',
      'import/no-extraneous-dependencies': 'off',
      'import/prefer-default-export': 'off',
      'no-shadow': 0,
      'no-console': 'warn',
      'react/react-in-jsx-scope': 'off',
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': 'warn',
      '@typescript-eslint/semi': 'off',
      'no-debugger': 'error',
      'no-use-before-define': [
        'error',
        {
          functions: false,
          variables: false,
        },
      ],
      'no-undef': 'off',
      'no-restricted-globals': ['off'],
      'consistent-return': 'off',
      'no-plusplus': 'off',
      'prefer-destructuring': 'off',
      camelcase: 'warn',
      curly: 'error',
      eqeqeq: 'error',
      'no-param-reassign': ['error', { props: false }],
      'global-require': 0,
      'no-underscore-dangle': ['error', { allow: ['_data'] }],
      'object-curly-newline': ['error', { multiline: true }],
      'operator-linebreak': 'off',
      'simple-import-sort/imports': 'error',
      'simple-import-sort/exports': 'error',
      'prettier/prettier': [
        'warn',
        {
          plugins: ['prettier-plugin-tailwindcss'],
          arrowSpacing: ['error', { before: true, after: true }],
          singleQuote: true,
          semi: false,
          useTabs: false,
          tabWidth: 2,
          trailingComma: 'all',
          printWidth: 120,
          bracketSpacing: true,
          arrowParens: 'always',
          endOfLine: 'auto',
        },
      ],
    },
  },
  {
    files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
    rules: {
      'simple-import-sort/imports': 'warn',
      'simple-import-sort/exports': 'warn',
      '@nx/enforce-module-boundaries': [
        'error',
        {
          enforceBuildableLibDependency: true,
          allow: [],
          depConstraints: [
            {
              sourceTag: '*',
              onlyDependOnLibsWithTags: ['*'],
            },
          ],
        },
      ],
    },
  },
  ...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
    ...config,
    files: ['**/*.ts', '**/*.tsx'],
    rules: {
      ...config.rules,
    },
  })),
  ...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
    ...config,
    files: ['**/*.js', '**/*.jsx'],
    rules: {
      ...config.rules,
    },
  })),
  ...compat.config({ env: { jest: true } }).map((config) => ({
    ...config,
    files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'],
    rules: {
      ...config.rules,
    },
  })),
  { ignores: ['**/*/next.config.js', './lint-staged.config.js'] },
]

3. 기존에 있던 prettier 파일 확장자 추가

기존에 .prettierrc 에 확장자가 없어서 인식이 안됐었다.
.prettierrc.json 으로 확장자를 추가하니, 잘 세팅되었다.

tsconfig.json은 기존에 쓰던 파일이 그대로 적용되었는데도 잘돼서 세팅할필요가 없었다.

4. husky추가

커밋할때 lint검사를 위해 husky를 추가했다.
pnpm add husky lint-staged

  • 최상단 package.json 의 script에 husky를 추가한다.
"scripts": {
    "prepare": "husky",
    "preinstall": "npx only-allow pnpm"
  },
  • .husky/pre_commit 에 아래와같이 작성한다.
    npx lint-staged --concurrent false --relative
  • 최상단에 lint-staged.config.js 파일 생성 후, 아래와 같이 작성한다.
module.exports = {
  '{apps,libs,tools}/**/*.{ts,tsx}': (files) => `nx affected --target=typecheck --files=${files.join(',')}`,
  '{apps,libs,tools}/**/*.{js,ts,jsx,tsx,json}': [
    (files) => `nx affected:lint --files=${files.join(',')}`,
    (files) => `nx format:write --files=${files.join(',')}`,
  ],
}

5. Dockerfile추가

사실 Docker라곤 전에 튜링의 사과에서1시간 실습해본 경험밖에 없어서, 여러 문서를 복붙해서 만들어봤는데 컨테이너가 생성되가지고 깜짝놀랬다.

nx container라는 라이브러리가 있어서 해당 라이브러리로 Dockerfile을 구성해봤다.
@nx-tools/nx-container

위의 docs대로 하면 Dockerfile이 생성되고, apps/하위의 project.json 에 container라는 값이생긴다.

  • apps/하위의 next.config.js에 compile에 대한 부분을 작성한다.
compiler: {
    removeConsole: {
      exclude: ['error', 'warn'],
    },
  },
  output: 'standalone',
  transpilePackages: ['@sf-fe/source'],
  experimental: {
    outputFileTracingRoot: path.join(__dirname, '../../'),
  },
  • 이렇게 하면 nx build <프로젝트명> 했을 때 apps/프로젝트/.next 에 standalone 폴더가 생성된다.

  • 최상단의 nx.json 에 build가 캐싱되도록 설정을 해준다.

 "targetDefaults": {
    "build": {
      "cache": true
    }
  },
  • 최상단에 docker-compose.yml 파일을 생성한다.
version: '3'

services:
  nx-app-base:
    restart: always
    build:
      context: .
      dockerfile: ./apps/<프로젝트명>/Dockerfile
    environment:
      - DEV_PLATFORM=DOCKER
    ports:
      - '3000:3000'
  • 최상단에서 build를 먼저 진행한다. nx build <프로젝트명>

  • 그리고 docker-compose up -d 명령어를 치면 컨테이너가 생성된다!

  • docker desktop에서 확인했을때 잘 돌아갔다.

  • 근데 build를 수동으로 해줘야해서.. 나중에 github actions로 build후 docker 이미지를 뽑을수있게 세팅을 해봐야겠다!

5. Github Actions 설정

사실 이 action은 lint검사하고 build하는것밖에 없다.
나중에 백엔드 개발자분께 AWS연결이랑 Docker container도 연결해달라고 해보려고 한다!

여기서 중요한게 nx cloud를 안쓸거면 NX_NO_CLOUD 환경변수를 true로 설정해야한다.

ci.yml 코드
  name: CI

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  actions: read
  contents: read

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # This enables task distribution via Nx Cloud
      # Run this command as early as possible, before dependencies are installed
      # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
      # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      # Cache node_modules
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      #- run: npm ci --legacy-peer-deps
      - run: pnpm install --frozen-lockfile
      - uses: nrwl/nx-set-shas@v4

      # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
      # - run: npx nx-cloud record -- echo Hello World
      # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
      - run: pnpm exec nx affected -t lint build
        env:
          NX_NO_CLOUD: true

이렇게해서 프로젝트를 세팅해봤는데, 모노레포를 처음해보다보니 생각보다 시간이 많이 걸렸다.
그래도 넘 뿌듯하당 😃

0개의 댓글