728x90
반응형
1. HTTP 클라이언트란
Node.js에서 HTTP 클라이언트는 외부 API를 호출하거나 웹 페이지를 가져오는 데 사용됩니다. 내장 http/https 모듈을 사용하거나, axios, node-fetch 같은 라이브러리를 사용할 수 있습니다.
2. 내장 http/https 모듈
2.1 http.get - 간단한 GET 요청
const https = require('https');
https.get('https://api.github.com/users/octocat', {
headers: { 'User-Agent': 'Node.js' }
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
const user = JSON.parse(data);
console.log('사용자:', user.login);
});
}).on('error', (err) => {
console.error('오류:', err);
});
2.2 http.request - 모든 HTTP 메서드
const https = require('https');
const options = {
hostname: 'jsonplaceholder.typicode.com',
port: 443,
path: '/posts',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Node.js'
}
};
const req = https.request(options, (res) => {
console.log('상태 코드:', res.statusCode);
console.log('헤더:', res.headers);
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
console.log('응답:', JSON.parse(data));
});
});
req.on('error', (err) => {
console.error('요청 오류:', err);
});
// 요청 본문 전송
req.write(JSON.stringify({
title: '새 글',
body: '내용입니다',
userId: 1
}));
req.end();
2.3 Promise로 래핑
const https = require('https');
function httpRequest(options, body = null) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks).toString();
const response = {
statusCode: res.statusCode,
headers: res.headers,
body: data
};
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(response);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (body) {
req.write(typeof body === 'string' ? body : JSON.stringify(body));
}
req.end();
});
}
// 사용
async function fetchUser() {
const response = await httpRequest({
hostname: 'api.github.com',
path: '/users/octocat',
headers: { 'User-Agent': 'Node.js' }
});
return JSON.parse(response.body);
}
3. fetch API (Node.js 18+)
Node.js 18부터 전역 fetch가 내장되었습니다.
3.1 기본 사용법
// GET 요청
const response = await fetch('https://api.github.com/users/octocat', {
headers: { 'User-Agent': 'Node.js' }
});
const data = await response.json();
console.log(data);
// POST 요청
const postResponse = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: '새 글',
body: '내용'
})
});
const result = await postResponse.json();
console.log(result);
3.2 에러 처리
async function fetchWithError(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
try {
const data = await fetchWithError('https://api.example.com/data');
console.log(data);
} catch (err) {
console.error('요청 실패:', err.message);
}
3.3 요청 취소
const controller = new AbortController();
const { signal } = controller;
// 5초 후 취소
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/slow', { signal });
const data = await response.json();
console.log(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('요청이 취소되었습니다');
} else {
throw err;
}
}
3.4 타임아웃 구현
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
const response = await fetchWithTimeout(
'https://api.example.com/data',
{ headers: { 'Accept': 'application/json' } },
3000
);
반응형
4. HTTP 클라이언트 클래스
class HttpClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders
};
}
async request(path, options = {}) {
const url = `${this.baseURL}${path}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
const response = await fetch(url, config);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
error.response = response;
throw error;
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return response.json();
}
return response.text();
}
get(path, options = {}) {
return this.request(path, { ...options, method: 'GET' });
}
post(path, body, options = {}) {
return this.request(path, { ...options, method: 'POST', body });
}
put(path, body, options = {}) {
return this.request(path, { ...options, method: 'PUT', body });
}
patch(path, body, options = {}) {
return this.request(path, { ...options, method: 'PATCH', body });
}
delete(path, options = {}) {
return this.request(path, { ...options, method: 'DELETE' });
}
}
// 사용
const api = new HttpClient('https://jsonplaceholder.typicode.com');
const posts = await api.get('/posts');
const newPost = await api.post('/posts', { title: 'Hello', body: 'World' });
const updated = await api.put('/posts/1', { title: 'Updated' });
await api.delete('/posts/1');
5. 재시도 로직
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// 5xx 에러는 재시도
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
// 4xx 에러는 재시도하지 않음
return response;
} catch (err) {
const isLastAttempt = i === retries - 1;
if (isLastAttempt) {
throw err;
}
console.log(`재시도 ${i + 1}/${retries}...`);
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}
// 지수 백오프
async function fetchWithExponentialBackoff(url, options = {}, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url, options);
} catch (err) {
if (i === maxRetries - 1) throw err;
const delay = Math.min(1000 * Math.pow(2, i), 30000);
const jitter = Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delay + jitter));
}
}
}
6. 동시 요청 처리
6.1 Promise.all
// 모든 요청 병렬 실행
const urls = [
'https://api.example.com/users/1',
'https://api.example.com/users/2',
'https://api.example.com/users/3'
];
const responses = await Promise.all(
urls.map(url => fetch(url).then(res => res.json()))
);
console.log(responses);
6.2 Promise.allSettled
// 일부 실패해도 모든 결과 수집
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(res => res.json()))
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${urls[index]}: 성공`, result.value);
} else {
console.log(`${urls[index]}: 실패`, result.reason);
}
});
6.3 동시성 제한
async function fetchWithConcurrencyLimit(urls, limit = 5) {
const results = [];
const executing = new Set();
for (const url of urls) {
const promise = fetch(url)
.then(res => res.json())
.then(data => {
executing.delete(promise);
return data;
});
results.push(promise);
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/items/${i}`
);
const data = await fetchWithConcurrencyLimit(urls, 10);
7. 스트리밍 응답
async function streamResponse(url) {
const response = await fetch(url);
if (!response.body) {
throw new Error('ReadableStream not supported');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
console.log('청크:', chunk);
}
}
// 파일 다운로드
const fs = require('fs');
const { Readable } = require('stream');
const { pipeline } = require('stream/promises');
async function downloadFile(url, destPath) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const fileStream = fs.createWriteStream(destPath);
await pipeline(Readable.fromWeb(response.body), fileStream);
console.log('다운로드 완료:', destPath);
}
8. 인터셉터 패턴
class HttpClientWithInterceptors {
constructor(baseURL) {
this.baseURL = baseURL;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
addRequestInterceptor(fn) {
this.requestInterceptors.push(fn);
}
addResponseInterceptor(fn) {
this.responseInterceptors.push(fn);
}
async request(path, options = {}) {
let config = { url: `${this.baseURL}${path}`, ...options };
// 요청 인터셉터 실행
for (const interceptor of this.requestInterceptors) {
config = await interceptor(config);
}
let response = await fetch(config.url, config);
// 응답 인터셉터 실행
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response, config);
}
return response;
}
}
// 사용
const client = new HttpClientWithInterceptors('https://api.example.com');
// 요청 로깅
client.addRequestInterceptor(async (config) => {
console.log('요청:', config.method || 'GET', config.url);
return config;
});
// 인증 헤더 추가
client.addRequestInterceptor(async (config) => {
config.headers = {
...config.headers,
Authorization: `Bearer ${getToken()}`
};
return config;
});
// 응답 로깅
client.addResponseInterceptor(async (response, config) => {
console.log('응답:', response.status, config.url);
return response;
});
// 토큰 갱신
client.addResponseInterceptor(async (response, config) => {
if (response.status === 401) {
await refreshToken();
return fetch(config.url, config);
}
return response;
});
9. 캐싱
class CachedHttpClient {
constructor(baseURL, ttl = 60000) {
this.baseURL = baseURL;
this.cache = new Map();
this.ttl = ttl;
}
getCacheKey(path, options) {
return `${options.method || 'GET'}:${path}`;
}
async get(path, options = {}) {
const cacheKey = this.getCacheKey(path, options);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() < cached.expiry) {
console.log('캐시 히트:', path);
return cached.data;
}
const response = await fetch(`${this.baseURL}${path}`, options);
const data = await response.json();
this.cache.set(cacheKey, {
data,
expiry: Date.now() + this.ttl
});
return data;
}
invalidate(path) {
for (const key of this.cache.keys()) {
if (key.includes(path)) {
this.cache.delete(key);
}
}
}
clearCache() {
this.cache.clear();
}
}
const cachedClient = new CachedHttpClient('https://api.example.com', 30000);
const data1 = await cachedClient.get('/users'); // 네트워크 요청
const data2 = await cachedClient.get('/users'); // 캐시에서 반환
결론
Node.js에서 HTTP 클라이언트는 내장 http/https 모듈, Node.js 18+의 fetch API, 또는 axios 같은 라이브러리로 구현할 수 있습니다. 프로덕션 환경에서는 타임아웃, 재시도, 에러 처리, 동시성 제한 등을 구현해야 합니다. fetch API가 Node.js에 내장되면서 별도 라이브러리 없이도 강력한 HTTP 클라이언트를 구축할 수 있게 되었습니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 Express.js 프레임워크 (0) | 2026.03.09 |
|---|---|
| Node.js의 Koa.js 프레임워크 (0) | 2026.03.08 |
| Node.js의 HTTP 서버 만들기 (0) | 2026.03.08 |
| Node.js의 파일 스트림 처리 (0) | 2026.03.07 |
| Node.js의 디렉토리 생성 및 삭제 (0) | 2026.03.07 |