들어가며

이 튜토리얼에서는 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 문서 보기