JavaScript/Node.js 택배API 연동 가이드
이 가이드에서 다루는 내용
• axios/fetch를 사용한 HTTP 요청
• 배송 조회 API 호출
• 웹훅 수신 서버 구현 (Express.js)
• 에러 처리 패턴
• axios/fetch를 사용한 HTTP 요청
• 배송 조회 API 호출
• 웹훅 수신 서버 구현 (Express.js)
• 에러 처리 패턴
1. 설치
Node.js 프로젝트에서 axios를 설치합니다.
npm install axios 또는 브라우저/Deno에서는 내장 fetch를 사용할 수 있습니다.
2. 환경 변수 설정
API 키를 환경 변수로 관리합니다.
# .env 파일
DELIVERY_PUBLIC_KEY=pk_live_your_public_key
DELIVERY_SECRET_KEY=sk_live_your_secret_key // dotenv 사용 시
require('dotenv').config();
const PUBLIC_KEY = process.env.DELIVERY_PUBLIC_KEY;
const SECRET_KEY = process.env.DELIVERY_SECRET_KEY;
const API_KEY = `${PUBLIC_KEY}:${SECRET_KEY}`; 보안 주의
Secret Key는 절대 클라이언트(브라우저) 코드에 노출하지 마세요. 반드시 서버에서만 사용하세요.
Secret Key는 절대 클라이언트(브라우저) 코드에 노출하지 마세요. 반드시 서버에서만 사용하세요.
3. 배송 조회 (axios)
기본 예제 (단건 조회)
const axios = require('axios');
const PUBLIC_KEY = process.env.DELIVERY_PUBLIC_KEY;
const SECRET_KEY = process.env.DELIVERY_SECRET_KEY;
const BASE_URL = 'https://api.deliveryapi.co.kr/v1';
async function trackDelivery(courierCode, trackingNumber) {
try {
const response = await axios.post(
`${BASE_URL}/tracking/trace`,
{
items: [{
courierCode,
trackingNumber
}]
},
{
headers: {
'Authorization': `Bearer ${PUBLIC_KEY}:${SECRET_KEY}`,
'Content-Type': 'application/json'
}
}
);
// 첫 번째 결과 반환
return response.data.data.results[0];
} catch (error) {
if (error.response) {
console.error('API 에러:', error.response.data);
throw new Error(error.response.data.message);
}
throw error;
}
}
// 사용 예시
trackDelivery('cj', '1234567890')
.then(result => {
if (result.success) {
console.log('배송 상태:', result.data.deliveryStatus);
console.log('현재 위치:', result.data.progresses[0]?.location || '정보 없음');
} else {
console.error('조회 실패:', result.message);
}
})
.catch(err => console.error(err)); 다건 조회 (최대 100건)
async function trackMultiple(items) {
const response = await axios.post(
`${BASE_URL}/tracking/trace`,
{ items },
{
headers: {
'Authorization': `Bearer ${PUBLIC_KEY}:${SECRET_KEY}`,
'Content-Type': 'application/json'
}
}
);
return response.data.data.results;
}
// 사용 예시: 여러 송장 동시 조회
const items = [
{ courierCode: 'cj', trackingNumber: '123456789012' },
{ courierCode: 'lotte', trackingNumber: '987654321098' },
{ courierCode: 'hanjin', trackingNumber: '112233445566' }
];
trackMultiple(items)
.then(results => {
results.forEach(result => {
if (result.success) {
console.log(`${result.data.courierName}: ${result.data.deliveryStatus}`);
} else {
console.error(`조회 실패 (${result.data?.trackingNumber}): ${result.message}`);
}
});
}); fetch 사용 예제
async function trackDelivery(courierCode, trackingNumber) {
const response = await fetch('https://api.deliveryapi.co.kr/v1/tracking/trace', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PUBLIC_KEY}:${SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
items: [{
courierCode,
trackingNumber
}]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const data = await response.json();
return data.data.results[0]; // 첫 번째 결과 반환
} 4. 택배사 목록 조회
async function getCouriers() {
const response = await axios.get(
`${BASE_URL}/tracking/couriers`,
{
headers: {
'Authorization': `Bearer ${PUBLIC_KEY}:${SECRET_KEY}`
}
}
);
return response.data;
}
// 결과 예시
// [
// { code: 'lotte', name: '롯데택배' },
// { code: 'cj', name: 'CJ대한통운' },
// { code: 'hanjin', name: '한진택배' },
// ...
// ] 5. 웹훅 수신 서버 (Express.js)
배송 상태 변경 알림을 받는 웹훅 서버를 구현합니다.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// 서명 검증 함수
function verifySignature(payload, signature, timestamp) {
// timestamp 유효성 검증 (5분 허용)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
// 서명 대상: timestamp.payload
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest('hex');
// 헤더값: sha256={hex} → prefix 제거 후 비교
const received = signature.replace('sha256=', '');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
// 웹훅 엔드포인트 (raw body 필요)
app.post('/webhook/delivery', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
// 서명 검증
if (!verifySignature(req.body.toString(), signature, timestamp)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, items } = JSON.parse(req.body.toString());
for (const item of items) {
switch (event) {
case 'tracking.polled':
console.log(`배송 상태 변경: ${item.trackingNumber}`);
console.log(`새 상태: ${item.currentStatus}`);
// 비즈니스 로직 처리
break;
case 'tracking.completed':
console.log(`전체 배송 완료: ${item.trackingNumber}`);
// 배송 완료 처리
break;
}
}
// 200 응답 (필수)
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
}); 6. 에러 처리
async function trackWithRetry(courier, trackingNumber, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await trackDelivery(courier, trackingNumber);
} catch (error) {
// 재시도 가능한 에러인지 확인
const isRetryable = error.response?.status >= 500
|| error.code === 'ECONNRESET';
if (isRetryable && attempt < maxRetries) {
// 지수 백오프
const delay = Math.pow(2, attempt) * 1000;
console.log(`재시도 ${attempt}/${maxRetries}, ${delay}ms 후...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
throw error;
}
}
}
// 에러 코드별 처리
function handleApiError(error) {
const code = error.response?.data?.code;
switch (code) {
case 'INVALID_TRACKING_NUMBER':
return '잘못된 송장번호입니다';
case 'COURIER_NOT_SUPPORTED':
return '지원하지 않는 택배사입니다';
case 'RATE_LIMIT_EXCEEDED':
return '요청 한도를 초과했습니다';
default:
return '일시적인 오류가 발생했습니다';
}
} 7. TypeScript 타입 정의
interface TrackingResult {
success: boolean;
data: {
courierCode: string;
courierName: string;
trackingNumber: string;
deliveryStatus: 'PENDING' | 'PICKED_UP' | 'IN_TRANSIT' | 'OUT_FOR_DELIVERY' | 'DELIVERED';
deliveryStatusText: string;
isDelivered: boolean;
progresses: Progress[];
};
}
interface Progress {
dateTime: string;
status: string;
statusCode?: string;
location: string;
description: string;
}
interface Courier {
code: string;
name: string;
}
async function trackDelivery(
courier: string,
trackingNumber: string
): Promise<TrackingResult> {
// ... 구현
}