모노레포 전환기 (FE: React, BE: NestJS)
프론트와 백엔드 간 파일 공유
main
date
May 2, 2024
slug
fe-be-monorepo-통합
author
status
Public
tags
TurboRepo
React
NestJS
모노레포
summary
프론트와 백엔드 간 파일 공유
type
Post
thumbnail
category
Development
updatedAt
Apr 14, 2026 09:38 PM
팀 내의 프로젝트의 경우 프론트와 백엔드를 하나의 레포지토리에서 관리하는 멀티 레포 방식으로 구성되어있다.
프론트와 백엔드 둘 다 타입스크립트 기반이라 매 번 동일한 코드를 작성하는데 (인터페이스, 유틸 함수 등등…) 불편함을 느끼고 있을 때 팀장님께서 인터페이스만이라도 공유해서 사용해보고 싶은데 방법이 없나 라는 이야기를 하셨을 때 어떻게 해야하나.. 고민을 많이 했던 것 같다.
사실 이런 고민은 예전부터 하고 있었다. 관련해서 찾아보면 모노레포 아키텍처 관련한 내용을 찾아볼 수 있었는데 대부분의 예제가 프론트는 프론트, 백엔드는 백엔드로만 구성되어있어 내가 하려는 방법이 옳지 않은 방법인가 라는 생각을 많이 했던 것 같다.
프론트와 백엔드 간의 관심사도 다르고, 프론트 팀과 백엔드 팀이 나뉘어 있으니 저런 것일 테니 그런 것일 테고 우리 회사는 풀스택을 지향하니.. 한번 시도해보자 라는 생각으로 모노레포 관련하여 레퍼런스를 찾아보니까 완전 없는 건 아니였다. 아! 물론 기술 스택이 프론트, 백엔드 둘 다 Typescript 기반이니 가능한 일~!
모노레포 관련해서 찾아보니 관련 셋업을 도와주는 툴들이 다양하게 있었다. (Yarn, Lerna,Turborepo 등등..)
어떤 툴을 사용할지 고민을 하긴 했지만 엄청 오래 걸리지는 않았다.
Truborepo가 기본적으로 Typescript를 사용한다고 하니 그럼 이거 쓰면 되잖아..??? 이렇게 되어버렸다😉
Turborepo 셋업 관련해서 찾아보면서 기존 프로젝트를 옮겨 나갔다.
그렇게 완성된 프로젝트 구조

apps 는 주요 프로젝트 (프론트, 백엔드)
packages는 apps 프로젝트에서 공유할 파일들이 들어간다.

api-interface 패키지는 프론트와 백엔드간 공유할 interface, enum, type 등 관련한 파일들로 구성되어있고

utility는 공유 함수들이 들어있다.
모노레포를 도입하기로 한 주된 목적은 우선 프론트와 백엔드 간의 인터페이스 공유이다. Request, Response 하는 데이터 타입은 프론트와 백엔드가 일치하므로 인터페이스를 공유할 수 있을 것 같았다.
하지만 백엔드에서는 dto 체크를 위해 class-validator를 사용한 class 기반 dto를 사용하고 프론트는 인터페이스 만으로 타입 체크를 하고 있는데 이 둘을 어떻게 일치 시킬지 고민이 되었다.
- 프론트와 백엔드 둘 다 class 기반으로 타입을 체크한다.
- 인터페이스를 작성하고 프론트에서는 해당 인터페이스를 사용하고, 백엔드에서는 해당 인터페이스를 구현 받아 class-validator 기반으로 dto를 재 정의한다.
첫 번째 class 기반으로 타입 체크는 어떤 식으로 접근을 해야 하는지 감이 잡히지 않아 급 한대로 2번을 선택하였다..
(프론트에서는 클래스 인스턴스를 생성하고 해당 인스턴스를 state로 넣는 방식일까..??? 그렇다면 백엔드는?? 어쨋든 class-validator로 재정의를 해야하는데?)
@bizchat/api-interfaces
@bizchat/api-interface 이름으로 뽑아서 사용 하면 된다.
// package.json { "name": "@bizchat/api-interface", "version": "0.0.34", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": "./dist/index.js" }, "dependencies": { "typescript": "latest" }, "scripts": { "build": "tsc", "dev": "tsc --watch", "prod": "tsc" } }
package.json에 정의한 name으로 외부에서 호출한다.
프론트에서는 인터페이스를 백엔드에서 전달 받는 값을 체크하고,
import { axiosInstance } from "@libs/axios/axiosInstance.tsx"; import { ProjectState } from "@bizchat/api-interface"; // 내가 만든 패키지 파일 import { AxiosResponseData, axiosResponseData, } from "@libs/axios/axiosResponse.ts"; import { AxiosRequestConfig, AxiosResponse } from "axios"; /** * @description 프로젝트 상세 조회 * @param {string} projectId 프로젝트 objectId */ export const findProjectApi = async ( projectId: string, ): Promise<ProjectState[] | null> => { const url = `${PROJECTS}/${projectId}`; const { data } = await axiosInstance.get< AxiosRequestConfig, AxiosResponse<AxiosResponseData<ProjectState[]>> >(url); return axiosResponseData<ProjectState[]>(data); };
백엔드는 위 인터페이스를 구현 받아서
class-validator 로 재 정의를 한다.import { ProjectState } from "@bizchat/api-interface"; import { IsString } from 'class-validator'; export class ProjectDto implements ProjectState { @IsString() title: string; // 이하 생략.. }
이렇게 하면 프론트와 백엔드 간 타입을 맞춰두고 만약 어느 한쪽의 값이 변경됐을 경우 다른 타입으로 인해 변경된 점을 쉽게 확인할 수 있다.
이렇게 작업을 해보면서 가장 큰 장점은 타입 정의를 한 명만 하면된다는 점인 것 같다.
이렇게 활용하기 전까지는 프론트 작업을 하는 사람이 타입을 정의, 백엔드 작업을 하는 사람도 타입을 정의.. 각각 타입을 정의 하다 보면 일치하지 않는 값도 생기고 다른 부분이 있다면 수정을 해야 했었다. 하지만 이 구조를 이용하게 되면 한 명이 타입을 정의하고 공유를 해주면 여러 사람이 같은 작업을 하지 않아도 된다.

@bizchat/utility

// package.json { "name": "@bizchat/utility", "version": "0.0.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": "./dist/index.js" }, "dependencies": { "typescript": "latest" }, "scripts": { "build": "tsc", "dev": "tsc --watch", "prod": "tsc" } }
유틸리티 함수들도 위 인터페이스와 사용법이 동일하다.
package.json name으로 명시한 이름으로 해당 파일을 뽑아 쓰면 된다.
import { groupBy } from '@bizchat/utility'; const sampleGrouping = (list) => { const group = groupBy(list, 'name'); }
⚠️ 이슈
Case 1. 패키지 수정
패키지를 수정할 경우 프론트와 백엔드 둘 다 영향을 줄 수 있기 때문에 조심해야한다.
단순히 한 곳을 위해 수정을 해야하는 상황이 발생한다면 공유 패키지를 사용하는 방법은 적합하지 않을 수 있다.
Case 2. Compiler Option - module
프론트의 경우 React, 백엔드는 NestJS 프로젝트로 구성되어있다.
각각의 tsconfig를 확인해보면
// 프론트 (React) { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", // ...이하 생략 }
// 백엔드 (NestJS) { "compilerOptions": { "module": "commonjs", // ...이하 생략 } }
서로의 컴파일 되는 module이 다른 것을 확인할 수 있다.
이 환경을 서로 동일하게 맞추어 줘야 한다.
이 이슈 관련하여 찾아보니 레퍼런스가 존재하여 쉽게 해결할 수 있었다.
프론트의 경우 Vite를 사용하고 있는데 관련한 플러그인을 다운받아 환경을 일치 시켜주었다.
npm install @rollup/plugin-commonjs --save-dev
rollup plugin을 다운 받고 vite.config.js에 등록해준다.
import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react-swc"; import tsconfigPaths from "vite-tsconfig-paths"; import commonjs from "@rollup/plugin-commonjs"; // <- 플러그인 설치 // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); return { define: { "process.env": env, }, plugins: [react(), tsconfigPaths()], build: { minify: false, sourcemap: true, rollupOptions: { // <- option 정의 output: { format: "umd", name: "bizchat", entryFileNames: "[name].js", chunkFileNames: "[name].js", assetFileNames: "[name].[ext]", }, plugins: [ // commonjs로 변경할 파일 선언 commonjs({ include: [/packages/], }), ], }, }, optimizeDeps: { include: ["@bizchat/api-interface/**/*", "@bizchat/utility/**/*"], }, }; });
위에서 이야기하지 않은 부분이 있는데 vite 환경에서 packages 파일을 사용하기 위해서는 vite.config.js에 해당 패키지들을 포함 시켜줘야한다.
// vite.config.js optimizeDeps: { include: ["@bizchat/api-interface/**/*", "@bizchat/utility/**/*"], }
Case 3. Rollup plugin
rollup plugin을 설치했을 때 윈도우 환경에서는 정상적으로 동작하였다. 하지만 m1 칩을 사용한 맥북에서는 프로젝트가 실행되지 않아 관련 내용을 찾아보다가 추가 플러그인을 사용해야 실행이 가능하다고 하여 추가적으로 아래 모듈을 설치하였다.
npm i @rollup/rollup-darwin-arm64




![[ETC] Monorepo - 개념](https://www.notion.so/image/https%3A%2F%2Fbeomy.github.io%2Fstatic%2Fdd214e6c1aab7e34210b9f0644856a40%2Ff635c%2Fmonorepo.png?table=block&id=719239d2-9021-4302-af00-a7fe637bd2e6&cache=v2)

