Fetch API 사용하기
Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공합니다. Fetch API가 제공하는 전역 fetch()
메서드로 네트워크의 리소스를 쉽게 비동기적으로 취득할 수도 있습니다.
콜백 기반 API인 XMLHttpRequest
와 달리, Fetch API는 서비스 워커에서도 쉽게 사용할 수 있는 프로미스 기반의 개선된 대체제입니다. 또한 Fetch API는 CORS를 포함한 고급 개념을 HTTP 확장으로 정의합니다.
기본적인 리소스 취득 요청은 이렇게 생겼습니다.
async function logJSONData() {
const response = await fetch("http://example.com/movies.json");
const jsonData = await response.json();
console.log(jsonData);
}
위 코드는 네트워크를 통해 JSON 파일을 취득해서 콘솔에 출력합니다. 가장 단순한 형태의 fetch()
는 가져오고자 하는 리소스의 경로를 나타내는 하나의 인수만 받습니다. 응답은 Response
객체로 표현되며, JSON 응답 본문을 바로 반환하지는 않습니다.
Response
객체 역시 JSON 응답 본문을 그대로 포함하지는 않습니다. Response
는 HTTP 응답 전체를 나타내는 객체로, JSON 본문 콘텐츠를 추출하기 위해서는 json()
메서드를 호출해야 합니다. json()
은 응답 본문 텍스트를 JSON으로 파싱한 결과로 이행하는, 또 다른 프로미스를 반환합니다.
참고: 다른 유형의 본문 콘텐츠를 추출할 수 있는 비슷한 메서드들을 본문 절에서 확인하세요.
취득 요청은 가져오려는 리소스의 지시문이 아닌, Content-Security-Policy
헤더의 connect-src
지시문에 의해 제어됩니다.
요청 옵션 제공
fetch()
메서드에는 선택적으로 두 번째 매개변수도 제공할 수 있습니다. 이 매개변수, init
객체를 사용하면 여러가지 설정을 제어할 수 있습니다.
fetch()
문서를 방문해 사용 가능한 전체 옵션의 목록과, 각각의 옵션에 대한 자세한 설명을 확인하세요.
// POST 메서드 구현 예제
async function postData(url = "", data = {}) {
// 옵션 기본 값은 *로 강조
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE 등
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data), // body의 데이터 유형은 반드시 "Content-Type" 헤더와 일치해야 함
});
return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
}
postData("https://example.com/answer", { answer: 42 }).then((data) => {
console.log(data); // JSON 데이터가 `data.json()` 호출에 의해 파싱됨
});
mode: "no-cors"
를 지정하면 요청에 사용할 수 있는 헤더가 다음 목록으로 제한됩니다.
Accept
Accept-Language
Content-Language
Content-Type
, 값으로는application/x-www-form-urlencoded
,multipart/form-data
, 또는text/plain
취득 요청 중단
아직 완료되지 않은 fetch()
작업을 취소하려면 AbortController
와 AbortSignal
인터페이스를 사용하세요.
const controller = new AbortController();
const signal = controller.signal;
const url = "video.mp4";
const downloadBtn = document.querySelector("#download");
const abortBtn = document.querySelector("#abort");
downloadBtn.addEventListener("click", async () => {
try {
const response = await fetch(url, { signal });
console.log("다운로드 완료", response);
} catch (error) {
console.error(`다운로드 오류: ${error.message}`);
}
});
abortBtn.addEventListener("click", () => {
controller.abort();
console.log("다운로드 중단됨");
});
자격 증명을 포함한 요청 전송
브라우저가 요청을 전송할 때 자격 증명을 포함하도록 하려면 fetch()
메서드에 전달하는 init
객체에 credential: 'include'
를 추가하세요. 동일 출처와 교차 출처 요청 모두에 사용할 수 있습니다.
fetch("https://example.com", {
credentials: "include",
});
참고: credentials: 'include'
를 추가한 경우, Access-Control-Allow-Origin
에 와일드카드를 사용할 수 없습니다. 자격 증명을 포함하려는 경우에는 반드시 정확한 출처를 지정해야 합니다. CORS 해제 확장 프로그램을 사용하더라도 와일드카드를 지정한 요청은 실패할 것입니다.
참고: 자격 증명 옵션의 값에 상관 없이, 브라우저는 프리플라이트 요청에는 자격 증명을 전송하지 않아야 합니다. 자세한 정보는 자격 증명을 포함한 CORS 요청을 참고하세요.
요청 URL이 스크립트와 같은 출처일 때만 자격 증명을 전송하려면 credentials: 'same-origin'
을 추가하세요.
// 스크립트의 출처도 'https://example.com'
fetch("https://example.com", {
credentials: "same-origin",
});
브라우저가 요청에서 자격 증명을 전송하지 않도록 하려면 credentials: 'omit'
을 사용하세요.
fetch("https://example.com", {
credentials: "omit",
});
JSON 데이터 업로드
fetch()
를 사용하면 JSON 인코딩된 데이터를 POST 요청에 포함할 수 있습니다.
async function postJSON(data) {
try {
const response = await fetch("https://example.com/profile", {
method: "POST", // 또는 'PUT'
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("성공:", result);
} catch (error) {
console.error("실패:", error);
}
}
const data = { username: "example" };
postJSON(data);
파일 업로드
fetch()
와 <input type="file">
입력 요소, FormData()
를 사용해서 파일을 업로드할 수 있습니다.
async function upload(formData) {
try {
const response = await fetch("https://example.com/profile/avatar", {
method: "PUT",
body: formData,
});
const result = await response.json();
console.log("성공:", result);
} catch (error) {
console.error("실패:", error);
}
}
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append("username", "abc123");
formData.append("avatar", fileField.files[0]);
upload(formData);
다수의 파일 업로드
fetch()
와 <input type="file" multiple>
입력 칸 요소, FormData()
를 사용해서 여러 파일을 업로드할 수 있습니다.
async function uploadMultiple(formData) {
try {
const response = await fetch("https://example.com/posts", {
method: "POST",
body: formData,
});
const result = await response.json();
console.log("성공:", result);
} catch (error) {
console.error("실패:", error);
}
}
const photos = document.querySelector('input[type="file"][multiple]');
const formData = new FormData();
formData.append("title", "My Vegas Vacation");
for (const [i, photo] of Array.from(photos.files).entries()) {
formData.append(`photos_${i}`, photo);
}
uploadMultiple(formData);
텍스트 파일을 한 줄씩 처리하기
응답에서 읽어오는 데이터 청크는 줄 단위로 깔끔하게 나뉘지 않으며, 문자열도 아니고 Uint8Array
입니다. 텍스트 파일을 가져와서 줄 단위로 처리하고자 한다면, '줄' 단위로 나누는 작업은 직접 구현해야 합니다. 이 예제는 줄 단위 반복기를 생성해서 파일을 처리하는 예시입니다. (너무 복잡해지지 않도록 텍스트 파일은 UTF-8로 가정하고, 네트워크 오류는 고려하지 않습니다.)
async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder("utf-8");
const response = await fetch(fileURL);
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk) : "";
const re = /\n|\r|\r\n/gm;
let startIndex = 0;
let result;
for (;;) {
let result = re.exec(chunk);
if (!result) {
if (readerDone) {
break;
}
let remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
startIndex = re.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = re.lastIndex;
}
if (startIndex < chunk.length) {
// 마지막 줄이 개행 문자로 끝나지 않았을 때
yield chunk.substr(startIndex);
}
}
async function run() {
for await (let line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
run();
취득 성공 여부 확인
fetch()
프로미스는 네트워크에 오류가 있었거나, 서버의 CORS 설정이 잘못된 경우 TypeError
로 거부됩니다. 그러나 이 두 경우는 권한처럼 설정의 문제고, 404와 같은 응답은 네트워크 오류가 아니므로 거부하지 않습니다. fetch()
가 성공했는지를 정확히 알아내려면 프로미스의 이행 여부를 확인한 후, Response.ok
속성의 값이 true
인지도 확인해야 합니다.
async function fetchImage() {
try {
const response = await fetch("flowers.jpg");
if (!response.ok) {
throw new Error("네트워크 응답이 OK가 아님");
}
const myBlob = await response.blob();
myImage.src = URL.createObjectURL(myBlob);
} catch (error) {
console.error("취득에 문제가 있었습니다:", error);
}
}
요청 객체를 직접 제공
fetch()
호출에 리소스의 경로를 제공하는 대신, Request()
생성자로 생성한 요청 객체를 인자로 전달할 수도 있습니다.
async function fetchImage(request) {
try {
const response = await fetch(request);
if (!response.ok) {
throw new Error("네트워크 응답이 OK가 아님");
}
const myBlob = await response.blob();
myImage.src = URL.createObjectURL(myBlob);
} catch (error) {
console.error("오류:", error);
}
}
const myHeaders = new Headers();
const myRequest = new Request("flowers.jpg", {
method: "GET",
headers: myHeaders,
mode: "cors",
cache: "default",
});
fetchImage(myRequest);
Request()
생성자는 fetch()
메서드와 동일한 매개변수를 받습니다. 기존에 존재하는 요청 객체를 전달해서 복사본을 생성하는 것도 가능합니다.
const anotherRequest = new Request(myRequest, myInit);
요청과 응답 본문은 한 번만 읽을 수 있으므로 복사본 생성은 꽤 유용합니다. 이런 식으로 복사본을 생성하면, 기존에 생성해둔 요청/응답 객체를 다시 사용하되 init
옵션만 교체할 수도 있습니다. 복사본은 원본의 본문을 읽기 전에 생성해야 합니다.
참고: 복사본 생성만을 위한 clone()
메서드도 있습니다. 생성자와 이 메서드 모두, 이미 본문을 읽은 요청 또는 응답을 복사하려고 시도하면 실패합니다. 복사본의 본문을 읽어도 원본에는 영향을 주지 않습니다.
헤더
Headers
인터페이스의 Headers()
생성자를 사용해서 자신만의 헤더 객체를 생성할 수 있습니다. 헤더 객체는 이름과 값을 연결하는 간단한 맵입니다.
const content = "Hello World";
const myHeaders = new Headers();
myHeaders.append("Content-Type", "text/plain");
myHeaders.append("Content-Length", content.length.toString());
myHeaders.append("X-Custom-Header", "ProcessThisImmediately");
생성자에 2차원 배열이나 객체 리터럴을 전달하는 것으로도 같은 결과를 얻을 수 있습니다.
const myHeaders = new Headers({
"Content-Type": "text/plain",
"Content-Length": content.length.toString(),
"X-Custom-Header": "ProcessThisImmediately",
});
헤더의 내용은 아래와 같이 가져올 수 있습니다.
console.log(myHeaders.has("Content-Type")); // true
console.log(myHeaders.has("Set-Cookie")); // false
myHeaders.set("Content-Type", "text/html");
myHeaders.append("X-Custom-Header", "AnotherValue");
console.log(myHeaders.get("Content-Length")); // 11
console.log(myHeaders.get("X-Custom-Header")); // ['ProcessThisImmediately', 'AnotherValue']
myHeaders.delete("X-Custom-Header");
console.log(myHeaders.get("X-Custom-Header")); // null
헤더 객체의 몇몇 작업은 서비스 워커에서나 유용하지만, 그래도 훨씬 편한 API를 통해서 헤더를 조작할 수 있습니다.
Headers
의 모든 헤더 메서드는 유효하지 않은 HTTP 헤더 이름을 받았을 때 TypeError
를 던집니다. 변경 메서드는 불변 보호(아래 참고)가 존재하면 TypeError
를 던집니다. 그 외에는 실패할 때 조용하게 실패합니다. 다음은 조용한 실패 코드의 예시입니다.
const myResponse = Response.error();
try {
myResponse.headers.set("Origin", "http://mybank.com");
} catch (e) {
console.log("은행인 척 할 수 없어요!");
}
콘텐츠를 파싱하기 전에 유효한 형식인지 확인할 때 헤더 객체를 유용하게 사용할 수 있습니다. 다음은 그 예시입니다.
async function fetchJSON(request) {
try {
const response = await fetch(request);
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new TypeError("앗, JSON이 아닙니다!");
}
const jsonData = await response.json();
// 데이터 추가 가공
} catch (error) {
console.error("오류:", error);
}
}
가드
헤더는 요청으로 전송할 수도 있고 응답으로 받을 수도 있으며 어떤 정보를 수정할 수 있고 수정할 수 없는지 다양한 제한이 존재하기 때문에, 헤더 객체는 '가드'(guard) 속성을 갖습니다. 가드 속성은 웹에 노출되진 않지만, 헤더 객체에 허용되는 변경 작업의 범위에 영향을 줍니다.
가능한 가드 값은 다음과 같습니다.
none
: 기본 값입니다.request
: 요청(Request.headers
)에서 획득한 헤더 객체를 보호합니다.request-no-cors
:Request.mode
가no-cors
인 요청에서 획득한 헤더 객체를 보호합니다.response
: 응답(Response.headers
)에서 획득한 헤더 객체를 보호합니다.immutable
: 헤더 객체를 읽기 전용으로 설정합니다. 대부분 서비스 워커에서 사용합니다.
참고: 가드가 적용된 응답 헤더에는 Content-Length
헤더를 추가하거나 설정할 수 없습니다. 마찬가지로,Set-Cookie
헤더 또한 응답에 추가할 수 없습니다. 즉, 서비스 워커에서 응답을 합성해서 쿠키를 설정하는 것은 불가능합니다.
응답 객체
위에서 본 바와 같이 Response
인스턴스는 fetch()
프로미스가 이행할 때 반환됩니다.
다음은 응답 객체에서 아마 가장 많이 사용하게 될 속성들입니다.
Response.status
— 상태 코드 값을 담은 정수 값입니다. 기본 값은 200입니다.Response.statusText
— 상태 코드 메시지를 담은 문자열 값입니다. 기본 값은 빈 문자열입니다. 참고로 HTTP/2는 상태 메시지를 지원하지 않습니다.Response.ok
— 위쪽 예제에서 사용했듯, 상태 코드가 200 이상 299 이하인지 간단하게 확인할 수 있는 불리언 값입니다.
응답 객체는 JavaScript에서 직접 생성할 수도 있지만, 이런 응답은 수신한 요청에 대해 respondWith()
메서드로 직접 응답해야 하는 서비스 워커에서나 활약할 수 있습니다.
const myBody = new Blob();
addEventListener("fetch", function (event) {
// fetch를 가로채는 ServiceWorker
event.respondWith(
new Response(myBody, {
headers: { "Content-Type": "text/plain" },
}),
);
});
Response()
생성자는 두 개의 선택적 인자를 받습니다. 하나는 응답 본문으로 쓰고, 다른 하나는Request()
가 받는 것과 비슷한 옵션 객체입니다.
참고: error()
정적 메서드는 오류 응답을 반환합니다. 마찬가지로, redirect()
는 지정한 URL로 리다이렉트를 유발하는 응답을 생성합니다. 이 두 메서드 역시 서비스 워커에서만 의미가 있습니다.
본문
요청과 응답 모두 본문 데이터를 포함할 수 있습니다. 본문 데이터는 아래 목록의 유형 중 하나의 인스턴스입니다.
ArrayBuffer
TypedArray
(Uint8Array 등등)DataView
Blob
File
String
또는 문자열 리터럴URLSearchParams
FormData
Request
와 Response
인터페이스는 본문을 추출할 수 있는 다음의 메서드들을 공유합니다. 추출 메서드는 모두 프로미스를 반환하며, 이 프로미스가 실제 본문 데이터로 이행합니다.
Request.arrayBuffer()
/Response.arrayBuffer()
Request.blob()
/Response.blob()
Request.formData()
/Response.formData()
Request.json()
/Response.json()
Request.text()
/Response.text()
본문 추출 메서드들을 사용하면 XHR을 사용할 때보다 더 쉽게 비 텍스트 데이터를 처리할 수 있습니다.
요청 본문은 body
속성을 설정하는 것으로 추가할 수 있습니다.
const form = new FormData(document.getElementById("login-form"));
fetch("/login", {
method: "POST",
body: form,
});
요청과 응답, 그리고 더 나아가 fetch()
함수는 본문을 보고 콘텐츠 유형을 알아내려고 시도합니다. 또한, 요청은 따로 명시하지 않았으면 Content-Type
을 헤더를 자동으로 설정합니다.