본문으로 건너뛰기

05 HTTP 요청

5장의 목적은 HTTP 클라이언트를 구축하는 방법을 알아본다.

코드 예제

기본 구조

HTTP 클라이언트 애플리케이션의 HTML
<body>
<button data-list>Read Todos list</button>
<button data-add>Add Todo</button>
<button data-update>Update todo</button>
<button data-delete>Delete Todo</button>
<div></div>
</body>
HTTP 클라이언트 애플리케이션의 메인컨트롤러
import todos from './todos.js';

const printResult = (action, result) => {
const time = new Date().toTimeString();
const node = document.createElement('p');
node.textContent = `${action.toUpperCase()}: ${JSON.stringify(
result
)} (${time})`;

document.querySelector('div').appendChild(node);
};

const onListClick = async () => {
const result = await todos.list();
printResult('list todos', result);
};

const onAddClick = async () => {
const result = await todos.create('A simple todo Element');
printResult('add todo', result);
};

const onUpdateClick = async () => {
const list = await todos.list();

const { id } = list[0];
const newTodo = {
id,
completed: true,
};

const result = await todos.update(newTodo);
printResult('update todo', result);
};

const onDeleteClick = async () => {
const list = await todos.list();
const { id } = list[0];

const result = await todos.delete(id);
printResult('delete todo', result);
};

document
.querySelector('button[data-list]')
.addEventListener('click', onListClick);

document
.querySelector('button[data-add]')
.addEventListener('click', onAddClick);

document
.querySelector('button[data-update]')
.addEventListener('click', onUpdateClick);

document
.querySelector('button[data-delete]')
.addEventListener('click', onDeleteClick);

이 컨트롤러에서는 HTTP 클라리언트를 직접 사용하는 대신 HTTP 요청을 todos 모델 객체에 래핑했다. 이런 캡슐화는 여러 가지 이유로 유용하다.

한 가지 이유는 테스트 가능성이다. todos 객체를 정적 데이터 세트(fixture)를 반환하는 모의(mock)으로 바꿀 수 있다. 이런 식으로 컨트롤러를 독립적으로 테스트할 수 있다.

또 다른 이유는 가독성이다. 모델 객체는 코드를 좀 더 명확하게 만든다.

노트

컨트롤러에서 HTTP 클라이언트를 직접 사용하지 않는다. 이런 함수는 모델 객체에서 캡슐화하는 것이 좋다.

todos 모델 객체
import http from './http.js';

const HEADERS = {
'Content-Type': 'application/json',
};

const BASE_URL = '/api/todos';

const list = () => http.get(BASE_URL);

const create = (text) => {
const todo = {
text,
completed: false,
};

return http.post(BASE_URL, todo, HEADERS);
};

const update = (newTodo) => {
const url = `${BASE_URL}/${newTodo.id}`;
return http.patch(url, newTodo, HEADERS);
};

const deleteTodo = (id) => {
const url = `${BASE_URL}/${id}`;
return http.delete(url);
};

export default {
list,
create,
update,
delete: deleteTodo,
};

XMLHttpRequest

XMLHttpRequest는 W3C가 비동기 HTTP 요청의 표준 방법을 정의한 첫 번째 시도이다.

XMLHttpRequest를 사용하는 HTTP 클라이언트
const setHeaders = (xhr, headers) => {
Object.entries(headers).forEach((entry) => {
const [name, value] = entry;

xhr.setRequestHeader(name, value);
});
};

const parseResponse = (xhr) => {
const { status, responseText } = xhr;

let data;
if (status !== 204) {
data = JSON.parse(responseText);
}

return {
status,
data,
};
};

const request = (params) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

const { method = 'GET', url, headers = {}, body } = params;

xhr.open(method, url);

setHeaders(xhr, headers);

xhr.send(JSON.stringify(body));

xhr.onerror = () => {
reject(new Error('HTTP Error'));
};

xhr.ontimeout = () => {
reject(new Error('Timeout Error'));
};

xhr.onload = () => resolve(parseResponse(xhr));
});
};

const get = async (url, headers) => {
const response = await request({
url,
headers,
method: 'GET',
});

return response.data;
};

const post = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'POST',
body,
});
return response.data;
};

const put = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PUT',
body,
});
return response.data;
};

const patch = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PATCH',
body,
});
return response.data;
};

const deleteRequest = async (url, headers) => {
const response = await request({
url,
headers,
method: 'DELETE',
});
return response.data;
};

export default {
get,
post,
put,
patch,
delete: deleteRequest,
};

HTTP 클라이언트의 핵심은 request 메서드다. XMLHttpRequest는 2006년에 정의된 API로 콜백을 기반으로 한다. 완료된 요청에 대한 onload 콜백, 오류로 끝나는 HTTP에 대한 onerror 콜백과 타임아웃된 요청에 대한 ontimeout 콜백이 있다.

HTTP 클라이언트의 공개 API는 프라미스를 기반으로 한다. 따라서 request 메서드는 표준 XMLHttpRequest 요청을 새로운 Promise 객체로 묶는다. 공개 메서드 get, post, put, patch, delete는 코드를 더 읽기 쉽게 해주는 request 메서드의 래퍼다.

다음은 XMLHttpRequest를 사용한 HTTP 요청의 흐름을 보여준다.

  1. 새로운 XMLHttpRequest 객체 생성 new XMLHttpRequest()
  2. 특정 URL로 요청을 초기화 xhr.open(method, url)
  3. 요청(헤더 설정, 타임아웃 등)을 구성
  4. 요청 전송 xhr.send(JSON.stringify(body))
  5. 요청이 끝날 때까지 대기
    • 요청이 성공적으로 끝나면 onload 콜백 호출
    • 요청이 오류로 끝나면 onerror 콜백 호출
    • 요청이 타임아웃으로 끝나면 ontimeout 콜백 호출

Fetch

Fetch는 원격 리소스에 접근하고자 만들어진 새로운 API이다. 이 API의 목적은 Requets나 Response 같은 많은 네트워크에 대한 표준 정의를 제공하는 것이다. 이런 방식으로 이 객체는 ServiceWorkder와 Cache 같은 다른 API와 상호 운용할 수 있다.

요청을 생성하려면 Fetch API로 작성된 HTTP 클라이언트의 구현인 window.fetch 메서드를 사용해야 한다.

Fetch API를 기반으로 하는 HTTP 클라이언트
const parsePresponse = async (response) => {
const { status } = response;
let data;
if (staus !== 204) {
// data = JSON.parse(responseText); 가 다음처럼 바뀜
data = await response.json();
}
return {
status,
data,
};
};

const request = async (params) => {
const { method = 'GET', url, headers = {}, body } = params;
const config = {
method,
headers: new window.Headers(headers),
};

if (body) {
config.body = JSON.stringify(body);
}

const response = await window.fetch(url, config);
return parseResponse(response);
};
const get = async (url, headers) => {
const response = await request({
url,
headers,
method: 'GET',
});
return response.dta;
};
const post = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'POST',
body,
});
return response.data;
};
const put = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PUT',
body,
});
return response.data;
};
const patch = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PATCH',
body,
});
return response.data;
};
const deleteRequest = async (url, headers) => {
const response = await request({
url,
headers,
method: 'DELETE',
});
return response.data;
};
export default {
get,
post,
put,
patch,
delete: deleteRequest,
};

이 HTTP 클라이언트는 XMLHttpRequest와 동일한 공용 API를 가진다. 두 번째 클라이언트의 코드는 window.fetch가 Promise 객체를 반환하기 때문에 훨씬 더 읽기 쉽다. 따라서 전통적인 콜백 기반의 XMLHttpRequest 접근 방식을 최신의 프라미스 기반으로 변환하기 위한 보일러플레이트 코드가 필요하지 않다.

window.fetch에서 반환된 프라미스는 Response 객체를 resolve한다. 이 객체를 사용해 서버가 보낸 응답 본문을 추출할 수 있다. 수신된 데이터의 형식에 따라 text(), blob(), json() 같은 다른 메서드를 사용한다.

Axios

axios는 오픈소스 라이브러리이다.

axios와 다른 방식과의 가장 큰 차이는 브라우저와 Node.js에서 바로 사용할 수 있다는 것이다. axios의 API는 프라미스를 기반으로 하고 있어 Fetch API와 매우 유사하다.

axios API를 기반으로 하는 HTTP 클라이언트
const parsePresponse = async (response) => {
const { status } = response;
let data;
if (staus !== 204) {
data = await response.json();
}
return {
status,
data,
};
};
const request = async (params) => {
const { method = 'GET', url, headers = {}, body } = params;
const config = {
url,
method,
headers,
data: body,
};

return axios(config);
};
const get = async (url, headers) => {
const response = await request({
url,
headers,
method: 'GET',
});
return response.dta;
};
const post = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'POST',
body,
});
return response.data;
};
const put = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PUT',
body,
});
return response.data;
};
const patch = async (url, body, headers) => {
const response = await request({
url,
headers,
method: 'PATCH',
body,
});
return response.data;
};
const deleteRequest = async (url, headers) => {
const response = await request({
url,
headers,
method: 'DELETE',
});
return response.data;
};
export default {
get,
post,
put,
patch,
delete: deleteRequest,
};

세 가지 HTTP 클라이언트 버전 중에서 가장 읽기 쉽다.

적합한 HTTP API를 선택하는 방법

호환성

비즈니스에서 인터넷 익스플로러의 지원이 중요하다면 Fetch API는 최신 브라우저에서만 동작하기 때문에 axiosXMLHttpRequest를 사용해야한다.

  • axios는 인터넷 익스플로러 11을 지원
  • 이전 버전의 인터넷 익스플로러에서 동작해야 하는 경우 XMLHttpRequest를 사용해야 한다.

휴대성

  • Fetch APIXMLHttpRequest는 모두 브라우저에서만 동작한다.
  • Node.js리액트 네이티브 같은 다른 자바스크립트 환경에서 코드를 실행해야 하는 경우 axios가 매우 좋은 솔루션이다.

발전성

Fetch API의 가장 중요한 기능 중 하나는 Request나 Response 같은 네트워크 관련 객체의 표준 정의를 제공하는 것이다.
이 특성은 ServiceWorker API나 Cache API와 잘맞기 때문에 코드베이스를 빠르게 발전시키고자 하는 경우 Fetch API를 매우 유용한 라이브러리로 만들어준다.

보안

axios에는 교차 사이트 요청위조나 XSRF에 대한 보호 시스템이 내장돼 있다.

러닝 커브

주니어는 콜백 작업에 익숙지 않기 때문에 XMLHttpRequest가 조금 이상하게 보일 수 있다.