K8S 기반의 모노레포 구조로 TypeScript 웹 애플리케이션 개발하기

프로젝트 초기 설정

본 문서에서는 Kubernetes(K8S), Docker, Yarn Workspaces, TypeScript, esbuild, React 및 Express를 활용하여 클라우드 네이티브 웹 애플리케이션을 구성하는 방법을 설명합니다. 최종적으로는 컨테이너화된 형태로 K8S에 배포 가능한 완전한 앱 아키텍처를 구축하게 됩니다.

모노레포 구조 설계

이 프로젝트는 다중 패키지를 포함하는 모노레포 형태로 구성됩니다. 주요 목적은 공통 로직의 재사용성 향상과 서비스 간 통신 예측의 용이함입니다. 다음과 같은 세 가지 하위 패키지로 나뉩니다:

  • app: React 기반 프론트엔드
  • common: 공유 타입 및 상수 정의
  • server: Express 기반 백엔드 서버

Yarn Workspaces 초기화

작업 디렉터리에서 아래 명령어를 실행합니다:

mkdir my-app && cd my-app
yarn init -2

루트 package.json 파일에 다음 내용을 추가합니다:

{
  "name": "my-app",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["packages/*"]
}

하위 패키지 폴더를 생성하고 각각에 package.json을 작성합니다:

packages/
├── app/
│   └── package.json
├── common/
│   └── package.json
├── server/
│   └── package.json

예시: packages/common/package.json

{
  "name": "@my-app/common",
  "version": "0.1.0",
  "private": true
}

TypeScript 설정

루트 디렉터리에서 TypeScript를 전역 개발 의존성으로 설치합니다:

yarn add -D -W typescript

루트에 tsconfig.json을 생성하여 컴파일 옵션을 정의합니다:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      "@my-app/*": ["packages/*"]
    },
    "jsx": "react-jsx",
    "outDir": "./dist",
    "strict": true
  },
  "exclude": ["node_modules", "dist"]
}

빌드 도구 스크립트 추가

패키지 작업을 단순화하기 위해 루트 package.json에 편의 스크립트를 등록합니다:

"scripts": {
  "app": "yarn workspace @my-app/app",
  "common": "yarn workspace @my-app/common",
  "server": "yarn workspace @my-app/server"
}

이제 yarn app add react처럼 특정 패키지에 의존성을 추가할 수 있습니다.

공통 모듈 작성

packages/common/src/index.ts에 공유 상수를 정의합니다:

export const SITE_TITLE = 'My Cloud App';

해당 패키지의 package.json에 진입점(entry point)을 지정합니다:

"main": "./src/index.ts"

React 프론트엔드 구성

필요한 의존성을 설치합니다:

yarn app add react react-dom
yarn app add -D @types/react @types/react-dom

정적 자산을 위한 public/index.html 파일을 생성합니다:

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>${SITE_TITLE}</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/script.js"></script>
  </body>
</html>

기본 리액트 컴포넌트를 작성합니다:

// packages/app/src/App.tsx
import { SITE_TITLE } from '@my-app/common';
import React, { useState } from 'react';

export const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{SITE_TITLE}에 오신 것을 환영합니다!</h1>
      <p>현재 값: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>증가</button>
    </div>
  );
};

Express 백엔드 서버 개발

서버 패키지에 필요한 의존성을 추가합니다:

yarn server add express cors
yarn server add -D @types/express @types/cors

정적 파일 제공 및 라우팅 처리를 위한 서버 코드를 작성합니다:

// packages/server/src/index.ts
import express from 'express';
import cors from 'cors';
import { SITE_TITLE } from '@my-app/common';
import path from 'path';

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

app.use(cors());
app.use(express.static(path.join(__dirname, '../../app/public')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, '../../app/public', 'index.html'));
});

app.listen(PORT, () => {
  console.log(`${SITE_TITLE} 서버 실행 중: http://localhost:${PORT}`);
});

esbuild를 이용한 번들링

고속 빌드 도구로 esbuild를 사용합니다. 먼저 전역 설치:

yarn add -D -W esbuild ts-node

루트에 scripts/build.ts를 생성하여 빌드 로직을 구현합니다:

import { build } from 'esbuild';

async function compileApp() {
  await build({
    entryPoints: ['packages/app/src/index.tsx'],
    outfile: 'packages/app/public/script.js',
    bundle: true,
    minify: true,
    sourcemap: false,
    define: { 'process.env.NODE_ENV': '"production"' },
    platform: 'browser',
    format: 'esm'
  });
}

async function compileServer() {
  await build({
    entryPoints: ['packages/server/src/index.ts'],
    outfile: 'packages/server/dist/index.js',
    bundle: true,
    external: ['express'],
    platform: 'node',
    target: 'node16'
  });
}

async function runBuild() {
  await Promise.all([compileApp(), compileServer()]);
}

runBuild();

빌드 명령어를 루트 스크립트에 추가합니다:

"scripts": {
  ...
  "build": "ts-node scripts/build.ts"
}

Docker 컨테이너화

애플리케이션을 컨테이너로 패키징하기 위해 Dockerfile을 생성합니다:

FROM node:16-alpine

WORKDIR /app

COPY package.json yarn.lock ./
COPY packages/app/package.json ./packages/app/
COPY packages/common/package.json ./packages/common/
COPY packages/server/package.json ./packages/server/

RUN yarn

COPY . .

RUN yarn build

EXPOSE 3000

CMD ["yarn", "serve"]

.dockerignore 파일로 불필요한 파일 복사를 방지합니다:

node_modules
*.log
dist
*.md

이미지 빌드 및 실행 스크립트를 추가합니다:

"scripts": {
  ...
  "docker": "docker build -t my-web-app .",
  "start:container": "docker run -p 3000:3000 my-web-app"
}

빌드 후 컨테이너 실행:

yarn docker
docker run -d -p 3000:3000 my-web-app

브라우저에서 http://localhost:3000 접속 시 앱 확인 가능합니다.

태그: TypeScript Yarn Workspace esbuild React Express

6월 20일 19:02에 게시됨