본문 바로가기
개발

monorepo프로젝트 docker build하기

by 박연호의 개발 블로그 2025. 2. 27.

사내에서 택배사 개발을 하면서 여러 개선 사항이 보였고 업무 + 개인 프로젝트의 연장선으로 택배 repo에 모노레포로 전환과 docker image로 build한 경험을 공유하고자 합니다. 사실 해왔던 것처럼 계속 개발 할 수가 있지만 택배사 구조를 한번 잡고 갈 필요가 있다고 생각하여 전환작업을 진행했습니다.


도입 배경

제가 입사했을 때의 택배사는 monorepo를 시도하려다 멈춘 느낌의 폴더 구조였습니다. pacakge/ 폴더 아래 cj, lotte, ..등의 택배사가 존재하였고 그 아래 서비스 코드와 node_modules, Dockerfile이 있었습니다. 사실상 package/ 폴더 아래의 택배사 폴더를 하나의 repo로 구분해도 전혀 이상하지 않은 구조였습니다.

 

이 구조에서 새로운 택배사가 추가해야 할때마다 핵심 비지니스 코드를 제외한 log, middleware, error, type 등의 중복코드는 대부분 기존의 코드를 가져와 사용했습니다. 또한 새로운 코드와 기존 모듈의 호환성 문제, 택배사 마다 일관되지 않은 요청/응답 타입으로 코드를 작성할 때마다 불안함을 느꼈습니다.


Lerna 적용

 

monorepo를 도와주는 tool로 Turborepo, Nx, Lerna가 있으며, 저희 택배사 repo의 크기가 작고 npm workspace와 호환이 좋은 Lerna를 사용했습니다. 

 

먼저 monorepo로 전환하면서 필요했던 기능은 아래와 같습니다.

 

1. 공통된 모듈(유틸함수와 타입)을 한 폴더에서 관리하며, 비지니스 코드에서는 가져와 사용하기만 한다.

2. npm module의 버전관리는 한 곳에서 한다.

3. docker image build를 할 수 있어야 한다.

 

 

npm install -g lerna 명령어로 lerna를 설치해 줍니다. 이후 lerna 프로젝트 초기 설정을 해줍니다.

lerna init

 

lerna init을 실행하면 아래 파일들이 자동으로 생성됩니다.  package.json의 workspace에는 lerna에서 관리할 프로젝트가 정의되어 있습니다. 

 

lerna 프로젝트를 만들었으니, lerna create 명령어로 lerna에서 관리 대상이 되는 package를 생성하겠습니다. monorepo의 구조에 따라 package는 서버 또는 프론트 패키지가 됩니다.

 

이 글에서 간단한 예시로, utils에서 생성한 함수를 server에서 호출해보겠습니다. 

lerna create server
lerna create utils

 

 

lerna create 명령어를 실행하니, packages/에 server와 utils 폴더가 생겼습니다. 이 폴더를 사용하여 우리가 만들려고하는 서버, 또는 프론트 코드, 유틸함수/공통 타입 모음집 등...으로 사용할 수 있습니다.

 

 

먼저 packages/server, pacakges/utils에 tsconfig.json를 생성하며, build한 파일이 dist/라는 경로에 생성됩니다. 

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "noImplicitAny": false,
    "removeComments": true,
    "noLib": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "ESNext",
    "sourceMap": true,
    "lib": ["ESNext"],
    "outDir": "./dist"
  },
  "exclude": ["node_modules"],
  "include": ["./src"]
}

 

 

이제 utils 에 간단한 함수를 만들어 줍니다. 이 함수를 packages/server에서 사용할 예정입니다.

export const add = (num1: number, num2: number) => num1 + num2;

 

그리고 utils/package.json에서 name와 main을 수정해야 합니다.

name은 package/utils의 모듈명으로  package/server에서 import { add } from "@utils/sdk"로 사용할 수 있도록 합니다.

main은 "@utils/sdk" 모듈의 진입점이며 tsconfig.json에서 outDir의 경로가 됩니다. 

 

 

 

 

이제 packages/server/src/index.ts에서 add함수를 사용해 보겠습니다. 물론 실행되면 모듈을 찾을 수 없어 에러가 발생합니다.

 

@utils/sdk 모듈을 packages/server에서 사용할 수 있도록 아래 명령어를 추가하도록 하겠습니다.

npm install @utils/sdk -w packages/server

위 명령어를 입력하면 packages/server/packages.json에 @utils/sdk 모듈이 추가되고, node_modules에 @utils/sdk가 생성됩니다.

 

이후 packages/server/src/index.ts의 에러가 사라지게 되고 정상 실행됩니다.

 

만약 새로운 utils에 새로운 함수를 추가하려면 어떻게 해야 할까요 ? 새로 생성한 코드가 node_modules/@utils/sdk에 존재해야 합니다. 그러기 위해서는 packages/utils/src/index.ts에 아래처럼 코드를 수정하고 lerna run build를 해줍니다.

export const add = (num1: number, num2: number) => num1 + num2;
export const log = (str: string) => console.log(str);

 

 

lerna run build를 하면 루트의 package.json의 workspace 내부에 있는 package들의 package.json을 돌면서 script의 build 명령어를 실행합니다. build 명령어가 실행되면서, log 함수가 node_modules/@utils/sdk에 추가되어 server에서 사용할 수 있게 됩니다.


Docker 이미지 만들기

결국에 이렇게 만든 모노레포 프로젝트는 Docker image로 만들 수 있어야 합니다. 

 

docker build할 때, 서버 패키지와 utils 패캐지를 모두 copy한 후, lerna run build를 하게 되면, 각 패키지가 build되며 생성된 node_modules에 @utils/sdk가 생성됩니다. 

FROM node:20-alpine

WORKDIR /usr/src/app

COPY package*.json .
COPY lerna.json .

COPY packages/server packages/server
COPY packages/utils packages/utils

RUN npm install

RUN npx lerna run build

CMD node packages/server/dist/index