들어가며
이 튜토리얼에서는 Node.js와 Express를 사용하여 Bank API를 연동하는 방법을 알아봅니다. API Key 설정부터 거래내역 조회, 에러 처리, 그리고 프로덕션 배포까지 실전에서 바로 사용할 수 있는 코드를 제공합니다.
- 프로젝트 설정 및 환경 변수 관리
- Bank API 클라이언트 구현
- Express REST API 서버 구축
- 에러 처리 및 Rate Limit 대응
- TypeScript로 타입 안전성 확보
사전 요구사항
- Node.js 18 이상
- Bank API Key (무료 가입: 가입하기)
- 기본적인 JavaScript/TypeScript 지식
1. 프로젝트 설정
프로젝트 초기화
# 프로젝트 폴더 생성
mkdir bank-api-nodejs
cd bank-api-nodejs
# npm 초기화
npm init -y
# 필요한 패키지 설치
npm install express dotenv
# 개발 의존성 (TypeScript 사용 시)
npm install -D typescript ts-node @types/node @types/express nodemon
TypeScript 설정 (선택사항)
# tsconfig.json 생성
npx tsc --init
tsconfig.json을 다음과 같이 수정합니다:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
환경 변수 설정
프로젝트 루트에 .env 파일을 생성합니다:
# .env
BANK_API_KEY=pk_live_your_api_key_here
BANK_API_SECRET=sk_live_your_secret_key_here
BANK_API_URL=https://api.bankapi.co.kr
PORT=3000
NODE_ENV=development
.env 파일은 절대 Git에 커밋하지 마세요.
.gitignore에 .env를 추가하세요.
2. Bank API 클라이언트 구현
기본 클라이언트 클래스
src/lib/bankApiClient.ts 파일을 생성합니다:
// src/lib/bankApiClient.ts
import 'dotenv/config';
interface TransactionRequest {
bankCode: string;
accountNumber: string;
accountPassword: string;
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD
}
interface Transaction {
date: string;
time: string;
description: string;
amount: number;
balance: number;
type: 'deposit' | 'withdrawal';
memo?: string;
}
interface TransactionResponse {
success: boolean;
data?: {
accountNumber: string;
bankCode: string;
transactions: Transaction[];
period: {
start: string;
end: string;
};
};
error?: {
code: string;
message: string;
};
}
interface RateLimitInfo {
limit: number;
remaining: number;
resetTime: number;
}
class BankApiClient {
private apiKey: string;
private secretKey: string;
private baseUrl: string;
private rateLimitInfo: RateLimitInfo | null = null;
constructor() {
this.apiKey = process.env.BANK_API_KEY || '';
this.secretKey = process.env.BANK_API_SECRET || '';
this.baseUrl = process.env.BANK_API_URL || 'https://api.bankapi.co.kr';
if (!this.apiKey || !this.secretKey) {
throw new Error('BANK_API_KEY and BANK_API_SECRET must be set');
}
}
private getAuthHeader(): string {
return `Bearer ${this.apiKey}:${this.secretKey}`;
}
getRateLimitInfo(): RateLimitInfo | null {
return this.rateLimitInfo;
}
async getTransactions(request: TransactionRequest): Promise {
const response = await this.request('/v1/transactions', {
method: 'POST',
body: JSON.stringify(request),
});
return response;
}
private async request(endpoint: string, options: RequestInit): Promise {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': this.getAuthHeader(),
'Content-Type': 'application/json',
...options.headers,
},
});
// Rate Limit 헤더 파싱
this.rateLimitInfo = {
limit: parseInt(response.headers.get('X-RateLimit-Limit') || '60'),
remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '60'),
resetTime: parseInt(response.headers.get('X-RateLimit-Reset') || '0') * 1000,
};
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new BankApiError(
error.message || `API Error: ${response.status}`,
response.status,
error.code,
response.headers.get('Retry-After')
);
}
return response.json();
}
}
class BankApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string,
public retryAfter?: string | null
) {
super(message);
this.name = 'BankApiError';
}
}
// 싱글톤 인스턴스
let clientInstance: BankApiClient | null = null;
export function getBankApiClient(): BankApiClient {
if (!clientInstance) {
clientInstance = new BankApiClient();
}
return clientInstance;
}
export { BankApiClient, BankApiError, TransactionRequest, TransactionResponse, Transaction };
3. Express 서버 구축
메인 서버 파일
src/index.ts 파일을 생성합니다:
// src/index.ts
import express, { Request, Response, NextFunction } from 'express';
import 'dotenv/config';
import { getBankApiClient, BankApiError } from './lib/bankApiClient';
const app = express();
const port = process.env.PORT || 3000;
// 미들웨어
app.use(express.json());
// 요청 로깅 미들웨어
app.use((req: Request, res: Response, next: NextFunction) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
// 라우트: 거래내역 조회
app.post('/api/transactions', async (req: Request, res: Response, next: NextFunction) => {
try {
const { bankCode, accountNumber, accountPassword, startDate, endDate } = req.body;
// 입력 검증
if (!bankCode || !accountNumber || !accountPassword || !startDate || !endDate) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_REQUEST',
message: '필수 파라미터가 누락되었습니다.',
},
});
}
// 날짜 형식 검증 (YYYY-MM-DD)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_DATE_FORMAT',
message: '날짜 형식은 YYYY-MM-DD 입니다.',
},
});
}
const client = getBankApiClient();
const result = await client.getTransactions({
bankCode,
accountNumber,
accountPassword,
startDate,
endDate,
});
// Rate Limit 정보를 응답 헤더에 추가
const rateLimitInfo = client.getRateLimitInfo();
if (rateLimitInfo) {
res.set('X-RateLimit-Limit', rateLimitInfo.limit.toString());
res.set('X-RateLimit-Remaining', rateLimitInfo.remaining.toString());
}
res.json(result);
} catch (error) {
next(error);
}
});
// 라우트: Rate Limit 정보 확인
app.get('/api/rate-limit', (req: Request, res: Response) => {
const client = getBankApiClient();
const rateLimitInfo = client.getRateLimitInfo();
res.json({
success: true,
data: rateLimitInfo || { message: 'No rate limit info available yet' },
});
});
// 헬스 체크
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 에러 핸들링 미들웨어
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Error:', err);
if (err instanceof BankApiError) {
// Bank API 에러 처리
if (err.status === 429) {
res.set('Retry-After', err.retryAfter || '60');
return res.status(429).json({
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'API 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.',
retryAfter: err.retryAfter,
},
});
}
return res.status(err.status).json({
success: false,
error: {
code: err.code || 'API_ERROR',
message: err.message,
},
});
}
// 일반 에러
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: '서버 내부 오류가 발생했습니다.',
},
});
});
// 서버 시작
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
package.json 스크립트 설정
{
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
4. Rate Limit 처리 고도화
실제 서비스에서는 Rate Limit을 더 세밀하게 처리해야 합니다. 지수 백오프와 요청 큐잉을 추가해봅시다.
재시도 로직이 포함된 클라이언트
// src/lib/bankApiClientWithRetry.ts
import { BankApiClient, BankApiError, TransactionRequest, TransactionResponse } from './bankApiClient';
interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
class BankApiClientWithRetry extends BankApiClient {
private retryOptions: RetryOptions;
private requestQueue: Array<{
execute: () => Promise;
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
private isProcessing = false;
private minRequestInterval = 100; // 최소 100ms 간격
constructor(retryOptions?: Partial) {
super();
this.retryOptions = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 30000,
...retryOptions,
};
}
async getTransactionsWithRetry(request: TransactionRequest): Promise {
return this.enqueue(() => this.executeWithRetry(request));
}
private async enqueue(execute: () => Promise): Promise {
return new Promise((resolve, reject) => {
this.requestQueue.push({ execute, resolve, reject });
this.processQueue();
});
}
private async processQueue(): Promise {
if (this.isProcessing || this.requestQueue.length === 0) return;
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const item = this.requestQueue.shift()!;
try {
const result = await item.execute();
item.resolve(result);
} catch (error) {
item.reject(error);
}
// 최소 요청 간격 유지
await this.sleep(this.minRequestInterval);
}
this.isProcessing = false;
}
private async executeWithRetry(request: TransactionRequest): Promise {
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.retryOptions.maxRetries; attempt++) {
try {
return await this.getTransactions(request);
} catch (error) {
if (error instanceof BankApiError) {
lastError = error;
// Rate Limit 에러인 경우
if (error.status === 429) {
const retryAfter = error.retryAfter
? parseInt(error.retryAfter) * 1000
: this.calculateBackoff(attempt);
console.log(`Rate limited. Waiting ${retryAfter}ms before retry ${attempt + 1}/${this.retryOptions.maxRetries}`);
await this.sleep(retryAfter);
continue;
}
// 서버 에러 (5xx)인 경우 재시도
if (error.status >= 500) {
const delay = this.calculateBackoff(attempt);
console.log(`Server error. Waiting ${delay}ms before retry ${attempt + 1}/${this.retryOptions.maxRetries}`);
await this.sleep(delay);
continue;
}
// 클라이언트 에러 (4xx, 429 제외)는 재시도하지 않음
throw error;
}
// 네트워크 에러 등
lastError = error as Error;
const delay = this.calculateBackoff(attempt);
await this.sleep(delay);
}
}
throw lastError || new Error('Max retries exceeded');
}
private calculateBackoff(attempt: number): number {
// 지수 백오프 + 지터
const exponentialDelay = Math.min(
this.retryOptions.baseDelayMs * Math.pow(2, attempt),
this.retryOptions.maxDelayMs
);
const jitter = Math.random() * this.retryOptions.baseDelayMs;
return exponentialDelay + jitter;
}
private sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export { BankApiClientWithRetry };
5. 캐싱 추가
동일한 요청을 반복하면 Rate Limit이 빠르게 소진됩니다. 간단한 메모리 캐시를 추가해봅시다.
// src/lib/cache.ts
interface CacheItem {
data: T;
expiresAt: number;
}
class SimpleCache {
private cache = new Map>();
private defaultTtlMs: number;
constructor(defaultTtlMs = 5 * 60 * 1000) { // 기본 5분
this.defaultTtlMs = defaultTtlMs;
// 만료된 항목 주기적 정리
setInterval(() => this.cleanup(), 60 * 1000);
}
get(key: string): T | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiresAt) {
this.cache.delete(key);
return null;
}
return item.data;
}
set(key: string, data: T, ttlMs?: number): void {
this.cache.set(key, {
data,
expiresAt: Date.now() + (ttlMs || this.defaultTtlMs),
});
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
private cleanup(): void {
const now = Date.now();
for (const [key, item] of this.cache.entries()) {
if (now > item.expiresAt) {
this.cache.delete(key);
}
}
}
}
export { SimpleCache };
캐시를 적용한 서비스 레이어
// src/services/transactionService.ts
import { BankApiClientWithRetry } from '../lib/bankApiClientWithRetry';
import { SimpleCache } from '../lib/cache';
import { TransactionRequest, TransactionResponse } from '../lib/bankApiClient';
import crypto from 'crypto';
class TransactionService {
private client: BankApiClientWithRetry;
private cache: SimpleCache;
constructor() {
this.client = new BankApiClientWithRetry();
this.cache = new SimpleCache(5 * 60 * 1000); // 5분 캐시
}
async getTransactions(request: TransactionRequest): Promise {
// 캐시 키 생성 (비밀번호는 포함하지 않음)
const cacheKey = this.generateCacheKey(request);
// 캐시 확인
const cached = this.cache.get(cacheKey);
if (cached) {
console.log('Cache hit:', cacheKey);
return cached;
}
// API 호출
console.log('Cache miss, calling API:', cacheKey);
const result = await this.client.getTransactionsWithRetry(request);
// 성공한 경우만 캐시
if (result.success) {
this.cache.set(cacheKey, result);
}
return result;
}
private generateCacheKey(request: TransactionRequest): string {
const keyData = `${request.bankCode}:${request.accountNumber}:${request.startDate}:${request.endDate}`;
return crypto.createHash('md5').update(keyData).digest('hex');
}
clearCache(): void {
this.cache.clear();
}
}
// 싱글톤
let serviceInstance: TransactionService | null = null;
export function getTransactionService(): TransactionService {
if (!serviceInstance) {
serviceInstance = new TransactionService();
}
return serviceInstance;
}
export { TransactionService };
6. 테스트
서버 실행
# 개발 모드로 실행
npm run dev
API 테스트 (curl)
# 거래내역 조회
curl -X POST http://localhost:3000/api/transactions \
-H "Content-Type: application/json" \
-d '{
"bankCode": "NH",
"accountNumber": "123-456-789012",
"accountPassword": "1234",
"startDate": "2026-01-01",
"endDate": "2026-01-07"
}'
# Rate Limit 정보 확인
curl http://localhost:3000/api/rate-limit
# 헬스 체크
curl http://localhost:3000/health
성공 응답 예시
{
"success": true,
"data": {
"accountNumber": "123-456-789012",
"bankCode": "NH",
"transactions": [
{
"date": "2026-01-07",
"time": "14:30:00",
"description": "카카오페이",
"amount": -15000,
"balance": 1234567,
"type": "withdrawal",
"memo": "점심 식사"
},
{
"date": "2026-01-06",
"time": "09:00:00",
"description": "급여",
"amount": 3500000,
"balance": 1249567,
"type": "deposit"
}
],
"period": {
"start": "2026-01-01",
"end": "2026-01-07"
}
}
}
7. 프로덕션 배포 팁
환경 변수 관리
프로덕션에서는 .env 파일 대신 시스템 환경 변수나 시크릿 매니저(AWS Secrets Manager, GCP Secret Manager)를 사용하세요.
로깅 및 모니터링
Winston, Pino 같은 로깅 라이브러리를 사용하고, DataDog, New Relic 등으로 모니터링하세요.
분산 캐시
멀티 인스턴스 환경에서는 메모리 캐시 대신 Redis를 사용하세요.
보안 헤더
helmet 미들웨어를 사용해 보안 헤더를 추가하고, CORS를 적절히 설정하세요.
추천 미들웨어
# 프로덕션 권장 패키지
npm install helmet cors compression express-rate-limit
// 프로덕션 미들웨어 설정
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
// 보안 헤더
app.use(helmet());
// CORS
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
methods: ['GET', 'POST'],
}));
// 응답 압축
app.use(compression());
// 서버 자체 Rate Limit (Bank API와 별개)
app.use(rateLimit({
windowMs: 60 * 1000, // 1분
max: 100, // IP당 100회
message: { error: 'Too many requests' },
}));
8. 완성된 프로젝트 구조
bank-api-nodejs/
├── src/
│ ├── index.ts # Express 서버 엔트리
│ ├── lib/
│ │ ├── bankApiClient.ts # 기본 API 클라이언트
│ │ ├── bankApiClientWithRetry.ts # 재시도 로직 포함
│ │ └── cache.ts # 메모리 캐시
│ └── services/
│ └── transactionService.ts # 비즈니스 로직
├── dist/ # 빌드 결과물
├── .env # 환경 변수 (Git 제외)
├── .gitignore
├── package.json
└── tsconfig.json
마무리
이 튜토리얼에서는 Node.js와 Express를 사용하여 Bank API를 연동하는 방법을 알아보았습니다. 기본적인 API 호출부터 Rate Limit 처리, 캐싱, 프로덕션 배포까지 실전에서 필요한 모든 내용을 다루었습니다.
핵심 포인트 요약
- 환경 변수로 API 키를 안전하게 관리하세요
- 지수 백오프로 Rate Limit을 우아하게 처리하세요
- 캐싱으로 불필요한 API 호출을 줄이세요
- 타입스크립트로 타입 안전성을 확보하세요
- 프로덕션에서는 분산 캐시와 모니터링을 추가하세요
다음 글에서는 Python을 사용하여 거래내역을 분석하는 방법을 알아보겠습니다.
지금 바로 시작해보세요
무료 플랜으로 가입하고 이 튜토리얼의 코드를 직접 실행해보세요.
무료로 시작하기 API 문서 보기