웹 프로그래밍

웹 서버 기초 (#3. Express.js로 만드는 Pokemon Dashboard) - END -

2025-12-08

TypeScript 기반 Express.js 5.0 프로젝트 설정 가이드

이 가이드는 기존 TypeScript 프로젝트를 Express.js 5.0 웹 서버로 변환하는 과정을 단계별로 설명합니다.

목차

  1. 프로젝트 구조 파악
  2. 의존성 설치
  3. package.json 수정
  4. Express.js 서버 구조 생성
  5. 개발 환경 설정
  6. 서버 실행 및 테스트

Express.js의 주요 특징

  1. TypeScript 완전 지원: 타입 안정성 보장
  2. 모듈화된 구조: 라우터를 별도 파일로 분리하여 유지보수성 향상
  3. 에러 핸들링: 전역 에러 핸들링 및 404 처리
  4. CORS 지원: Cross-Origin 요청 처리
  5. 로깅: 요청 로깅 미들웨어

프로젝트 구성하기

1. 프로젝트 구조 파악

git clone https://github.com/sigmadream/templates-ts.git ts-web
src/
├── index.ts          # Express.js 메인 서버
├── routes/
│   └── api.ts        # API 라우터
└── __tests__/        # 테스트 파일들

2. 의존성 설치

  • express: Express.js >= 5.0 프레임워크
  • tsx: 개발 시 자동 재시작 도구
  • cors: Cross-Origin Resource Sharing 미들웨어
  • @types/express: Express.js TypeScript 타입 정의
  • @types/cors: CORS 미들웨어 타입 정의
npm install express @types/express
npm install -D tsx cors @types/cors

3. package.json 수정

"scripts": {
  // ...
  "start": "node dist/index.js",
  "dev": "tsx watch src/index.ts"
}
  • start: 프로덕션 환경에서 서버 실행
  • dev: 개발 환경에서 nodemon으로 서버 실행

4. Express.js 서버 구조 생성

// src/index.ts

import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import apiRoutes from './routes/api.js';

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

// 미들웨어 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 로깅 미들웨어
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
  next();
});

// 기본 라우트
app.get('/', (req: Request, res: Response) => {
  res.json({
    message: 'Express.js 5.0 with TypeScript 서버가 실행 중입니다!',
    version: '1.0.0',
    timestamp: new Date().toISOString(),
    endpoints: {
      api: '/api',
      health: '/api/health'
    }
  });
});

// 라우터 설정
app.use('/api', apiRoutes);

// 에러 핸들링 미들웨어
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error('에러 발생:', err);
  res.status(500).json({
    error: '서버 내부 오류가 발생했습니다.',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined
  });
});

// 404 핸들러
app.use((req: Request, res: Response) => {
  res.status(404).json({
    error: '요청한 리소스를 찾을 수 없습니다.',
    path: req.originalUrl
  });
});

// 서버 시작
app.listen(PORT, () => {
  console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
  console.log(`http://localhost:${PORT}`);
});

export default app;

API 라우터

// src/routes/api.ts

import { Router, Request, Response } from 'express';

const router = Router();

// 헬스 체크
router.get('/health', (req: Request, res: Response) => {
  res.json({
    status: 'OK',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV || 'development'
  });
});

// API 정보
router.get('/info', (req: Request, res: Response) => {
  res.json({
    name: 'Express.js 5.0 TypeScript API',
    version: '1.0.0',
    description: 'TypeScript 기반 Express.js 5.0 API 서버',
    endpoints: {
      health: '/api/health',
      info: '/api/info'
    }
  });
});

export default router;

5. 개발 환경 설정(Option)

// env
// env.example
# 서버 설정
PORT=3000
NODE_ENV=development

# 데이터베이스 설정 (필요시)
# DB_HOST=localhost
# DB_PORT=5432
# DB_NAME=myapp
# DB_USER=username
# DB_PASSWORD=password

# JWT 설정 (필요시)
# JWT_SECRET=your-secret-key
# JWT_EXPIRES_IN=24h

6. 서버 실행 및 테스트

# 개발 서버 실행
npm run dev # 개발 서버 실행

# 프로덕션 빌드
npm run build # 빌드
npm start # 실행

API 엔드포인트 테스트

  • GET / - 서버 정보
  • GET /api/health - 헬스 체크
  • GET /api/info - API 정보

Postman 등을 활용해서 테스트 가능

7. 테스트

npm install -D vitest vite-tsconfig-paths supertest @types/supertest
import request from 'supertest';
import app from '../index.js';

describe('API Health Endpoint', () => {
  describe('GET /api/health', () => {
    it('should return health status with correct structure', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      // status가 'OK'인지 확인
      expect(response.body.status).toBe('OK');

      // 필수 필드들이 존재하는지 확인
      expect(response.body).toHaveProperty('status');
      expect(response.body).toHaveProperty('uptime');
      expect(response.body).toHaveProperty('timestamp');
      expect(response.body).toHaveProperty('environment');
    });

    it('should return 200 status code', async () => {
      await request(app)
        .get('/api/health')
        .expect(200);
    });

    it('should have correct content type', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      expect(response.headers['content-type']).toMatch(/application\/json/);
    });
  });
});

다음 단계

  1. PUG 템플릿 연동
  2. 데이터베이스 연동: SQLite,PostgreSQL, MongoDB 등 데이터베이스 연결
  3. 인증 시스템: JWT 기반 인증 구현
  4. 유효성 검사: express-validator를 사용한 입력 검증
  5. 테스트: Jest를 사용한 단위 테스트 및 통합 테스트
  6. 문서화: Swagger/OpenAPI 문서 생성
  7. 배포: Docker 컨테이너화 및 클라우드 배포

GET and HTML

데이터베이스 연동: SQLite 데이터베이스 연결

SQLite 3

  • SQLite 3는 C 언어로 개발된 경량의 임베디드형 오픈소스 관계형 데이터베이스 시스템(RDBMS)
  • 파일 기반 DBMS로, 서버-클라이언트 모델이 아닌 애플리케이션에 직접 임베디드 되어 동작
  • 폭넓은 플랫폼 및 언어(Python, Java, Node.js 등)에서 공식적으로 지원
  • 빠른 프로토타입 개발이나 소규모/중규모 웹 및 데스크탑 앱에서 경량 DB로 활용

SQLite 및 서버와 연결

  • 라이브러리 설치
npm install sqlite3 @types/sqlite3
  • database/connection.ts
import sqlite3 from 'sqlite3';
import path from 'path';

export class DatabaseConnection {
    // 속성
    private db: sqlite3.Database | null = null;
    private dbPath: string;
    constructor(dbPath: string = 'db/pokedex.db') {
        this.dbPath = path.resolve(dbPath);
    }

    // 데이터베이스 연결
    async connect(): Promise<void> {
        return new Promise((resolve, reject) => {
            this.db = new sqlite3.Database(this.dbPath, (err) => {
                if (err) {
                    console.error('데이터베이스 연결 실패:', err);
                    reject(err);
                } else {
                    console.log(`데이터베이스 연결 성공: ${this.dbPath}`);
                    resolve();
                }
            });
        });
    }

    // 데이터베이스 연결 해제
    async disconnect(): Promise<void> {
        return new Promise((resolve, reject) => {
            if (this.db) {
                this.db.close((err) => {
                    if (err) {
                        console.error('데이터베이스 연결 해제 실패:', err);
                        reject(err);
                    } else {
                        console.log('데이터베이스 연결 해제 성공');
                        this.db = null;
                        resolve();
                    }
                });
            } else {
                resolve();
            }
        });
    }

    // 쿼리 실행 (SELECT)
    async query(sql: string, params: any[] = []): Promise<any[]> {
        if (!this.db) {
            throw new Error('데이터베이스가 연결되지 않았습니다.');
        }
        return new Promise((resolve, reject) => {
            this.db!.all(sql, params, (err, rows) => {
                if (err) {
                    console.error('쿼리 실행 실패:', err);
                    reject(err);
                } else {
                    resolve(rows);
                }
            });
        });
    }

    // 단일 행 조회
    async get(sql: string, params: any[] = []): Promise<any> {
        if (!this.db) {
            throw new Error('데이터베이스가 연결되지 않았습니다.');
        }
        return new Promise((resolve, reject) => {
            this.db!.get(sql, params, (err, row) => {
                if (err) {
                    console.error('쿼리 실행 실패:', err);
                    reject(err);
                } else {
                    resolve(row);
                }
            });
        });
    }

    isConnected(): boolean {
        return this.db !== null;
    }
}

let dbInstance: DatabaseConnection | null = null;
export const getDatabase = (): DatabaseConnection => {
    if (!dbInstance) {
        dbInstance = new DatabaseConnection();
    }
    return dbInstance;
};
export default DatabaseConnection;
  • index.ts
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import apiRoutes from './routes/api';
import { getDatabase } from './database/connection';

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

// 미들웨어 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 로깅 미들웨어
app.use((req: Request, _res: Response, next: NextFunction) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
    next();
});

// 기본 라우트
app.get('/', (_req: Request, res: Response) => {
    res.json({
        message: 'Express.js 5.0 with TypeScript 서버가 실행 중입니다!',
        version: '1.0.0',
        timestamp: new Date().toISOString(),
        endpoints: {
            api: '/api',
            health: '/api/health',
        },
    });
});

// 라우터 설정
app.use('/api', apiRoutes);

// 에러 핸들링 미들웨어
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
    console.error('에러 발생:', err);
    res.status(500).json({
        error: '서버 내부 오류가 발생했습니다.',
        message: process.env.NODE_ENV === 'development' ? err.message : undefined,
    });
});

// 404 핸들러
app.use((req: Request, res: Response) => {
    res.status(404).json({
        error: '요청한 리소스를 찾을 수 없습니다.',
        path: req.originalUrl,
    });
});

// index.ts
// 데이터베이스 연결 및 서버 시작
async function startServer() {
    try {
        await getDatabase().connect();
        app.listen(PORT, () => {
            console.log(`http://localhost:${PORT} 에서 실행 중입니다.`);
        });
    } catch (error) {
        console.error('서버 시작 실패:', error);
        process.exit(1);
    }
}

// index.ts
// 서버 종료
process.on('SIGINT', async () => {
    console.log('\n서버를 종료합니다...');
    try {
        await getDatabase().disconnect();
        console.log('데이터베이스 연결이 해제되었습니다.');
        process.exit(0);
    } catch (error) {
        console.error('데이터베이스 연결 해제 실패:', error);
        process.exit(1);
    }
});

startServer();

export default app;

URL #1: API(JSON)

JSON API

모든 포켓몬 조회 (페이지네이션, 검색, 필터링 지원)

  • services/pokemonService.ts
import { DatabaseConnection } from '../database/connection';

export interface Pokemon {
    id: number;
    pokedex_number: string;
    name: string;
    description: string;
    types: string;
    height: number;
    category: string;
    weight: number;
    gender: string;
    abilities: string;
}

export interface PokemonQuery {
    page?: number;
    limit?: number;
    search?: string;
    type?: string;
    sortBy?: 'id' | 'name' | 'pokedex_number';
    sortOrder?: 'ASC' | 'DESC';
}

export class PokemonService {
    private db: DatabaseConnection;

    constructor(database: DatabaseConnection) {
        this.db = database;
    }

    async getAllPokemon(query: PokemonQuery = {}): Promise<{ pokemon: Pokemon[]; total: number; page: number; limit: number }> {
        const {
            page = 1,
            limit = 20,
            search = '',
            type = '',
            sortBy = 'id',
            sortOrder = 'ASC'
        } = query;

        const offset = (page - 1) * limit;

        // WHERE 조건 구성
        let whereConditions: string[] = [];
        let params: any[] = [];

        if (search) {
            whereConditions.push('(name LIKE ? OR description LIKE ?)');
            params.push(`%${search}%`, `%${search}%`);
        }

        if (type) {
            whereConditions.push('types LIKE ?');
            params.push(`%${type}%`);
        }

        const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';

        // 전체 개수 조회
        const countSql = `SELECT COUNT(*) as total FROM pokemon ${whereClause}`;
        const countResult = await this.db.get(countSql, params);
        const total = countResult.total;

        // 데이터 조회
        const sql = `
      SELECT id, pokedex_number, name, description, types, height, category, weight, gender, abilities
      FROM pokemon 
      ${whereClause}
      ORDER BY ${sortBy} ${sortOrder}
      LIMIT ? OFFSET ?
    `;

        const pokemon = await this.db.query(sql, [...params, limit, offset]);

        return {
            pokemon,
            total,
            page,
            limit
        };
    }
}

export default PokemonService;
  • routes/pokemon.ts
import { Router, Request, Response } from 'express';
import { PokemonService, PokemonQuery } from '../services/pokemonService';
import { getDatabase } from '../database/connection';

const router = Router();
const pokemonService = new PokemonService(getDatabase());

router.get('/', async (req: Request, res: Response) => {
    try {
        const query: PokemonQuery = {
            page: req.query.page ? parseInt(req.query.page as string) : undefined,
            limit: req.query.limit ? parseInt(req.query.limit as string) : undefined,
            search: req.query.search as string,
            type: req.query.type as string,
            sortBy: req.query.sortBy as 'id' | 'name' | 'pokedex_number',
            sortOrder: req.query.sortOrder as 'ASC' | 'DESC'
        };

        const result = await pokemonService.getAllPokemon(query);

        res.json({
            success: true,
            data: result.pokemon,
            pagination: {
                page: result.page,
                limit: result.limit,
                total: result.total,
                totalPages: Math.ceil(result.total / result.limit)
            }
        });
    } catch (error) {
        console.error('포켓몬 목록 조회 실패:', error);
        res.status(500).json({
            success: false,
            error: '포켓몬 목록을 조회하는 중 오류가 발생했습니다.'
        });
    }
});

export default router;
  • routes/api.ts
import { Router, Request, Response } from 'express';
import pokemonRoutes from './pokemon';

const router = Router();
router.use('/pokemon', pokemonRoutes);

// 헬스 체크
router.get('/health', (_req: Request, res: Response) => {
  res.json({
    status: 'OK',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV || 'development',
  });
});

// API 정보
router.get('/info', (_req: Request, res: Response) => {
  res.json({
    name: 'Express.js 5.0 TypeScript API',
    version: '1.0.0',
    description: 'TypeScript 기반 Express.js 5.0 API 서버',
    endpoints: {
      health: '/api/health',
      info: '/api/info',
    },
  });
});

export default router;

URL #2: HTML

문자열, HTML, 그리고 템플릿(Pug)

  • Pug는 웹 페이지의 UI를 기술하는 방식에서 근본적인 차이를 보이는 템플릿 언어
    • HTML은 태그와 닫는 태그, 중첩 구조, 속성을 명시적으로 작성하는 전통적인 마크업 언어
    • Pug는 들여쓰기로 계층 관계를 표현하고, 닫는 태그 없이 짧게 동적 웹 UI를 작성할 수 있어 코드가 한눈에 들어옴
    • 속성은 괄호로 묶어 표기하고, 클래스(.class)나 id(#id)는 태그와 결합해서 선언
  • Pug는 기존 HTML을 간결하고 계층적으로 표현할 수 있게 해주는 Node.js 기반 템플릿 엔진
npm install pug @types/pug
  • layout.pug
doctype html
html(lang="ko")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    title= title || '포켓몬 API'
    link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css")
    link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css")
    style.
      .pokemon-card {
        transition: transform 0.2s;
      }
      .pokemon-card:hover {
        transform: translateY(-5px);
      }
      .type-badge {
        font-size: 0.8em;
      }
      .equal-height-card {
        display: flex;
        flex-direction: column;
        height: 100%;
      }
      .equal-height-card .card-body {
        flex: 1;
        display: flex;
        flex-direction: column;
      }
      .equal-height-card .table-responsive {
        flex: 1;
        overflow-y: auto;
      }
  body
    nav.navbar.navbar-expand-lg.navbar-dark.bg-primary
      .container
        a.navbar-brand(href="/")
          i.fas.fa-dragon.me-2
          | 포켓몬 API
        .navbar-nav
          a.nav-link(href="/") 홈
          a.nav-link(href="/pokemon") 포켓몬 목록
          a.nav-link(href="/pokemon/stats") 통계
          a.nav-link(href="/docs") API 문서
          a.nav-link(href="/health") 시스템 상태

    main.container.mt-4
      block content

    footer.bg-light.mt-5.py-4
      .container.text-center
        p.text-muted © 2025 포켓몬 API. 모든 포켓몬 데이터는 교육 목적으로만 사용됩니다.

    script(src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js")
    script(src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js")
    script(src="https://cdn.jsdelivr.net/npm/chart.js")
    block scripts
  • index.pug
extends layout

block content
  .row
    .col-12
      .jumbotron.bg-primary.text-white.p-5.rounded.mb-4
        h1.display-4
          i.fas.fa-dragon.me-3
          | 포켓몬 API
        p.lead 포켓몬 데이터를 조회하고 탐색할 수 있는 웹 인터페이스입니다.
        a.btn.btn-light.btn-lg(href="/pokemon", role="button")
          i.fas.fa-list.me-2
          | 포켓몬 목록 보기

  .row
    .col-md-3.mb-4
      .card.h-100
        .card-body.text-center
          i.fas.fa-search.fa-3x.text-primary.mb-3
          h5.card-title 포켓몬 검색
          p.card-text 이름, 타입, 카테고리로 포켓몬을 검색할 수 있습니다.
          a.btn.btn-primary(href="/pokemon") 검색하기

    .col-md-3.mb-4
      .card.h-100
        .card-body.text-center
          i.fas.fa-chart-bar.fa-3x.text-success.mb-3
          h5.card-title 통계 조회
          p.card-text 포켓몬의 타입별, 카테고리별 통계를 확인할 수 있습니다.
          a.btn.btn-success(href="/pokemon/stats") 통계 보기

    .col-md-3.mb-4
      .card.h-100
        .card-body.text-center
          i.fas.fa-code.fa-3x.text-info.mb-3
          h5.card-title API 문서
          p.card-text RESTful API를 통해 프로그래밍 방식으로 데이터에 접근할 수 있습니다.
          a.btn.btn-info(href="/docs") API 정보

    .col-md-3.mb-4
      .card.h-100
        .card-body.text-center
          i.fas.fa-heartbeat.fa-3x.text-success.mb-3
          h5.card-title 시스템 상태
          p.card-text 서버 상태, 가동 시간, 메모리 사용량 등 시스템 정보를 확인할 수 있습니다.
          a.btn.btn-success(href="/health") API 상태

  .row.mt-5
    .col-12
      .card
        .card-header
          h5.mb-0
            i.fas.fa-info-circle.me-2
            | API 정보
        .card-body
          .row
            .col-md-6
              h6 API 엔드포인트
              ul.list-unstyled
                li
                  code GET /api/pokemon
                  |  - 포켓몬 목록 조회
                li
                  code GET /api/pokemon/:id
                  |  - 특정 포켓몬 조회
                li
                  code GET /api/pokemon/search/name/:name
                  |  - 이름으로 검색
                li
                  code GET /api/pokemon/type/:type
                  |  - 타입별 조회
                li
                  code GET /api/pokemon/stats/overview
                  |  - 통계 조회
            .col-md-6
              h6 사용 가능한 파라미터
              ul.list-unstyled
                li
                  strong page
                  |  - 페이지 번호
                li
                  strong limit
                  |  - 페이지당 항목 수
                li
                  strong search
                  |  - 검색어
                li
                  strong type
                  |  - 포켓몬 타입
                li
                  strong sortBy
                  |  - 정렬 기준
                li
                  strong sortOrder
                  |  - 정렬 순서 (ASC/DESC)

block scripts
  script.
    // API 상태 확인
    async function checkApiStatus() {
      try {
        const response = await axios.get('/api/health');
        console.log('API 상태:', response.data);
      } catch (error) {
        console.error('API 상태 확인 실패:', error);
      }
    }
    
    // 페이지 로드 시 API 상태 확인
    document.addEventListener('DOMContentLoaded', checkApiStatus);
  • routes/web.ts
import { Router, Request, Response } from 'express';
import { PokemonService } from '../services/pokemonService';
import { getDatabase } from '../database/connection';

const router = Router();
const pokemonService = new PokemonService(getDatabase());

// 홈페이지
router.get('/', (_req: Request, res: Response) => {
    res.render('index', {
        title: '포켓몬 API - 홈'
    });
});

// 포켓몬 목록 페이지
router.get('/pokemon', async (req: Request, res: Response) => {
    try {
        const query = {
            page: req.query.page ? parseInt(req.query.page as string) : 1,
            limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
            search: req.query.search as string,
            type: req.query.type as string,
            sortBy: req.query.sortBy as 'id' | 'name' | 'pokedex_number',
            sortOrder: req.query.sortOrder as 'ASC' | 'DESC'
        };

        const result = await pokemonService.getAllPokemon(query);

        res.render('pokemon', {
            title: '포켓몬 목록',
            pokemon: result.pokemon,
            pagination: {
                page: result.page,
                limit: result.limit,
                total: result.total,
                totalPages: Math.ceil(result.total / result.limit)
            },
            query: query
        });
    } catch (error) {
        console.error('포켓몬 목록 조회 실패:', error);
        res.status(500).render('error', {
            title: '서버 오류',
            error: '포켓몬 목록을 불러오는 중 오류가 발생했습니다.'
        });
    }
});

export default router;
  • index.ts
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import apiRoutes from './routes/api';
import webRoutes from './routes/web';
import { getDatabase } from './database/connection';

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

// PUG 템플릿 엔진 설정
app.set('view engine', 'pug');
app.set('views', './views');

// 미들웨어 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 로깅 미들웨어
app.use((req: Request, _res: Response, next: NextFunction) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
    next();
});

// 기본 라우트 - PUG 템플릿으로 홈페이지 렌더링
app.get('/', (_req: Request, res: Response) => {
    res.render('index', {
        title: '포켓몬 API - 홈'
    });
});

// 라우터 설정
app.use('/api', apiRoutes);
app.use('/', webRoutes);

// 에러 핸들링 미들웨어
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
    console.error('에러 발생:', err);
    res.status(500).json({
        error: '서버 내부 오류가 발생했습니다.',
        message: process.env.NODE_ENV === 'development' ? err.message : undefined,
    });
});

// 404 핸들러
app.use((req: Request, res: Response) => {
    res.status(404).json({
        error: '요청한 리소스를 찾을 수 없습니다.',
        path: req.originalUrl,
    });
});

// 데이터베이스 연결 및 서버 시작
async function startServer() {
    try {
        await getDatabase().connect();
        app.listen(PORT, () => {
            console.log(`http://localhost:${PORT} 에서 실행 중입니다.`);
        });
    } catch (error) {
        console.error('서버 시작 실패:', error);
        process.exit(1);
    }
}

// 서버 종료
process.on('SIGINT', async () => {
    console.log('\n서버를 종료합니다...');
    try {
        await getDatabase().disconnect();
        console.log('데이터베이스 연결이 해제되었습니다.');
        process.exit(0);
    } catch (error) {
        console.error('데이터베이스 연결 해제 실패:', error);
        process.exit(1);
    }
});

startServer();

export default app;

참고문헌