Fetch API 사용하기

Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공합니다. Fetch API가 제공하는 전역 fetch() 메서드로 네트워크의 리소스를 쉽게 비동기적으로 취득할 수도 있습니다.

콜백 기반 API인 XMLHttpRequest와 달리, Fetch API는 서비스 워커에서도 쉽게 사용할 수 있는 프로미스 기반의 개선된 대체제입니다. 또한 Fetch API는 CORS를 포함한 고급 개념을 HTTP 확장으로 정의합니다.

기본적인 리소스 취득 요청은 이렇게 생겼습니다.

js
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() 문서를 방문해 사용 가능한 전체 옵션의 목록과, 각각의 옵션에 대한 자세한 설명을 확인하세요.

js
// 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() 작업을 취소하려면 AbortControllerAbortSignal 인터페이스를 사용하세요.

js
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'를 추가하세요. 동일 출처와 교차 출처 요청 모두에 사용할 수 있습니다.

js
fetch("https://example.com", {
  credentials: "include",
});

참고: credentials: 'include'를 추가한 경우, Access-Control-Allow-Origin에 와일드카드를 사용할 수 없습니다. 자격 증명을 포함하려는 경우에는 반드시 정확한 출처를 지정해야 합니다. CORS 해제 확장 프로그램을 사용하더라도 와일드카드를 지정한 요청은 실패할 것입니다.

참고: 자격 증명 옵션의 값에 상관 없이, 브라우저는 프리플라이트 요청에는 자격 증명을 전송하지 않아야 합니다. 자세한 정보는 자격 증명을 포함한 CORS 요청을 참고하세요.

요청 URL이 스크립트와 같은 출처일 때만 자격 증명을 전송하려면 credentials: 'same-origin'을 추가하세요.

js
// 스크립트의 출처도 'https://example.com'

fetch("https://example.com", {
  credentials: "same-origin",
});

브라우저가 요청에서 자격 증명을 전송하지 않도록 하려면 credentials: 'omit'을 사용하세요.

js
fetch("https://example.com", {
  credentials: "omit",
});

JSON 데이터 업로드

fetch()를 사용하면 JSON 인코딩된 데이터를 POST 요청에 포함할 수 있습니다.

js
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()를 사용해서 파일을 업로드할 수 있습니다.

js
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()를 사용해서 여러 파일을 업로드할 수 있습니다.

js
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로 가정하고, 네트워크 오류는 고려하지 않습니다.)

js
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인지도 확인해야 합니다.

js
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() 생성자로 생성한 요청 객체를 인자로 전달할 수도 있습니다.

js
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() 메서드와 동일한 매개변수를 받습니다. 기존에 존재하는 요청 객체를 전달해서 복사본을 생성하는 것도 가능합니다.

js
const anotherRequest = new Request(myRequest, myInit);

요청과 응답 본문은 한 번만 읽을 수 있으므로 복사본 생성은 꽤 유용합니다. 이런 식으로 복사본을 생성하면, 기존에 생성해둔 요청/응답 객체를 다시 사용하되 init 옵션만 교체할 수도 있습니다. 복사본은 원본의 본문을 읽기 전에 생성해야 합니다.

참고: 복사본 생성만을 위한 clone() 메서드도 있습니다. 생성자와 이 메서드 모두, 이미 본문을 읽은 요청 또는 응답을 복사하려고 시도하면 실패합니다. 복사본의 본문을 읽어도 원본에는 영향을 주지 않습니다.

헤더

Headers 인터페이스의 Headers() 생성자를 사용해서 자신만의 헤더 객체를 생성할 수 있습니다. 헤더 객체는 이름과 값을 연결하는 간단한 맵입니다.

js
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차원 배열이나 객체 리터럴을 전달하는 것으로도 같은 결과를 얻을 수 있습니다.

js
const myHeaders = new Headers({
  "Content-Type": "text/plain",
  "Content-Length": content.length.toString(),
  "X-Custom-Header": "ProcessThisImmediately",
});

헤더의 내용은 아래와 같이 가져올 수 있습니다.

js
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를 던집니다. 그 외에는 실패할 때 조용하게 실패합니다. 다음은 조용한 실패 코드의 예시입니다.

js
const myResponse = Response.error();
try {
  myResponse.headers.set("Origin", "http://mybank.com");
} catch (e) {
  console.log("은행인 척 할 수 없어요!");
}

콘텐츠를 파싱하기 전에 유효한 형식인지 확인할 때 헤더 객체를 유용하게 사용할 수 있습니다. 다음은 그 예시입니다.

js
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.modeno-cors인 요청에서 획득한 헤더 객체를 보호합니다.
  • response: 응답(Response.headers)에서 획득한 헤더 객체를 보호합니다.
  • immutable: 헤더 객체를 읽기 전용으로 설정합니다. 대부분 서비스 워커에서 사용합니다.

참고: 가드가 적용된 응답 헤더에는 Content-Length 헤더를 추가하거나 설정할 수 없습니다. 마찬가지로,Set-Cookie 헤더 또한 응답에 추가할 수 없습니다. 즉, 서비스 워커에서 응답을 합성해서 쿠키를 설정하는 것은 불가능합니다.

응답 객체

위에서 본 바와 같이 Response 인스턴스는 fetch() 프로미스가 이행할 때 반환됩니다.

다음은 응답 객체에서 아마 가장 많이 사용하게 될 속성들입니다.

응답 객체는 JavaScript에서 직접 생성할 수도 있지만, 이런 응답은 수신한 요청에 대해 respondWith() 메서드로 직접 응답해야 하는 서비스 워커에서나 활약할 수 있습니다.

js
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로 리다이렉트를 유발하는 응답을 생성합니다. 이 두 메서드 역시 서비스 워커에서만 의미가 있습니다.

본문

요청과 응답 모두 본문 데이터를 포함할 수 있습니다. 본문 데이터는 아래 목록의 유형 중 하나의 인스턴스입니다.

RequestResponse 인터페이스는 본문을 추출할 수 있는 다음의 메서드들을 공유합니다. 추출 메서드는 모두 프로미스를 반환하며, 이 프로미스가 실제 본문 데이터로 이행합니다.

본문 추출 메서드들을 사용하면 XHR을 사용할 때보다 더 쉽게 비 텍스트 데이터를 처리할 수 있습니다.

요청 본문은 body 속성을 설정하는 것으로 추가할 수 있습니다.

js
const form = new FormData(document.getElementById("login-form"));
fetch("/login", {
  method: "POST",
  body: form,
});

요청과 응답, 그리고 더 나아가 fetch() 함수는 본문을 보고 콘텐츠 유형을 알아내려고 시도합니다. 또한, 요청은 따로 명시하지 않았으면 Content-Type을 헤더를 자동으로 설정합니다.

기능 감지

Fetch API는 WindowWorker 스코프에 Headers, Request, Response, 또는 fetch()의 존재 여부로 지원 여부를 확인할 수 있습니다.

js
if (window.fetch) {
  // fetch로 요청 보내기
} else {
  // XMLHttpRequest 사용하기?
}

같이 보기