Fetch API의 모든 것

웹 개발의 핵심, 비동기 통신을 담당하는 Fetch API를 권퓨터와 함께 완벽하게 파헤쳐 봅시다!

최신 웹 애플리케이션 개발에서 서버와 데이터를 주고받는 비동기 통신은 필수적인 요소입니다. 자바스크립트의 Fetch API는 기존의 XMLHttpRequest를 대체하며 더욱 현대적이고 강력한 기능을 제공합니다.

이 가이드에서는 Fetch API의 기본 사용법부터 고급 기능, 그리고 실제 프로젝트에 적용할 때 필요한 주의사항과 팁까지 권퓨터가 자세히 알려드립니다. 웹 개발 효율을 높이고 더 안정적인 애플리케이션을 구축하는 데 필요한 모든 정보를 얻어가세요.

Fetch API, 왜 알아야 할까요?

Fetch API, 왜 알아야 할까요?

웹 애플리케이션은 사용자 인터랙션에 따라 동적으로 데이터를 불러오고 업데이트해야 합니다. 이때 서버와 통신하는 방식이 바로 ‘비동기 통신’입니다. Fetch API는 이런 비동기 통신을 위한 최신 표준 인터페이스로, 웹 개발의 생산성과 코드 가독성을 크게 향상시킵니다.

기존의 XMLHttpRequest (XHR) 방식은 콜백 지옥(Callback Hell)과 복잡한 에러 처리, 그리고 직관적이지 않은 문법으로 인해 개발자들에게 많은 어려움을 주었습니다. 반면 Fetch APIPromise 기반으로 설계되어 더욱 간결하고 명확한 코드를 작성할 수 있게 합니다.

결국 Fetch API는 현대 웹 개발에서 필수적인 비동기 통신 표준이며, 기존 방식의 한계를 극복하는 열쇠입니다.

Fetch API의 등장 배경과 필요성

2000년대 초반, 웹 페이지의 동적인 상호작용을 위해 AJAX (Asynchronous JavaScript and XML) 기술이 등장하면서 XMLHttpRequest가 표준으로 자리 잡았습니다. 이 기술은 페이지 전체를 새로고침하지 않고도 서버와 통신하여 필요한 데이터만 업데이트할 수 있게 해주었죠. 하지만 시간이 지나면서 웹 애플리케이션의 복잡도가 증가하고, XHR의 한계가 명확해지기 시작했습니다.

예를 들어, 여러 개의 비동기 요청이 순차적으로 실행되어야 하거나, 특정 조건에 따라 다른 요청이 발생해야 하는 경우 XHR의 콜백 기반 구조는 코드를 읽고 유지보수하기 어렵게 만들었습니다. 또한, HTTP 응답을 처리하는 과정도 번거로웠습니다.

XMLHttpRequest와 Fetch API 비교

Fetch APIXHR에 비해 어떤 장점을 가지는지 구체적으로 살펴봅시다. 가장 큰 차이점은 Promise 기반이라는 점입니다.

1. 코드의 간결성 및 가독성: FetchPromise를 반환하므로 async/await 문법과 함께 사용하여 동기 코드처럼 깔끔하게 비동기 로직을 작성할 수 있습니다. XHR은 여러 개의 콜백 함수를 중첩하여 사용해야 했습니다.

2. 응답 처리의 용이성: Fetch는 네트워크 응답을 스트림(stream)으로 처리할 수 있는 Response 객체를 반환하며, .json(), .text() 등 편리한 메서드를 제공합니다. XHRresponseText를 수동으로 파싱해야 했습니다.

3. 표준화된 인터페이스: Fetch APIRequest, Response, Headers 객체를 통해 HTTP의 핵심 개념을 명확하게 모델링합니다.

Fetch API의 기본 문법과 GET/POST 요청

Fetch API의 기본 문법과 GET/POST 요청

Fetch API의 가장 기본적인 사용법은 fetch() 전역 함수를 호출하는 것입니다. 이 함수는 필수적으로 요청할 URL을 인자로 받으며, 선택적으로 Request 객체를 구성하는 옵션 객체를 두 번째 인자로 받을 수 있습니다.

가장 중요한 것은 Fetch가 Promise를 반환하므로, 항상 .then().catch() 또는 async/await를 사용하여 처리해야 한다는 점입니다.

GET 요청으로 데이터 가져오기

가장 일반적인 형태의 요청은 서버로부터 데이터를 가져오는 GET 요청입니다. fetch() 함수는 기본적으로 GET 메서드를 사용합니다.

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) { // HTTP 상태 코드가 200번대가 아닐 경우 에러 처리
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json(); // 응답 본문을 JSON으로 파싱
  })
  .then(data => {
    console.log('데이터:', data);
  })
  .catch(error => {
    console.error('Fetch 에러 발생:', error);
  });

// async/await 사용 예시
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log('데이터 (async/await):', data);
  } catch (error) {
    console.error('Fetch 에러 발생 (async/await):', error);
  }
}
fetchData();

위 예시에서 response.okHTTP 상태 코드가 200-299 범위에 있는지 여부를 나타냅니다. Fetch API는 네트워크 오류 시에만 .catch() 블록으로 이동하므로, HTTP 오류(예: 404 Not Found, 500 Internal Server Error)는 개발자가 직접 확인해야 합니다.

POST 요청으로 데이터 전송하기

서버에 데이터를 생성하거나 업데이트하는 POST 요청은 fetch() 함수의 두 번째 인자인 옵션 객체를 사용하여 구성합니다. 이 객체에는 method, headers, body 등의 속성이 포함됩니다.

async function postData() {
  const dataToSend = {
    title: '권퓨터의 Fetch API 가이드',
    body: 'Fetch API에 대한 모든 것!',
    userId: 1
  };

  try {
    const response = await fetch('https://api.example.com/posts', {
      method: 'POST', // HTTP 메서드 지정
      headers: {
        'Content-Type': 'application/json' // 전송하는 데이터 타입 지정
      },
      body: JSON.stringify(dataToSend) // JavaScript 객체를 JSON 문자열로 변환하여 전송
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();
    console.log('POST 요청 성공:', result);
  } catch (error) {
    console.error('POST 요청 에러 발생:', error);
  }
}
postData();

주목할 점은 body에 전송할 데이터를 JSON.stringify()를 사용하여 JSON 문자열로 변환해야 한다는 것입니다. 또한, Content-Type 헤더를 application/json으로 설정하여 서버에 데이터 형식을 알려주는 것이 중요합니다.

Fetch API로 복잡한 HTTP 요청 처리하기

Fetch API로 복잡한 HTTP 요청 처리하기

실제 웹 애플리케이션에서는 단순히 데이터를 가져오거나 전송하는 것을 넘어, 인증 토큰을 포함하거나, 요청을 취소하거나, 특정 시간 내에 응답을 받지 못하면 타임아웃 처리하는 등 보다 복잡한 상황에 직면합니다. Fetch API는 이러한 요구사항들을 충족시키기 위한 다양한 옵션과 기능을 제공합니다.

복잡한 요청을 효과적으로 관리하려면 Fetch의 다양한 옵션과 Promise 패턴에 대한 깊은 이해가 필요합니다.

HTTP 헤더 및 인증 토큰 설정

대부분의 API 요청은 인증(Authentication)을 필요로 합니다. 이때 Authorization 헤더에 Bearer Token과 같은 인증 정보를 포함하여 전송합니다.

async function fetchWithAuth() {
  const token = 'YOUR_AUTH_TOKEN'; // 실제 인증 토큰
  try {
    const response = await fetch('https://api.example.com/protected-data', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}` // 인증 토큰 포함
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log('인증된 데이터:', data);
  } catch (error) {
    console.error('인증 요청 에러:', error);
  }
}
fetchWithAuth();

headers 속성에는 Headers 객체나 일반 JavaScript 객체를 전달할 수 있습니다. 중요한 것은 필요한 모든 헤더를 정확히 설정하는 것입니다.

요청 타임아웃 처리

Fetch API 자체에는 타임아웃 옵션이 내장되어 있지 않습니다. 하지만 Promise.race()AbortController를 조합하여 타임아웃 기능을 구현할 수 있습니다.

function fetchWithTimeout(url, options, timeout = 5000) { // 기본 타임아웃 5초
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout); // 지정된 시간 후 요청 취소

  return fetch(url, {
    ...options,
    signal: controller.signal // AbortController의 signal을 fetch 옵션에 추가
  })
  .finally(() => clearTimeout(id)); // 요청 완료 후 타임아웃 제거
}

async function fetchDataWithTimeout() {
  try {
    const response = await fetchWithTimeout('https://api.example.com/slow-data', {}, 3000); // 3초 타임아웃
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log('타임아웃 적용 데이터:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('Fetch 요청 타임아웃 또는 취소됨:', error);
    } else {
      console.error('Fetch 에러 발생:', error);
    }
  }
}
fetchDataWithTimeout();

이 패턴은 setTimeout으로 설정된 시간이 지나면 AbortController를 통해 fetch 요청을 취소합니다. 취소된 요청은 AbortError를 발생시키므로, 이를 통해 타임아웃 여부를 판단할 수 있습니다.

요청 취소 (AbortController)

사용자가 페이지를 이탈하거나, 검색어를 빠르게 여러 번 입력하여 이전 검색 요청이 더 이상 필요 없을 때처럼 불필요한 네트워크 요청을 중단해야 할 때가 있습니다. AbortController는 이러한 요청 취소 기능을 제공합니다.

const controller = new AbortController();
const signal = controller.signal;

async function fetchCancellableData() {
  try {
    const response = await fetch('https://api.example.com/long-running-task', { signal });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log('취소 가능한 요청 결과:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('Fetch 요청이 사용자 또는 시스템에 의해 취소되었습니다.');
    } else {
      console.error('Fetch 에러 발생:', error);
    }
  }
}

fetchCancellableData();

// 3초 후 요청을 취소 (예시)
setTimeout(() => {
  controller.abort();
}, 3000);

AbortControllersignalfetch 옵션으로 전달되며, controller.abort()를 호출하면 연결된 모든 fetch 요청이 취소됩니다.

Fetch API 사용 시 꼭 알아야 할 주의사항

Fetch API 사용 시 꼭 알아야 할 주의사항

Fetch API는 강력하지만, 몇 가지 특징 때문에 개발자가 주의해야 할 점들이 있습니다. 이를 간과하면 예상치 못한 버그나 보안 문제가 발생할 수 있으니 꼼꼼히 확인해야 합니다.

안정적이고 안전한 웹 애플리케이션을 위해 Fetch API의 동작 방식과 브라우저 보안 정책을 정확히 이해하는 것이 중요합니다.

네트워크 에러와 HTTP 에러 구분

앞서 언급했듯이, fetch()는 네트워크 연결 실패와 같은 심각한 오류가 발생했을 때만 Promise를 거부(reject)합니다. 4xx (클라이언트 오류)나 5xx (서버 오류)와 같은 HTTP 상태 코드는 Promise를 거부하지 않고, 정상적인 응답으로 간주합니다. 따라서 개발자가 response.ok 속성을 사용하여 명시적으로 HTTP 상태 코드를 확인해야 합니다.

async function checkResponseStatus() {
  try {
    const response = await fetch('https://api.example.com/non-existent-resource');
    // 네트워크 에러가 아니므로 .catch()로 가지 않고 이 코드 블록이 실행됨
    if (!response.ok) {
      // HTTP 에러 처리 (예: 404 Not Found)
      const errorData = await response.json().catch(() => ({})); // JSON 파싱 실패 대비
      throw new Error(`HTTP Error: ${response.status} - ${errorData.message || response.statusText}`);
    }
    const data = await response.json();
    console.log('성공:', data);
  } catch (error) {
    // 네트워크 에러 또는 위에서 throw한 HTTP 에러 처리
    console.error('요청 실패:', error.message);
  }
}
checkResponseStatus();

이 점은 XHRonerror 이벤트와 onload 이벤트로 네트워크 오류와 HTTP 오류를 다르게 처리했던 것과 대비됩니다.

크로스 도메인 요청 (CORS)

웹 브라우저는 보안상의 이유로 Same-Origin Policy (동일 출처 정책)를 따릅니다. 이는 다른 도메인으로의 요청을 기본적으로 제한하는 정책입니다. 만약 클라이언트와 서버가 다른 도메인에 있다면, CORS (Cross-Origin Resource Sharing) 정책을 준수해야 합니다.

Fetch 요청 시 mode 옵션을 설정하여 CORS 동작 방식을 제어할 수 있습니다.

  • cors (기본값): 동일 출처 및 교차 출처 요청을 허용합니다. 서버가 적절한 CORS 헤더를 보내야 합니다.
  • no-cors: 교차 출처 요청을 허용하지만, 응답에 접근할 수 없도록 제한합니다. 주로 GET 요청 시 이미지, 스크립트 등 특정 리소스를 로드할 때 사용됩니다. 응답 status는 항상 200으로 표시되며, 응답 본문에 접근할 수 없습니다.
  • same-origin: 동일 출처 요청만 허용합니다. 교차 출처 요청 시 오류가 발생합니다.
fetch('https://another-domain.com/data', {
  method: 'GET',
  mode: 'cors' // 기본값이지만 명시적으로 설정 가능
})
.then(response => response.json())
.then(data => console.log('CORS 요청 성공:', data))
.catch(error => console.error('CORS 요청 에러:', error));

대부분의 경우 mode: 'cors'를 사용하며, 서버 측에서 Access-Control-Allow-Origin 헤더를 적절히 설정해야 합니다. no-cors 모드는 응답에 접근할 수 없으므로, 데이터 API 호출에는 적합하지 않습니다.

인증 정보 전송 (Credentials)

쿠키, HTTP 인증 헤더 또는 클라이언트 SSL 인증서와 같은 인증 정보를 교차 출처 요청에 포함해야 할 때가 있습니다. 이 경우 credentials 옵션을 사용합니다.

  • omit (기본값): 인증 정보를 보내지 않습니다.
  • same-origin: 동일 출처 요청에만 인증 정보를 보냅니다.
  • include: 모든 요청(동일 출처 및 교차 출처)에 인증 정보를 보냅니다. 교차 출처 요청 시 서버가 Access-Control-Allow-Credentials: true 헤더를 포함해야 합니다.
fetch('https://api.example.com/user-profile', {
  method: 'GET',
  credentials: 'include' // 쿠키와 같은 인증 정보 포함
})
.then(response => response.json())
.then(data => console.log('사용자 프로필:', data))
.catch(error => console.error('프로필 로드 에러:', error));

credentials: 'include'를 사용할 때는 서버에서 Access-Control-Allow-Origin 헤더를 와일드카드(*)로 설정할 수 없다는 점을 주의해야 합니다. 특정 출처를 명시해야 합니다.

Fetch API, 더 나은 개발을 위한 마무리

Fetch API, 더 나은 개발을 위한 마무리

Fetch API는 현대 웹 개발에서 비동기 데이터 통신을 위한 강력하고 유연한 도구입니다. Promise 기반의 간결한 문법과 async/await와의 시너지는 개발자의 생산성을 크게 향상시킵니다. 하지만 단순한 사용법을 넘어, 에러 처리, 타임아웃, 요청 취소, 그리고 CORS와 같은 고급 개념들을 정확히 이해하고 적용하는 것이 중요합니다.

권퓨터가 제시한 가이드를 통해 Fetch API를 마스터하고, 여러분의 웹 애플리케이션을 더욱 견고하고