[What To Eat] React + Express 모노레포 프로젝트 세팅하기

My_Code·2025년 5월 29일

What To Eat

목록 보기
1/6

모노레포에 대한 궁금증이 생겨서 모노레포 형태의 프론트엔드+백엔드 프로젝트를 진행하려고 합니다.

프로젝트 개요

이번 프로젝트의 주제는 식사 메뉴 투표 커뮤니티 입니다. 일단 프로젝트 세팅에 대한 기술 스택은 다음과 같습니다.

프론트엔드: React, TypeScript, Vite, Tailwind CSS
백엔드: Node.js, Express, TypeScript
모노레포 도구: Turborepo

모노레포(Monorepo) 는 여러 프로젝트를 하나의 저장소에서 관리하는 방법입니다. 이번 포스트는 Turborepo를 사용하여 프론트엔드(React)와 백엔드(Express)를 함께 관리하는 TypeScript 기반 모노레포 프로젝트를 세팅하는 과정을 설명합니다.

모노레포 방식은 코드 재사용성을 높이고, 종속성 관리를 단순화하며, 프로젝트 간 일관성을 유지하는 데 도움이 됩니다. 이를 통해 개발 생산성을 크게 향상시킬 수 있다고 합니다.

Turborepo란? 공식 문서 설명에 따르면, JavaScript나 TypeScript 코드를 위해 최적화된 빌드 시스템이라고 한다. JavaScript와 TypeScript의 린트나 빌드, 테스트와 같은 코드베이스 작업은 시간이 꽤 소요되는 작업인데, Turborepo는 캐싱을 통해 로컬 설정을 진행하고 CI 속도를 높여준다.


초기 프로젝트 세팅

Turborepo 를 사용해 기본 모노레포 구조를 생성합니다. 이 명령어는 기본적인 파일 구조와 설정 파일을 자동으로 생성해 줍니다.

이번에는 apps에 있는 기본 docsweb은 지우고 frontendbackend로 바꿔서 직접 설정하려고 합니다.

# Turborepo 프로젝트 생성
npx create-turbo@latest

루트 package.json은 모노레포의 중앙 제어 센터 역할을 합니다. workspaces 설정을 통해 npm/yarn/pnpm이 하위 패키지들을 인식하고 관리할 수 있게 됩니다.

{
  "name": "what-to-eat",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "prettier": "^3.5.3",
    "turbo": "^2.5.3",
    "typescript": "5.8.2"
  },
  "packageManager": "pnpm@9.0.0",
  "engines": {
    "node": ">=18"
  }
}

turbo.json은 Turborepo의 핵심 설정 파일로, 각 작업(dev, build 등)의 의존성과 캐싱 규칙을 정의합니다. 이를 통해 빌드 시간을 단축하고 효율적인 워크플로우를 구성할 수 있습니다.

{
  "$schema": "https://turborepo.com/schema.json",
  "ui": "tui",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**", "apps/backend/dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {},
    "test": {}
  }
}

공유 패키지 설정

공유 패키지는 모노레포의 핵심 장점 중 하나입니다. 여러 앱에서 공통으로 사용되는 코드, 설정, 컴포넌트를 분리하여 일관성과 재사용성을 높입니다.

아직 어떻게 활용해야 할지 감이 오지 않지만 조금씩 구현하면서 활용할 방법을 생각해야겠습니다.

모든 프로젝트에서 일관된 TypeScript 설정을 사용하기 위한 공유 패키지를 생성합니다. 이를 통해 프로젝트 간 타입 정의의 일관성을 유지할 수 있습니다.

아래 명령어를 통해 모든 프로젝트에서 일관된 TypeScript 설정을 사용하기 위한 공유 패키지를 생성합니다. 이를 통해 프로젝트 간 타입 정의의 일관성을 유지할 수 있습니다.

mkdir -p packages/tsconfig
cd packages/tsconfig
npm init -y
// packages/tsconfig/package.json

{
  "name": "tsconfig",
  "version": "0.0.0",
  "private": true,
  "files": [
    "base.json",
    "react-app.json",
    "node-app.json"
  ]
}

모든 TypeScript 프로젝트의 기반이 되는 공통 설정으로, 기본적인 컴파일 옵션을 정의합니다.

// packages/tsconfig/base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "base settings",
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "exclude": ["node_modules"]
}

React 애플리케이션에 특화된 TypeScript 설정으로, JSX 지원과 DOM 타입을 포함합니다.

// packages/tsconfig/react-app.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "React app",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "jsx": "react-jsx",
    "noEmit": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  }
}

Node.js 백엔드에 적합한 TypeScript 설정으로, CommonJS 모듈 시스템과 소스맵 생성을 지원합니다.

// packages/tsconfig/node-app.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node app",
  "extends": "./base.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "sourceMap": true
  }
}

아래 명령을 통해 코드 품질과 일관성을 유지하기 위한 공유 ESLint 설정을 생성합니다. 이를 통해 모든 프로젝트에서 동일한 코딩 규칙을 적용할 수 있습니다.

mkdir -p packages/eslint-config
cd packages/eslint-config
npm init -y
npm install -D eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin
// packages/eslint-config/package.json

{
  "name": "@whattoeat/eslint-config",
  "version": "0.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "eslint-plugin-react": "^7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "@typescript-eslint/eslint-plugin": "^5.59.0",
    "@typescript-eslint/parser": "^5.59.0"
  }
}

React와 TypeScript에 적합한 ESLint 규칙을 정의합니다. 최신 React 문법(React 17+ JSX 변환)을 지원하도록 일부 규칙을 조정했습니다.

// packages/eslint-config/index.js:

module.exports = {
  extends: ["plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended"],
  parser: "@typescript-eslint/parser",
  settings: {
    react: {
      version: "detect",
    },
  },
  rules: {
    "react/react-in-jsx-scope": "off",
  },
};

프론트엔드 앱 설정

아래 명령어를 통해 Vite를 사용하는 React+TypeScript 프로젝트를 생성합니다. Vite는 빠른 개발 서버와 효율적인 빌드 도구를 제공하는 최신 프론트엔드 도구입니다.

cd apps
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install

공유 TypeScript 설정을 상속받고, 경로 별칭(@)을 설정하여 import 경로를 간소화합니다.

// apps/frontend/tsconfig.json

{
  "extends": "../../packages/tsconfig/react-app.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Vite 구성 파일에 대한 TypeScript 설정으로, 프로젝트 참조(Project References) 기능을 사용하기 위한 composite 설정이 포함됩니다.

// apps/frontend/tsconfig.node.json

{
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": false,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": false,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}

apps/frontend에서 사용할 컴포넌트와 Tailwind CSS에 대한 설정은 각자 원하는 형태로 설정하시면 됩니다. 저는 일단 백엔드부터 구현 후 그에 맞게 프론트를 구현할 계획입니다.


백엔드 앱 설정

Express 기반 백엔드를 위한 기본 패키지와 TypeScript 개발 환경을 설정합니다. cors 미들웨어는 프론트엔드와의 통신을 위해 필요합니다.

cd apps
mkdir backend
cd backend
npm init -y
npm install express cors cookie-parser bcrpyt jsonwebtoken
npm install -D typescript ts-node @types/express @types/node @types/cors @type/cookie-parser nodemon dotenv

공유 패키지에 있는 Node.js TypeScript 설정을 상속받고, 소스 코드 디렉토리를 지정합니다.

// apps/backend/tsconfig.json

{
  "extends": "../../packages/tsconfig/node-app.json",
  "compilerOptions": {
    "rootDir": "src"
  },
  "include": ["src/**/*"]
}

개발, 빌드, 실행을 위한 스크립트와 필요한 의존성을 정의합니다. nodemon은 개발 중 파일 변경을 감지하여 서버를 자동으로 재시작합니다. 아마도 bcrpyt, jsonwebtoken 타입 패키지 설치가 필요할 것 같긴한데, 사용할 때 추가로 설치하면 될 것 같습니다.

// apps/backend/package.json

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon --watch src --ext ts,js --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "lint": "eslint src --ext .ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "@prisma/client": "^6.8.2",
    "bcrypt": "^6.0.0",
    "cookie-parser": "^1.4.7",
    "cors": "^2.8.5",
    "express": "^5.1.0",
    "jsonwebtoken": "^9.0.2"
  },
  "devDependencies": {
    "@types/cookie-parser": "^1.4.8",
    "@types/cors": "^2.8.18",
    "@types/express": "^5.0.2",
    "@types/node": "^22.15.24",
    "dotenv": "^16.5.0",
    "nodemon": "^3.1.10",
    "prisma": "^6.8.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.8.3"
  }
}

기본적인 Express 서버로, CORS 설정과 JSON 파싱 미들웨어를 적용하고 바로 다음에 진행할 데이터베이스 연결을 위해 dotenv.config() 설정도 합니다.

// apps/backend/src/index.ts

import express, { NextFunction, Request, Response } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';

dotenv.config();

const app = express();
const SERVER_PORT = process.env.SERVER_PORT || 3000;

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(cookieParser());

// 라우트 정의
app.get('/', (req: Request, res: Response) => {
  res.json({ message: 'API 서버가 실행 중입니다.' });
});

app.listen(SERVER_PORT, () => {
  console.log(`서버가 포트 ${SERVER_PORT}에서 실행 중입니다`);
});

프로젝트 실행

Turborepo를 사용하면 하나의 명령어로 모든 앱과 패키지를 동시에 개발하고 빌드할 수 있어 개발 효율성이 향상됩니다. 일단 개발 환경에서 실행하기 위해 루트 디렉토리에서 아래와 같은 명령을 사용합니다.

npm run dev

트러블 슈팅

TypeScript Project References 오류

문제:

error TS6306: Referenced project must have setting "composite": true.
error TS6310: Referenced project may not disable emit.

원인:
TypeScript의 Project References 기능을 사용할 때, 참조되는 프로젝트(tsconfig.node.json)에서 특정 설정이 필요합니다. composite: true가 필요하고, noEmit: true는 사용할 수 없습니다.

해결:

{
  "compilerOptions": {
    "composite": true,  // 추가
    "noEmit": false,    // true에서 false로 변경
    // ...
  }
}

ES 모듈과 CommonJS 혼용 문제

문제:

ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and package.json contains "type": "module".

원인:
프론트엔드 package.json에 "type": "module" 설정이 있어 모든 .js 파일이 ES 모듈로 처리됩니다. 그러나 PostCSS와 Tailwind 설정 파일은 CommonJS 형식(module.exports)을 사용합니다.

해결:
Node.js는 .cjs 확장자 파일을 항상 CommonJS로 인식하므로 문제가 해결됩니다.

postcss.config.js → postcss.config.cjs
tailwind.config.js → tailwind.config.cjs

Tailwind CSS v4 PostCSS 플러그인 변경

문제:

[plugin:vite:css] [postcss] It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package...

원인:
Tailwind CSS v4에서는 아키텍처 변경으로 PostCSS 플러그인이 별도 패키지로 분리되었습니다.

해결:
1. 패키지 설치:

npm install @tailwindcss/postcss
  1. PostCSS 설정 수정:
// frontend/postcss.config.cjs

module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
};

TypeScript 컴포넌트 확장자 문제

문제:

error TS5097: An import path can only end with a '.tsx' extension when 'allowImportingTsExtensions' is enabled.

원인:
TypeScript 설정에서 allowImportingTsExtensions: false로 설정된 경우 import 문에서 파일 확장자(.tsx)를 사용할 수 없습니다.

해결:

// 변경 전
import App from './App.tsx'

// 변경 후
import App from './App'

기타

  • 패키지 설치 시 현재 디렉토리를 잘 확인해야 합니다.
    • 프론트에서 사용하는 패키지는 apps/frontend에서 설치 명령어 사용
  • 공유 패키지에서 공유 ui는 선택사항합니다.
    • 저는 하나의 앱에서만 사용할 예정이기에 공유 ui는 사용하지 않습니다.
    • 만약 사용자, 관리자, 홈페이지 등등으로 나눠지는 경우 활용하면 좋습니다.
profile
조금씩 정리하자!!!

0개의 댓글