Node.js + Express — 가장 많이 사용하는 타입들 알아보기
공식문서 기반의 타입스크립트 입문기
들어가며 (Express에서 제공하는 주요 타입들)
Express로 API 서버를 개발하다 보면 자연스럽게 마주치는 타입들이 있습니다. Request, Response, Router 같은 것들이죠. 이 타입들은 Express가 제공하는 인터페이스의 일부로, 서버 개발을 더 안전하고 편리하게 만들어줍니다.
이번 편에서는 Express 개발에서 가장 많이 사용하는 타입들을 소개하고, 각 타입이 어떤 역할을 하는지 가볍게 살펴보겠습니다. 특히 개발자가 명시적으로 타입을 지정해서 사용해야 하는 RequestHandler, ErrorRequestHandler, Router, Server 등의 타입을 중심으로 설명하겠습니다. 타입스크립트를 처음 접하는 분들도 쉽게 따라올 수 있도록, 실제 사용 예시와 함께 설명하겠습니다.
주요 타입 소개
Express와 Node.js의 타입들은 크게 두 가지로 나눌 수 있습니다. 먼저 TypeScript가 상황에 따라 자동으로 추론하거나 적용하는 타입들을 살펴보고, 그 다음에 개발자가 명시적으로 지정해야 하는 타입들을 자세히 알아보겠습니다.
명시적으로 지정하지 않아도 되는 타입들
이 타입들은 Express 미들웨어나 핸들러 함수의 파라미터로 사용될 때 자동으로 타입이 추론되거나, Express가 내부적으로 제공하는 인터페이스입니다. 특별히 타입을 지정하지 않아도 TypeScript가 적절한 타입을 적용해줍니다.
HTTP 인터페이스: Request, Response, NextFunction
이 세 타입은 Express 앱의 핵심 인터페이스로, 미들웨어 함수의 파라미터에서 자동으로 추론됩니다.
-
Request: 들어오는 HTTP 요청을 표현합니다. 미들웨어 파라미터로 사용될 때
req.body,req.params,req.query등의 속성이 자동으로 타입화되어, 요청 데이터를 안전하게 접근할 수 있습니다. -
Response: 나가는 HTTP 응답을 표현합니다. 미들웨어 파라미터로 사용될 때
res.status(),res.json(),res.send()메서드들의 반환 타입이 자동으로 명확해져, 응답 형식을 일관되게 유지할 수 있습니다. -
NextFunction: 미들웨어 체인에서 다음 미들웨어로 제어를 넘기는 함수입니다. 미들웨어 파라미터로 사용될 때 자동으로 타입이 적용되어,
next(error)처럼 에러를 전달할 수 있습니다.
Node.js 스트림 타입: IncomingMessage, ServerResponse
Node.js의 HTTP 스트림 타입들로, Express의 Request/Response가 내부적으로 확장하는 기본 타입들입니다. 일반적인 Express 개발에서는 직접 사용할 일이 많지 않습니다.
-
IncomingMessage: Node.js의 HTTP 요청 스트림 타입입니다. Express의 Request가 이를 확장한 형태로, 로우 레벨의 스트림 I/O를 타입화합니다.
-
ServerResponse: Node.js의 HTTP 응답 스트림 타입입니다. Express의 Response가 이를 확장해서, 스트림 기반 응답 처리를 안전하게 합니다.
명시적으로 지정해야 하는 타입들
이 타입들은 함수 타입 선언, 변수 선언, 제네릭 타입 등에서 직접 타입을 지정해야 합니다. Express 개발에서 가장 많이 작성하게 되는 타입들입니다.
핸들러 타입: RequestHandler, ErrorRequestHandler
Express 핸들러의 시그니처를 명시적으로 정의할 때 사용하는 타입들입니다.
-
RequestHandler: 일반적인 미들웨어와 라우트 핸들러의 타입입니다. 함수를 선언할 때
(req, res, next) => void형태의 시그니처를 직접 지정해야 합니다. -
ErrorRequestHandler: 에러 처리 미들웨어의 타입입니다. 함수를 선언할 때
(error, req, res, next) => void형태로 첫 번째 파라미터가Error객체임을 명시적으로 지정해야 합니다.
라우터/서버 타입: Router, Server
Express 앱의 구조와 Node.js 서버를 타입화한 것들로, 변수 선언 시 직접 타입을 지정해야 합니다.
-
Router: Express 라우터 인스턴스의 타입입니다.
const router: Router = Router()처럼 변수 선언 시 직접 타입을 지정해야 합니다. -
Server: Node.js HTTP 서버의 타입입니다.
const server: Server = app.listen()처럼 서버 변수를 선언할 때 직접 타입을 지정해야 합니다.
Node.js 기본 타입: NodeJS.Timeout, NodeJS.Process
Node.js 런타임 객체들의 타입으로, 변수를 선언할 때 직접 지정해야 합니다.
-
NodeJS.Timeout:
setTimeout()의 반환값 타입입니다.const timer: NodeJS.Timeout = setTimeout()처럼 타이머 변수를 선언할 때 직접 타입을 지정해야 합니다. -
NodeJS.Process: Node.js 프로세스 객체의 타입입니다. 프로세스 관련 변수를 사용할 때 직접 타입을 지정해야 합니다.
사전 준비
Express와 Node.js의 타입들을 사용하려면 먼저 필요한 타입 정의 패키지들을 설치해야 합니다. 이 타입들은 DefinitelyTyped에서 제공되며, 개발 시점에만 필요하므로 devDependencies로 설치합니다.
npm install --save-dev @types/express @types/node- @types/express: Express.js의 모든 타입 정의를 제공합니다.
Request,Response,Router등의 타입을 사용할 수 있게 해줍니다. - @types/node: Node.js의 타입 정의를 제공합니다.
IncomingMessage,ServerResponse,NodeJS.Timeout등의 타입과 Node.js 전역 객체들을 타입화합니다.
참고: Express 4.x 버전을 사용하는 경우
@types/express@4를 명시적으로 설치해야 할 수 있습니다. 최신 버전의 Express를 사용한다면 기본적으로 최신 타입이 설치됩니다.
실전 패턴 (사용자 API 구축하기)
이제 하나의 시나리오를 따라 각 타입들을 어떻게 적용하는지 살펴보겠습니다. 간단한 사용자 관리 API를 구축하면서, 각 타입의 역할을 확인해 보겠습니다.
1. 기본 라우터와 핸들러 설정
Router 타입은 Express 라우터 인스턴스의 타입입니다. router.get(), router.post() 등의 메서드 타입이 정의되어 있어, 경로별 핸들러를 타입 안전하게 추가할 수 있습니다. Router 타입을 사용하면 API 엔드포인트를 체계적으로 구성하고, 잘못된 HTTP 메서드나 경로 설정을 방지할 수 있습니다.
import express, { Router } from 'express';
const app = express();
const userRouter: Router = Router();
app.use(express.json());
app.use('/api/users', userRouter);
const server = app.listen(3000, () => {
console.log('Server started');
});2. Request/Response 타입으로 API 핸들러 구현
Request와 Response 타입은 Express 앱의 핵심 인터페이스입니다. Request는 들어오는 HTTP 요청을, Response는 나가는 HTTP 응답을 타입화합니다. 이 타입들을 활용하면 요청 데이터에 안전하게 접근하고, 응답 형식을 일관되게 유지할 수 있습니다.
import { RequestHandler } from 'express';
interface User {
id: number;
name: string;
email: string;
}
const createUser: RequestHandler = async (req, res, next) => {
try {
// req.body 타입 안전성 보장
const userData = req.body as { name: string; email: string };
const newUser: User = {
id: Date.now(),
name: userData.name,
email: userData.email,
};
// res.status(), res.json() 타입 안전성 보장
res.status(201).json({
success: true,
data: newUser,
});
} catch (error) {
next(error);
}
};
const getUser: RequestHandler = async (req, res, next) => {
try {
const userId = parseInt(req.params.id);
const user: User = {
id: userId,
name: 'John Doe',
email: 'john@example.com',
};
res.json({
success: true,
data: user,
});
} catch (error) {
next(error);
}
};3. ErrorRequestHandler로 에러 처리 미들웨어 구현
ErrorRequestHandler는 에러 처리 미들웨어의 타입입니다. 일반 RequestHandler와 달리 첫 번째 파라미터가 Error 객체임을 명시합니다. 이 타입을 사용하면 에러 객체의 속성에 안전하게 접근하고, 예외 처리를 체계적으로 구현할 수 있습니다.
import { ErrorRequestHandler, RequestHandler } from 'express';
const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
console.error('Error:', error);
// 에러 객체의 속성에 타입 안전하게 접근
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
error: { message },
});
};
const requestLogger: RequestHandler = (req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
};4. Router로 경로 구성과 NextFunction으로 미들웨어 체인 연결
NextFunction은 미들웨어 체인에서 다음 미들웨어로 제어를 넘기는 함수 타입입니다. 에러를 전달할 때 next(error)처럼 사용할 수 있습니다. Router 타입과 결합하면 미들웨어 체인의 타입 안전성을 완벽하게 확보할 수 있습니다.
import { Router } from 'express';
const userRouter: Router = Router();
// 미들웨어 체인 구성 (타입 안전성 보장)
userRouter.use(requestLogger);
// 경로별 핸들러 연결 (Router 타입으로 안전하게 추가)
userRouter.post('/', createUser);
userRouter.get('/:id', getUser);
// 에러 처리 미들웨어는 앱 레벨에서 추가
app.use(errorHandler);5. Server 타입으로 서버 생명주기 관리
Server 타입은 Node.js HTTP 서버의 타입입니다. 서버 이벤트 리스너의 타입을 보장해서 서버 시작/종료 로직을 안전하게 작성할 수 있습니다. NodeJS.Timeout과 함께 사용하여 타이머 관리까지 타입화할 수 있습니다.
// app.ts (계속)
import { Server } from 'http';
import { NodeJS } from '@types/node';
let server: Server;
// 서버 시작 (Server 타입으로 이벤트 리스너 타입 보장)
const startServer = () => {
server = app.listen(3000, () => {
console.log('Server started');
});
server.on('error', (error) => {
console.error('Server error:', error);
});
};
// 서버 종료 (그레이스풀 셧다운)
const stopServer = () => {
if (server) {
server.close(() => {
console.log('Server stopped');
process.exit(0);
});
}
};
process.on('SIGTERM', stopServer);
process.on('SIGINT', stopServer);
// 타이머 관리 (NodeJS.Timeout 활용)
const timeoutId: NodeJS.Timeout = setTimeout(() => {
console.log('Timer executed');
}, 1000);
clearTimeout(timeoutId);함정과 주의사항
Request 타입의 함정
Request 타입은 Express의 기본 설정에서 body, params, query 속성이 any 타입으로 정의되어 있습니다. JSON 미들웨어를 추가하지 않거나, 별도의 타입 가드를 구현하지 않으면 TypeScript의 타입 안전성을 제대로 활용할 수 없습니다.
이 함정은 특히 API 엔드포인트가 많아질수록 문제가 됩니다. 요청 데이터의 구조를 미리 파악하기 어렵고, 런타임에서만 발견되는 타입 관련 버그가 증가합니다.
// ❌ 잘못된 사용
app.post('/users', (req, res) => {
const user = req.body; // any 타입, 런타임 에러 위험
// ...
});// ✅ 올바른 사용
interface CreateUserRequest {
name: string;
email: string;
}
app.post('/users', (req: Request<{}, any, CreateUserRequest>, res) => {
const user = req.body; // CreateUserRequest 타입 보장
// ...
});Response 타입의 함정
Response 타입의 메서드 체이닝에서 반환 타입을 정확히 이해하지 못하면 컴파일 에러가 발생합니다. 특히 status() 메서드는 Response 객체를 반환하지만, json()이나 send() 같은 메서드들은 Response 객체가 아닌 void나 다른 타입을 반환할 수 있습니다.
이 함정은 응답을 구성하는 과정에서 자주 발생합니다. TypeScript가 메서드 체이닝의 타입을 엄격하게 검사하기 때문에, Express 응답 객체의 메서드 반환 타입을 정확히 알아야 합니다.
// ❌ 타입 에러 발생
res.status(200).json({ data }); // 타입 체킹 통과
res.json({ data }).status(200); // 타입 에러: json()은 Response를 반환하지 않음NextFunction의 함정
Express 미들웨어에서 next()를 호출하지 않으면 요청 처리가 중단됩니다. TypeScript의 타입 시스템은 next() 호출을 강제하지 않기 때문에, 실수로 next()를 빼먹으면 런타임에서 요청이 응답 없이 중단되는 문제가 발생합니다.
이 함정은 특히 조건부 로직이 있는 미들웨어에서 자주 발생합니다. 인증 미들웨어에서 토큰 검증 실패 시 응답을 보내고 next()를 호출하지 않는 경우, 요청이 영원히 처리되지 않은 채로 남게 됩니다.
// ❌ 요청이 중단됨
const authMiddleware: RequestHandler = (req, res, next) => {
if (!req.headers.authorization) {
res.status(401).json({ error: 'Unauthorized' });
// next()를 호출하지 않음!
}
};Router 타입의 함정
Router 타입의 HTTP 메서드들(get, post, put, delete 등)은 반드시 RequestHandler 타입의 함수를 인자로 기대합니다. 일반 함수를 전달하면 TypeScript가 타입 에러를 발생시킵니다.
이 함정은 미들웨어 함수를 직접 전달할 때 자주 발생합니다. RequestHandler 타입이 아닌 일반 함수를 router.get() 등의 메서드에 넘기려고 하면 컴파일 에러가 발생합니다.
// ❌ 타입 에러
const handler = (req, res) => { /* ... */ };
router.get('/users', handler); // Error: handler가 RequestHandler 타입이 아님// ✅ 올바른 사용
const handler: RequestHandler = (req, res, next) => { /* ... */ };
router.get('/users', handler);예상 질문(FAQ)
Q. 왜 RequestHandler를 써야 하나요? 그냥 화살표 함수로 작성하면 안 되나요?
RequestHandler는 Express의 미들웨어 시그니처를 정확히 정의한 타입입니다. (req, res, next) => void 형태를 강제해서, 실수로 next를 빼먹거나 파라미터 순서를 바꾸는 일을 방지합니다. JavaScript로만 개발할 때는 이런 실수가 런타임에 발견되지만, TypeScript에서는 코드 작성 시점에 잡아줍니다.
Q. ErrorRequestHandler의 첫 번째 파라미터가 정말 Error 타입인가요?
네, 맞습니다. Express의 에러 처리 미들웨어는 (error, req, res, next) 형태로 정의되어 있습니다. 첫 번째 파라미터가 Error 객체임을 타입 시스템이 보장해서, error.message나 error.stack 같은 속성에 안전하게 접근할 수 있습니다.
Q. Server 타입을 왜 써야 하나요? app.listen()의 반환값을 그냥 사용하면 안 되나요?
app.listen()은 Server 타입을 반환하지만, 타입을 명시하지 않으면 IDE의 자동 완성이 제대로 작동하지 않습니다. Server 타입을 활용하면 on(), close() 같은 메서드들의 타입이 보장되어, 서버 이벤트 리스너나 그레이스풀 셧다운 로직을 안전하게 작성할 수 있습니다.
Q. NodeJS.Timeout을 왜 import 해야 하나요? 그냥 setTimeout의 반환값을 사용하면 안 되나요?
Node.js의 타이머 함수들은 브라우저와 다른 타입을 가집니다. @types/node에서 제공하는 NodeJS.Timeout을 사용해야, clearTimeout() 호출 시 타입 에러가 발생하지 않습니다. 브라우저의 number 타입과 혼동하지 않도록 주의해야 합니다.
요약
Express와 Node.js의 타입들은 JavaScript로만 개발할 때 겪는 많은 문제를 해결해 줍니다. Request와 Response 타입으로 HTTP 인터페이스를 안전하게 다루고, RequestHandler로 미들웨어 체인의 타입 안전성을 확보하며, Router로 라우팅 구조를 체계적으로 관리할 수 있습니다.
특히 Server 타입으로 서버 생명주기를 관리하고, NodeJS.Timeout으로 타이머를 타입화하면, 런타임 에러의 상당 부분을 컴파일 타임에 잡아낼 수 있습니다.
참조
- Node.js TypeScript 가이드
- Express TypeScript 시작하기 (DefinitelyTyped)
- Express TypeScript 가이드
- express.RequestHandler (공식 시그니처 참고)