
여러 비슷한 성격의 비즈니스 서비스를 한 레포지토리에 넣기위해 모노레포로 구성하기로 했다.
모노레포의 장점은 Eslint, Prettier를 root에서 관리하고 각 앱들에 적용시킬수 있다는 점이다.
또, 공통된 CI/CD를 가져갈수있어서 계속 똑같은 코드를 여러 저장소에 복붙하지 않아도 되서 좋은것같다.👍
지금은 Nx monorepo 에 Next.js앱을 1개 생성한 상태이다.
npx create-nx-workspace@latest --preset=next
기존 서비스는 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" 를 추가한다..eslintrc.json 파일 변환eslint가 업데이트가 되면서 eslint.config.js로 변환이 필요했다.
기존에 있는 json으로 변환이 가능해서 아래의 명령어를 입력해서 변환을 했다.
nx g @nx/eslint:convert-to-flat-config
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'] },
]
기존에 .prettierrc 에 확장자가 없어서 인식이 안됐었다.
.prettierrc.json 으로 확장자를 추가하니, 잘 세팅되었다.
tsconfig.json은 기존에 쓰던 파일이 그대로 적용되었는데도 잘돼서 세팅할필요가 없었다.
커밋할때 lint검사를 위해 husky를 추가했다.
pnpm add husky lint-staged
"scripts": {
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
npx lint-staged --concurrent false --relativelint-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(',')}`,
],
}
사실 Docker라곤 전에 튜링의 사과에서1시간 실습해본 경험밖에 없어서, 여러 문서를 복붙해서 만들어봤는데 컨테이너가 생성되가지고 깜짝놀랬다.
nx container라는 라이브러리가 있어서 해당 라이브러리로 Dockerfile을 구성해봤다.
@nx-tools/nx-container
위의 docs대로 하면 Dockerfile이 생성되고, apps/하위의 project.json 에 container라는 값이생긴다.
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 이미지를 뽑을수있게 세팅을 해봐야겠다!
사실 이 action은 lint검사하고 build하는것밖에 없다.
나중에 백엔드 개발자분께 AWS연결이랑 Docker container도 연결해달라고 해보려고 한다!
여기서 중요한게 nx cloud를 안쓸거면 NX_NO_CLOUD 환경변수를 true로 설정해야한다.
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
이렇게해서 프로젝트를 세팅해봤는데, 모노레포를 처음해보다보니 생각보다 시간이 많이 걸렸다.
그래도 넘 뿌듯하당 😃