본문 바로가기

pnpm을 이용한 모노레포 마이그레이션

pnpm을 이용한 모노레포 마이그레이션

모노레포를 적용하게 된 계기 

 

기존 라이브러리 성능 문제

 

일전에 Notion DB로 사이트를 구축하면서 'react-notion-x' 라이브러리를 사용하였다. 이후 nextjs에 조금 더 확장되어 기능과 최적화 등이 고도화된 다른 라이브러리인 'nextjs-starter-kit'이 있었지만 전자의 라이브러리를 선택하게된 이유는 

 

(1) 노션 API로 인증 받는 코드를 nextjs로 직접 짜고 싶어서 

(2) 멀티 탭 데이터베이스(동일한 데이터를 키워드별, 날짜별 등으로 분류 가능) 기능이 있어서

 

였다.  

 

https://doyu-l.tistory.com/644

 

NotionAPI로 블로그 만들기 (3) Next.js App Router 미들웨어로 redirection 설정하기

Next.js의 App Router에서의 react-notion-x 라이브러리 적용기입니다. + 다국적 언어 지원 라이브러리인 react-i18next도 함께 사용하고 있습니다. 문제상황 react-notion-x는 상세페이지 클릭시 자동으로 /{pageId

doyu-l.tistory.com

 

하지만 버전 1을 완성하고, lighthouse 등에서 성능을 분석해보니 성능 최적화를 해야 할 필요성이 있었다.

 

예를 들면 이미지 최적화가 되어 있지 않아서 10px짜리 아이콘도 원래 크기 그대로 payload로 전달이 되고 있는 등(next/image 등 최적화 기능이 코드는 들어가 있으나 라이브러리 이슈로 적용이 안 되었다) 결론적으로는 해당 라이브러리를 성능 문제로 이 이상 쓸 수 없게 된 것이다.

 

문제를 인지한 이상 흐린 눈을 할 수 없기에 어쩔 수 없이 바로 리팩토링 작업에 들어갔고 두 번째로 고려했던 nextjs-starter-kit 라이브러리로 마이그레이션하기로 결정했다.

 

두 가지의 프로젝트

 

해당 라이브러리는 사용방법이 굉장히 간단했는데, 레포지토리를 그대로 클론해와서 root 파일에 존재하는 'pageId' 변수에 나의 페이지 Id 값으로 갈아끼우고 그냥 배포하면 되었다. 

 

방대한 코드

 

물론, 그냥 그대로 노션으로 블로그 배포만을 목적으로 한다면 저렇게 하면 되었지만, 나의 경우에는 기존에 내가 구축한 사이트에 기능의 일부로 노션 페이지가 필요했다. 전체적인 사이트에서 일관적인 디자인과 기능, 폰트나 Footer와 같은 공통 컴포넌트 등이 그대로 적용되어야 할 여지가 있었던 것이다. 

 

모노레포 체제가 필요하다

 

다시 말해, 나의 프로젝트는 모노레포 체제로 마이그레이션해야 할 필요성이 생겼다.

 

모노레포란? : 버전 관리 시스템에서 두 개 이상의 프로젝트 코드가 동일한 저장소에 저장되는 소프트웨어 개발 전략이며 모노레포 산하의 프로젝트들은 의존성이 존재하거나 같은 제품군이거나 하는 등의 정의된 관계가 존재한다. 

 

만약, 내가 두 프로젝트를 멀티레포로 관리했다면 실제 작업 절차는 아래와 같았을 것이다.

 

  (1) 기존 A 저장소 일단 냅두고, 새로운 B 저장소 만들고 환경 세팅(tsconfig 등)해주기 (중복 코드 발생)

  (2) B 저장소에 공통 디자인, 공통 컴포넌트 폴더, 라이브러리를 A로부터 복사~붙여넣기 및 다운로드 (중복 코드 발생)

  (3) vercel 기준으로 B 저장소 기준으로 배포 

  (4) 각 코드 에디터를 따로 열어서 코드 수정, 기능 추가, 빌드, 테스트, 배포 관리
       (pnpm dev, pnpm build 등 따로 따로)

 

하지만 모노레포로 관리했기에 아래와 같이 작업을 할 수 있었다.

 

  (1) 기존 A 저장소를 모노레포로 마이그레이션 하면서 A 패키지로 전환, B 패키지와 공통 패키지 생성 및 환경 세팅하기 (중복 코드 아님)

  (2) 공통 패키지에 A와 B 공통 코드들을 갖다넣고 필요한 곳에 import 시키기 (중복 코드 아님)

  (3) vercel 기준으로 A 패키지 폴더, B 패키지 폴더 기준으로 배포 설정 수정 
      -> 한 브랜치에 어느 패키지든 update가 감지되면 두 프로젝트 자동 배포

      -> 물론 코드의 변화가 있는 프로젝트만 빌드/ 테스트 설정도 가능

  (4) 코드 에디터 하나로 관리 
       (pnpm dev, pnpm notion dev 등 패키지 전부 또는 특정 패키지만 명령어 입력 가능)

 

물론 모노레포와 멀티레포의 장단점이 분명히 존재하지만, 내 프로젝트에서는 중복 코드가 최소화되고 유지 보수가 쉬워진다는 점에서 선택하지 않을 이유가 없었다.

 

 

모노레포로의 마이그레이션

 

패키지 구조

 

우선 프로젝트는 pnpm으로 관리하고 있었기에 일관적으로 pnpm을 이용하여 모노레포 시스템을 구축하기로 했다. 또한 프로젝트 크기가 크지 않아서 turborepo 등의 전문적인 모노레포 관리를 위한 툴 사용은 다음을 기약하며 우선은 pnpm을 사용해보았다.

 

구축하려는 폴더 구조는 아래와 같다.

 

3개의 패키지 체제
packages
|--- core // (블로그 코드)
|    |--- src
|    |    |--- app // (페이지)
|    |     ...
|    |--- public
|    |    ...
|--- notion // (nextjs-notion 라이브러리 코드)
|    |--- pages (페이지)
|    |--- site.config.js // 노션 연동 설정
|--- shared // (css, 컴포넌트 등 공유 코드)
|    |--- src

 

  • core은 기존 코드가 들어있는 패키지이다. (nextjs 13버전의 app router)
  • notion은 처음에 언급한 nextjs-notion-starter-kit 라이브러리 코드가 통으로 들어간다. (nextjs 12버전의 pages router)
  • shared는 위의 두 패키지에서 공통으로 사용되는 코드들이 있는 패키지이다.

 

루트 폴더 세팅

 

우선 루트 폴더에서 각 3개의 패키지를 사용하기 위한 세팅을 해준다.

 

pnpm-workspace.yaml
  • 파일을 따로 생성하기 싫다면 package.json에 같이 적어주어도 된다. 
  • workspace : packages 폴더 기준으로 모노레포 시스템을 사용할 것임을 명시
packages:
  - 'packages/*'

 

package.json
  • packageManager : pnpm을 활용함을 명시
// 필요한 항목만 남겨두었다.

{
  "name": "mono-repo",
  "packageManager": "pnpm@8.6.2",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    
    // 각 패키지를 개별적으로 실행시켜주는 명령어 (f는 filter의 약자)
    // 'pnpm core dev' 하면 core만 개발 환경에서 실행
    // 뒤에 있는 각 패키지 이름은 각 패키지의 package.json에서 설정
    "shared": "pnpm -F @mono-repo/shared",
    "core": "pnpm -F @mono-repo/core",  
    "notion": "pnpm -F @mono-repo/notion",
    
    // 모든 패키지를 동시에 실행시켜주는 명령어 (r는 recursive의 약자)
    // 'pnpm dev'하면 3가지 패키지가 모두 실행
    "dev": "pnpm -r dev",
    "build": "pnpm -r build"
  },
  
  // 만약 workspaces 파일을 굳이 생성하기 싫다면 아래와 같이 적어도 된다.
  "workspaces": ["packages/*"],
  "license": "ISC",
}

 

pnpm build, pnpm dev를 하게 되면 아래와 같이 3개의 패키지가 모두 빌드에 들어간다.

 

pnpm build 

 

pnpm dev 

 

tsconfig.options.json

tsconfig.options.json을 만들어서 compilerOptions만 넣어두었다.

  • baseUrl : paths, references 설정을 위해 필수
  • paths : package.json에 @mono-repo/notion 등 별칭을 지어놔도 paths를 참고하여 해당 이름을 가진 패키지를 찾아간다.
  • references(바로 아래 tsconfig.json에 있음) 
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "incremental": true,
    "noEmitOnError": true,
    "skipLibCheck": true,

    // ECMAScript interoperability
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "target": "esnext",

    "rootDir": "./",
    "baseUrl": ".", 

    "skipDefaultLibCheck": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,

    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "resolveJsonModule": true,
    "jsx": "preserve",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "core": ["./packages/core"],
      "notion": ["./packages/notion"],
      "shared": ["./packages/shared"],
    },
  }
}

 

tsconfig.json
  • references : 각 하위 패키지에서 루트 tsconfig 속성을 가져오기 위해 각 패키지의 tsconfig가 들어있는 경로를 인식하게 해준다.
{
  "extends": "./tsconfig.options.json",
  "files": [],
  "references": [
    {
      "path": "packages/core"
    },
    {
      "path": "packages/notion"
    },
    {
      "path": "packages/shared"
    }
  ],
  "include": ["**/*.ts", "**/*.tsx", ".eslintrc.js"],
  "exclude": ["node_modules", "dist"]
}

 

.eslintrc.js
  • 기본적으로 통일된 코드 스타일을 위해 prettier와 ESLint를 전역에 설정하는 것은 특별한 것이 없기 때문에 생략하겠다. 
  • parserOptions -> project : 타입 스크립트와 싱크를 맞추기 위함 
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
    project: './tsconfig.json',
  },
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  settings: {
    react: {
      version: 'detect',
    },
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts', '.tsx'],
      paths: ['src'],
    },
    'import/resolver': {
      node: {
        extensions: ['.ts', '.tsx'],
      },
      typescript: {},
    },
  },
  .
  .
  .

 

루트에서는 이 정도 설정을 해주었다.

 

공용 패키지 사용하기 위한 설정

 

우선 내 경우에는 공용 패키지인 shared에서 Footer 컴포넌트를 notion 패키지로 절대 경로를 사용해서 import를 해오고자 했다. 

(여기서 엄청난 삽질이 시작된다.)

 

참고로 다른 레퍼런스를 훑어보면 보통은 공통 패키지를 절대경로로 설정해서 가져오는 것이 아니라, 다른 패키지의 package.json의 dependencies에 공통 패키지를 추가하여 설치 후에 import해서 사용한다고 한다. (또르륵... 나 뭐한거지?)

(관련하여 설명이 되어있는 블로그 링크)

 

결과

notion 패키지에서 shared 패키지에 있는 Footer 컴포넌트를 alias 경로로 가져오는 모습

 

1-1. shared 패키지 설정 

 

package.json
  • package.json은 패키지 이름만 설정해주면 된다.
{
  "name": "@mono-repo/shared", // 패키지 이름 루트 packages.json script와 일치시키기
.
.
.
// 아래는 참고
  "dependencies": {
    "react": "^18",
    "react-dom": "^18",
    "next": "13.5.6"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "13.5.6"
  }
}

 

1-2. shared transpile 설정 (App router)

 

트랜스파일(transpile) 설정을 하지 않고 바로 다른 패키지의 코드를 import하면 브라우저에 아래와 같은 에러 메시지가 출력이 된다.

Module parse failed: Unexpected token (1:21)
You may need an appropriate loader to handle this file type,

 

transpile(트랜스파일) : 다른 언어 등으로 구성된 모듈을 호환 가능한 JavaScript 버전으로 변환
Bundle(번들) : 최적화된 파일로 결합

-> 즉 종속성이 있는 다른 패키지 코드를 가져올 때 트랜스파일 및 번들링을 처리해야 함! 

 

shared/next.config.js
  • nextjs App Router에서는 아래와 같이 모노레포에서 트랜스파일과 번들 의존성을 자동으로 지원해준다.
  • 예전에는 next-transpile-modules 라이브러리를 다운 받아서 사용해야 했다
    (아래 notion 패키지는 nextjs 12버전 pages router이기 때문에 설정을 예전 방식으로 진행했다.)
/** @type {import('next').NextConfig} */
const path = require('path');

const nextConfig = {
  // nextjs 13 이상부터 아래와 같이 설정하면 끝! 
  transpilePackages: ['@mono-repo/notion', '@mono-repo/core'],
  
  // 이 부분은 scss 공용 스타일 파일 경로 단순화를 위한 설정이다. 
  sassOptions: {
    includePaths: [path.join(__dirname, 'src/styles/base')],
  }
}

module.exports = nextConfig

 

shared/tsconfig.json
  • extends : 부모 tsconfig.json 내용에서 확장시키겠다는 의미 
  • composite : referenced, 즉 참조된 패키지는 이 속성이 true여야 한다.
    • 이 프로젝트가 모노레포의 일부 패키지라는 것을 타입스크립트 컴파일러에게 알려주는 역할
    • rootDir 속성이 따로 선언되어 있지 않다면 tsconfig 파일이 있는 위치가 기준이 된다.
    • declaration 역시 true여야 한다. 
  • paths : alias 경로 설정
  • references : paths에 관련된 패키지 경로 참조 
  • noEmit : false여야 한다.
    • 참조 프로젝트에서 이 프로젝트의 TypeScript 코드를 사용할 때 컴파일된 JavaScript 파일을 생성할 수 있도록 해준다.
  • outDir : './dist'라고 설정해야 해당 패키지 말고 모든 패키지의 코드를 컴파일하지 않는다.
  • declarationMap : 내가 잘 쓰는 'go to definition'을 쓰기 위해서는 true여야 한다.
{
  "extends": "../../tsconfig.json",  
  "compilerOptions": {
    "composite": true,
    "noEmit": false,  // false를 해주지 않으면 에러가 뜬다
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
    "allowSyntheticDefaultImports": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ],
      "@mono-repo/shared/*": [  // alias 경로 설정 
        "../shared/src/*"
      ],
    }
  },
  "references": [
    { "path": "../shared" },    // 꼭!!! 넣어줘야 한다.
  ],
  "include": [
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
  ],
  "exclude": [
    "node_modules"
  ]
}

 

2. notion transpile 설정 (Pages router)

  • 똑같이 트랜스파일 설정을 해주었다.
  • 예전 방식대로 next-transpile-modules 라이브러리를 다운 받아서 각 패키지의 경로를 추가하여 세팅해주면 된다.
  • 나의 경우 다양한 라이브러리로 세팅을 해주고 있었기 때문에 withPlugins를 이용하여 각 세팅을 결합해주었다. 
/* eslint-disable @typescript-eslint/no-var-requires */

// 여러 모듈을 통한 멀티 세팅이 필요할 때 withPlugins 사용
const withPlugins = require('next-compose-plugins');
const withBundleAnalyzer = require('@next/bundle-analyzer');

// 트랜스파일할 패키지 경로 
const withTM = require('next-transpile-modules')(["../shared"], ["../core"]);

const nextConfig = {
// 필요한 설정들 넣기
.
.
.
};


// 여러 모듈을 기반으로 한 설정이 필요할 때 아래와 같이 설정하면 된다.
module.exports = withPlugins([
  [withBundleAnalyzer, {
  enabled: process.env.ANALYZE === 'true',
}], 
  [withTM]
 ] , nextConfig)

 

배포 설정

 

아래와 같이 기존에 있던 프로젝트는 core 패키지로 마이그레이션 되었으므로 Root Directory를 해당 폴더로 다시 지정해준다. 배포가 필요한 다른 패키지도 마찬가지로 배포 설정을 해당 폴더를 기준으로 배포해준다.

 

나 같은 경우는 기존 배포 브랜치는 'main'이었지만, 코드는 그대로 놔두고 새롭게 'mono-repo/main' 브랜치를 만들어서 해당 브랜치를 기준으로 두 패키지를 배포했다.

 

vercel에서 배포 브랜치 변경

 

기존 프로젝트 배포될 폴더 수정

 

노션 패키지 새로 배포

 

 

결과 

아래와 같이 메인 페이지, 상세 페이지 전부 초록초록한 것을 확인할 수 있다.

우선 라이브러리로 인한 성능 저하는 해결했으므로 다음은 기존 프로젝트에서의 성능 최적화 작업을 진행할 예정이다.

 

수치로 보는 최적화된 성능

 

 

프로젝트 일부로 노션 패키지 화면 삽입 + 아래 공통 패키지에서 가져온 Footer 컴포넌트

 

트러블 슈팅 

 

이 모든 트러블 슈팅은 위에서 언급한 것처럼 패키지들 사이에서도 alias로 파일들을 가져오기 위한 눈물겨운 사투이다... 

하지만, 보통은 공통 패키지를 다른 패키지의 package.json의 dependencies에 추가하여 사용한다고 한다. (또르륵...)

 

1. eslintrc.js : 경로 오류

 

 notion 패키지에서 shared 패키지의 Footer 컴포넌트를 import 할때 이슈가 생겼다.

  • 파일의 경로를 읽지 못하는 것..! 
Unable to resolve path to module

 

관련 키워드로 찾아본 스택오버플로우 질문글에는 무려 20가지의 답변이 제시되어 있었다....

 

다양한 답변들

 

하나, 하나 다 시도해보았고 에러는 사라질 기미가 보이지 않았다... 

하지만, 해당 스택오버플로우의 3번째 답변글의 솔루션을 적용했을 때!! 드디어 빨간 줄이 사라지는 것을 볼 수 있었다.

  • 즉, tsconfig.json에서 설정한 alias 경로를 적용하기 위해 eslintrc에 아래와 같은 설정을 추가해주면 경로를 로드해올 수 있다.

 

tsconfig.js
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
    project: './tsconfig.json',
  },
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  settings: {
    react: {
      version: 'detect',
    },
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts', '.tsx'],
      paths: ['src'],
    },
    'import/resolver': {
      node: {
        extensions: ['.ts', '.tsx'],
      },
      typescript: {},
    },

 

2. 애증의 reference

 

  • 또!! notion 패키지에서 shared 패키지의 Footer 컴포넌트를 import 할때 이슈가 생겼다. 😂😂
    • '파일이 tsconfig.json 리스트에 존재하지 않아 - 오류가 뜸'
    • 오류 문구에 tsconfig의 include에 해당 파일을 넣으라고 함 
File '경로/packages/패키지명/.../파일.ts' is not in project file list. Projects must list all files or use an 'include' pattern.

 

문제의 오류 메시지

 

시키는 대로 tsconfig의 include에 넣었음

  • 다른 패키지의 파일을 해당 패키지 tsconfig 파일에 include 시켰기 때문에 타입 검사를 쫙 하고 당연히 해당 패키지 기준으로 파일 경로가 엉망이기 때문에 에러가 남..

 

아 어쩌란 말이냐~~ ㅠㅠㅠ 

하지만 아래와 같이 include는 가만히 놔두고, reference 항목에 다른 패키지들의 path를 추가함으로써 오류는 해결했다.

  • shared 패키지에 의존성을 가지고 있는 notion 패키지의 tsconfig.json에 shared에 대한 레퍼런스를 추가해줘야 컴파일이 된다.

즉, 오류 안내 메시지를 100% 믿을 수 없다는 것...! 

 

참고

 

모노레포란?

https://d2.naver.com/helloworld/0923884 

https://sion-log.vercel.app/toss-monorepo 

https://velog.io/@dbwjd5864/%EC%9A%B0%EC%95%84%EC%BD%98-2022-%EC%9A%B0%EB%A6%AC%EB%8A%94-%ED%95%98%EB%82%98%EB%8B%A4-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-with-pnpm

 

모노레포 구축하기

 

추천 : https://moonrepo.dev/docs/guides/javascript/typescript-project-refs#tsconfigoptionsjson

  • (설정이 자세해서 패키지 구축은 문서를 보면서 하는 것을 추천한다!)

(추천 : turborepo 코드 설명) https://dev-scratch.tistory.com/161

(yarn) https://9yujin.tistory.com/101 

 

+ TSconfig 전반적인 설정 도움되었던 글

https://valcker.medium.com/configuring-typescript-monorepo-with-eslint-prettier-and-webstorm-61a71f218104

 

트랜스파일 

https://blog.smilecat.dev/posts/next-transpile-modules

 

yarn으로 모노레포 구축 

https://blog.logrocket.com/build-monorepo-next-js/

 

pnpm으로 모노레포 구축 

https://bepyan.github.io/blog/dev-setting/pnpm-monorepo  

 

eslintrc.js 트러블 슈팅 

https://stackoverflow.com/questions/55198502/using-eslint-with-typescript-unable-to-resolve-path-to-module

https://lightrun.com/answers/johvin-eslint-import-resolver-alias-nextjs-app-with-absolute-path---getting-eslint-unable-to-resolve-path-to-module-error

 

tsconfig.json의 reference 트러블 슈팅 

https://deptno.github.io/posts/2018/typescript-monorepo/ 

https://maxrohde.com/2021/09/30/typescript-monorepo-with-yarn-and-project-references  

 

nextjs 트랜스파일 공식문서

https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages

 

타입스크립트 TSConfig

 

타입스크립트 references 속성 외 

https://www.typescriptlang.org/docs/handbook/project-references.html  

 

 

모노레포 샘플 프로젝트

(turborepo) https://github.com/kidow/kidorepo 

(yarn) https://github.com/goldstack 

728x90
⬆︎