icon
14장 : 백엔드 개발

Node.js와 Express


웹 애플리케이션은 사용자에게 보이는 부분인 프론트엔드(Frontend)와, 데이터 처리, 비즈니스 로직, 데이터베이스 관리 등을 담당하는 백엔드(Backend)로 구성됩니다. 프론트엔드가 사용자 경험(UX)을 결정한다면, 백엔드는 애플리케이션의 핵심 기능과 데이터의 안정성을 책임집니다.

이 절에서는 백엔드 개발에 널리 사용되는 기술 스택 중 하나인 Node.jsExpress에 대해 알아봅니다. Node.js는 자바스크립트를 서버 측 환경에서 실행할 수 있게 해주며, Express는 Node.js 위에서 웹 서버 및 API를 빠르고 쉽게 구축할 수 있도록 돕는 프레임워크입니다. 여기에 타입스크립트를 통합하여 더욱 견고하고 유지보수하기 쉬운 백엔드 코드를 작성하는 방법을 살펴봅니다.


Node.js 소개

Node.js는 Google Chrome의 V8 JavaScript 엔진으로 빌드된 자바스크립트 런타임입니다. 즉, 웹 브라우저 밖에서도 자바스크립트 코드를 실행할 수 있게 해줍니다. Node.js의 가장 큰 특징은 다음과 같습니다.

  • 비동기 논블로킹 I/O: Node.js는 단일 스레드 기반의 이벤트 루프(Event Loop) 모델을 사용하여 I/O(입출력) 작업을 비동기적으로 처리합니다. 이는 많은 동시 연결을 효율적으로 처리할 수 있게 하여 높은 처리량과 확장성을 제공합니다.
  • 자바스크립트 사용: 프론트엔드와 백엔드 모두 자바스크립트(또는 타입스크립트)를 사용하여 풀 스택(Full-stack) 개발이 가능해집니다. 이는 개발 생산성을 높이고, 개발팀의 기술 스택 통일을 용이하게 합니다.
  • 방대한 생태계 (NPM): Node.js는 세계에서 가장 큰 패키지 생태계인 NPM(Node Package Manager)을 통해 수많은 라이브러리와 도구를 제공합니다.

Node.js 설치

Node.js는 공식 웹사이트에서 다운로드하여 설치할 수 있습니다. 설치 후 터미널에서 다음 명령어로 버전을 확인할 수 있습니다.

node -v
npm -v

Express 소개

Express는 Node.js를 위한 빠르고 개방적이며 최소한의 웹 프레임워크입니다. 웹 애플리케이션과 API를 구축하는 데 필요한 강력한 기능 세트를 제공합니다.

  • 간결한 API: 라우팅, 미들웨어, 템플릿 엔진 통합 등 웹 애플리케이션 개발에 필요한 기본적인 기능들을 쉽고 직관적인 API로 제공합니다.
  • 유연성: 특정 아키텍처나 패턴을 강제하지 않고, 개발자가 원하는 방식으로 애플리케이션을 구성할 수 있도록 높은 자유도를 제공합니다.
  • 미들웨어 시스템: 요청(request)과 응답(response) 사이에서 다양한 작업을 수행할 수 있는 미들웨어 개념을 통해 기능을 확장하기 용이합니다. (예: 로깅, 인증, 데이터 파싱)

Node.js 및 Express 프로젝트에 타입스크립트 적용하기

타입스크립트를 Node.js 및 Express 프로젝트에 적용하면, 런타임 이전에 타입 오류를 감지하고, 코드의 가독성과 유지보수성을 높일 수 있습니다.

프로젝트 초기 설정

새로운 프로젝트를 시작하고 타입스크립트 환경을 설정하는 단계입니다.

프로젝트 초기화

mkdir my-express-ts-app
cd my-express-ts-app
npm init -y

필수 의존성 설치 Node.js, Express, 그리고 타입스크립트 관련 패키지를 설치합니다.

npm install express
npm install --save-dev typescript @types/node @types/express ts-node nodemon
  • typescript: 타입스크립트 컴파일러
  • @types/node: Node.js 코어 모듈에 대한 타입 정의
  • @types/express: Express 프레임워크에 대한 타입 정의
  • ts-node: 타입스크립트 파일을 직접 실행할 수 있게 해주는 도구 (개발용)
  • nodemon: 파일 변경 시 자동으로 서버를 재시작해주는 도구 (개발용)

tsconfig.json 설정: npx tsc --init 명령어를 실행하여 기본 tsconfig.json 파일을 생성합니다.

npx tsc --init

생성된 tsconfig.json 파일을 다음과 같이 수정하거나 추가합니다.

tsconfig.json
{
  "compilerOptions": {
    "target": "es2018",                        // Node.js 버전과 호환되는 ES 버전
    "module": "commonjs",                     // Node.js는 CommonJS 모듈 시스템 사용
    "lib": ["es2018"],                        // 사용 가능한 런타임 라이브러리
    "outDir": "./dist",                       // 컴파일된 JS 파일이 저장될 디렉토리
    "rootDir": "./src",                       // 소스 코드의 루트 디렉토리
    "strict": true,                           // 엄격한 타입 검사 활성화
    "esModuleInterop": true,                  // CommonJS/ES Modules 간의 상호 운용성 지원
    "skipLibCheck": true,                     // 선언 파일(*.d.ts)에 대한 타입 검사 건너뛰기
    "forceConsistentCasingInFileNames": true, // 파일 이름 대소문자 일관성 강제
    "noImplicitAny": true,                    // 'any' 타입으로 추론되는 경우 오류 발생
    "resolveJsonModule": true                 // JSON 파일을 모듈로 가져올 수 있게 허용
  },
  "include": ["src/**/*.ts"],                 // 컴파일할 파일 지정
  "exclude": ["node_modules"]                 // 컴파일에서 제외할 파일 지정
}

package.json 스크립트 추가: 개발 및 빌드 명령어를 package.json에 추가합니다.

package.json
{
  "name": "my-express-ts-app",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js", // 컴파일 후 메인 파일
  "scripts": {
    "build": "tsc", // TypeScript 컴파일
    "start": "node dist/index.js", // 컴파일된 JS 실행
    "dev": "nodemon --exec ts-node src/index.ts" // 개발 모드 (TS 파일 직접 실행)
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.14.9",
    "nodemon": "^3.1.4",
    "ts-node": "^10.9.2",
    "typescript": "^5.5.3"
  }
}

Express 서버 구축 예시

이제 타입스크립트 환경에서 간단한 Express 서버를 구축해봅시다.

기본 서버 파일 생성 (src/index.ts)

src/index.ts
import express, { Request, Response, NextFunction } from 'express';

const app = express();
const port = 3000;

// JSON 요청 본문을 파싱하기 위한 미들웨어
app.use(express.json());

// 루트 경로 핸들러
app.get('/', (req: Request, res: Response) => {
  res.send('Hello from Express with TypeScript!');
});

// GET 요청 예시
app.get('/api/users', (req: Request, res: Response) => {
  const users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];
  res.json(users);
});

// POST 요청 예시: 새로운 사용자 추가
interface NewUserRequest extends Request {
  body: { name: string; email: string };
}

app.post('/api/users', (req: NewUserRequest, res: Response) => {
  const newUser = req.body; // req.body는 타입스크립트에서 기본적으로 any로 추론됩니다.
                           // 인터페이스로 명시하여 타입 안정성을 확보할 수 있습니다.
  console.log('Received new user:', newUser);
  // 실제 애플리케이션에서는 데이터베이스에 저장하는 로직이 들어갑니다.
  res.status(201).json({ message: 'User created successfully', user: newUser });
});

// 에러 핸들링 미들웨어 (가장 마지막에 위치)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

// 서버 시작
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
  console.log('Press Ctrl+C to stop the server.');
});

개발 서버 실행

터미널에서 다음 명령어를 실행하여 개발 서버를 시작합니다.

npm run dev

파일을 수정하면 nodemon이 자동으로 서버를 재시작해줍니다.

빌드 및 프로덕션 서버 실행

프로덕션 배포를 위해서는 먼저 타입스크립트 코드를 자바스크립트로 컴파일해야 합니다.

npm run build
npm run start

build 명령어가 dist 디렉토리에 .js 파일을 생성하고, start 명령어가 이 컴파일된 자바스크립트 파일을 실행합니다.


Express 라우팅 및 미들웨어에 타입 적용

Express는 라우팅과 미들웨어를 통해 요청 처리를 구성합니다. 타입스크립트와 함께 사용하면 이 과정도 타입 안전하게 만들 수 있습니다.

라우터 분리 및 타입 적용

대규모 애플리케이션에서는 모든 라우트를 하나의 파일에 넣는 대신, 기능별로 라우터를 분리하여 관리하는 것이 일반적입니다.

src/routes/userRoutes.ts
import { Router, Request, Response } from 'express';

const router = Router();

// 사용자 관련 타입 정의
interface User {
  id: number;
  name: string;
  email: string;
}

// GET /api/users/:id
router.get('/:id', (req: Request<{ id: string }>, res: Response<User | { message: string }>) => {
  const userId = parseInt(req.params.id);

  // 간단한 더미 데이터
  const users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  const user = users.find(u => u.id === userId);

  if (user) {
    res.json(user);
  } else {
    res.status(404).json({ message: 'User not found' });
  }
});

export default router;
src/index.ts (메인 서버 파일)
import express, { Request, Response, NextFunction } from 'express';
import userRoutes from './routes/userRoutes'; // 라우터 임포트

const app = express();
const port = 3000;

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
  res.send('Hello from Express with TypeScript!');
});

// /api/users 경로로 들어오는 모든 요청은 userRoutes가 처리
app.use('/api/users', userRoutes); // 라우터 미들웨어 등록

// ... (기존 에러 핸들링 미들웨어 및 서버 시작 코드)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

Express의 Request, Response 객체는 제네릭 타입을 지원하여 req.params, req.query, req.body, res.json 등의 타입을 명시적으로 지정할 수 있습니다. 예를 들어, Request<{ id: string }>req.params.idstring 타입임을 나타냅니다.

커스텀 미들웨어에 타입 적용

미들웨어도 타입스크립트와 함께 작성할 수 있습니다.

src/middleware/loggerMiddleware.ts
import { Request, Response, NextFunction } from 'express';

// 커스텀 Request 타입을 확장하여 user 속성 추가 (예: 인증 미들웨어에서 사용자 정보 추가)
interface CustomRequest extends Request {
  user?: { id: number; username: string };
}

const loggerMiddleware = (req: CustomRequest, res: Response, next: NextFunction) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  // 예를 들어, 인증 미들웨어에서 사용자 정보를 req.user에 추가할 수 있습니다.
  // req.user = { id: 1, username: 'testuser' };
  next(); // 다음 미들웨어 또는 라우트 핸들러로 제어권 넘기기
};

export default loggerMiddleware;
src/index.ts (메인 서버 파일)
import express from 'express';
import userRoutes from './routes/userRoutes';
import loggerMiddleware from './middleware/loggerMiddleware'; // 미들웨어 임포트

const app = express();
const port = 3000;

app.use(express.json());
app.use(loggerMiddleware); // 모든 요청에 대해 로거 미들웨어 적용

app.get('/', (req, res) => { // req는 loggerMiddleware에서 CustomRequest로 확장되었음을 반영
  res.send('Hello from Express with TypeScript!');
});

app.use('/api/users', userRoutes);

// ... (에러 핸들링 미들웨어 및 서버 시작 코드)

Node.js와 Express, 타입스크립트의 이점

Node.js와 Express를 타입스크립트와 함께 사용하는 것은 다음과 같은 이점을 제공합니다.

  • 타입 안전성: API 요청/응답, 미들웨어 인자, 데이터베이스 모델 등 모든 데이터 흐름에 타입을 적용하여 런타임 오류를 줄이고 코드의 안정성을 높입니다.
  • 생산성 향상: IDE의 자동 완성, 리팩토링 기능이 강화되어 개발 속도가 향상됩니다.
  • 코드 가독성 및 유지보수성: 타입 정의는 코드의 의도를 명확하게 하고, 복잡한 비즈니스 로직을 이해하기 쉽게 만들어 장기적인 유지보수에 유리합니다.
  • 견고한 API 설계: 명확한 타입 정의는 API 인터페이스를 문서화하는 역할을 하며, 프론트엔드 개발자와의 협업을 원활하게 합니다.
  • 단일 언어 스택: 프론트엔드와 백엔드 모두 자바스크립트(타입스크립트)를 사용하여 개발팀의 기술 스택 부담을 줄이고 풀 스택 개발을 용이하게 합니다.

결론

Node.js와 Express는 강력하고 유연한 백엔드 개발 환경을 제공합니다. 여기에 타입스크립트의 정적 타입 시스템을 결합하면, 개발 생산성을 높이고, 런타임 오류를 줄이며, 대규모 애플리케이션의 유지보수를 용이하게 하는 견고하고 안정적인 백엔드 서비스를 구축할 수 있습니다. 다음 절에서는 데이터베이스와의 연동 방법에 대해 자세히 살펴보겠습니다.