icon
9장 : 웹 API와 브라우저 기능

Fetch API와 AJAX


우리는 8장 "최신 ECMAScript 기능"을 통해 let, const, 화살표 함수, 구조 분해 할당, 스프레드 연산자, 그리고 Symbol, Map, Set, Proxy, Reflect와 같은 자바스크립트 언어 자체의 강력한 기능들을 학습했습니다. 이제 여러분은 더욱 견고하고 간결하며 효율적인 자바스크립트 코드를 작성할 수 있게 되었습니다.

하지만 자바스크립트의 진정한 힘은 단순히 언어적인 능력뿐만 아니라, 웹 브라우저가 제공하는 다양한 웹 API(Web APIs) 와 상호작용할 때 발휘됩니다. 웹 API는 브라우저가 제공하는 기능들을 자바스크립트 코드를 통해 접근하고 조작할 수 있도록 해주는 인터페이스입니다. DOM 조작, 이벤트 처리, 지오로케이션, 로컬 스토리지 등 수많은 웹 API가 존재하며, 이들은 웹 애플리케이션을 동적이고 상호작용적으로 만드는 데 필수적입니다.

9장 "웹 API와 브라우저 기능"에서는 웹 애플리케이션 개발에 가장 중요한 몇 가지 웹 API와 브라우저 기능을 집중적으로 다룹니다. 그 첫걸음으로, 웹 애플리케이션이 서버와 비동기적으로 데이터를 주고받는 기술인 AJAX(Asynchronous JavaScript and XML) 와 그 현대적인 표준인 Fetch API에 대해 알아보겠습니다. 이는 사용자 경험을 향상시키는 동적인 웹 페이지 구현의 핵심입니다.

웹 페이지는 처음 로드될 때 서버로부터 모든 HTML, CSS, JavaScript를 받아옵니다. 하지만 사용자와의 상호작용에 따라 페이지의 일부만 업데이트해야 하거나, 새로운 데이터를 서버로부터 받아와야 하는 경우가 빈번하게 발생합니다. 이때 페이지 전체를 새로고침하지 않고도 서버와 비동기적으로 통신하여 데이터를 주고받는 기술이 필요한데, 이를 AJAX(Asynchronous JavaScript and XML) 라고 합니다.

AJAX는 'Asynchronous JavaScript And XML'의 약자이지만, 실제로는 XML 대신 JSON과 같은 다른 데이터 형식을 더 많이 사용합니다. 핵심은 비동기성(Asynchronous)JavaScript를 이용한 통신입니다. AJAX는 사용자 경험을 크게 향상시키는데, 페이지 새로고침 없이 필요한 데이터만 가져와 부분적으로 업데이트하므로 빠르고 매끄러운 인터페이스를 구현할 수 있습니다.

과거에는 AJAX 통신을 위해 주로 XMLHttpRequest 객체를 사용했지만, ES2015(ES6) 이후 등장한 Fetch APIPromise 기반으로 설계되어 더욱 간결하고 강력하며 유연한 비동기 통신 방법을 제공합니다. 현대 웹 개발에서는 Fetch API가 AJAX 통신의 표준처럼 사용되고 있습니다.


XMLHttpRequest (XHR)

XMLHttpRequest 객체는 AJAX의 전통적인 구현체입니다. 복잡하고 콜백 지옥에 빠지기 쉬웠지만, 웹 개발의 중요한 한 획을 그었습니다.

// // fetch API가 없을 때의 XMLHttpRequest 사용 예시 (간략)
// function fetchDataXHR() {
//     const xhr = new XMLHttpRequest();
//     xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1'); // 요청 초기화
//     xhr.onload = function() { // 응답이 로드되었을 때 (성공)
//         if (xhr.status >= 200 && xhr.status < 300) {
//             console.log("XHR 응답 성공:", JSON.parse(xhr.responseText));
//         } else {
//             console.error("XHR 응답 실패:", xhr.status);
//         }
//     };
//     xhr.onerror = function() { // 요청 실패 (네트워크 오류 등)
//         console.error("XHR 네트워크 오류 발생");
//     };
//     xhr.send(); // 요청 전송
// }
// fetchDataXHR();

XMLHttpRequest는 상태 변화에 따른 이벤트 리스너를 등록하고, 성공/실패 여부를 직접 판단해야 하는 등 다소 번거로운 점이 있었습니다.


Fetch API: AJAX의 현대적인 표준

Fetch APIPromise를 기반으로 비동기 네트워크 요청을 수행하는 현대적인 인터페이스입니다. XMLHttpRequest보다 사용하기 쉽고, 더 강력하며, 더 많은 기능을 제공합니다.

fetch() 함수의 기본 사용법

fetch() 함수는 첫 번째 인자로 요청할 URL을 받으며, Promise를 반환합니다. 이 Promise는 네트워크 요청에 대한 Response 객체로 이행(resolve)됩니다. Response 객체에는 HTTP 응답에 대한 정보(상태 코드, 헤더 등)가 담겨 있습니다. 실제 응답 본문(body)의 데이터(JSON, 텍스트 등)를 얻으려면 Response 객체의 메서드를 추가로 호출해야 합니다.

// GET 요청 예시
fetch('https://jsonplaceholder.typicode.com/posts/1') // Promise 반환
    .then(response => {
        // 응답 상태 확인: response.ok는 200-299 상태 코드를 나타냄
        if (!response.ok) {
            throw new Error(`HTTP 오류! 상태: ${response.status}`);
        }
        // 응답 본문을 JSON으로 파싱 (Promise 반환)
        return response.json();
    })
    .then(data => {
        console.log("Fetch GET 응답 데이터:", data);
    })
    .catch(error => {
        console.error("Fetch 요청 실패:", error);
    });

주의: fetch()는 네트워크 오류와 같이 실제 요청이 실패했을 때만 Promise를 거부(reject)합니다. HTTP 상태 코드 404(Not Found)나 500(Server Error)과 같은 서버 응답 오류는 Promise를 거부하지 않고, Response 객체의 ok 프로퍼티가 false로 설정됩니다. 따라서 항상 response.ok를 확인하여 HTTP 오류를 처리해야 합니다.

다양한 HTTP 메서드와 옵션

fetch() 함수의 두 번째 인자로 init 객체를 전달하여 다양한 요청 옵션을 설정할 수 있습니다.

// POST 요청 예시
const newPost = {
    title: '새로운 게시물',
    body: '이것은 Fetch API로 작성된 새로운 게시물 내용입니다.',
    userId: 1,
};

fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST', // HTTP 메서드 지정
    headers: {
        'Content-Type': 'application/json; charset=UTF-8', // 요청 헤더 지정
    },
    body: JSON.stringify(newPost), // 전송할 데이터 (JSON.stringify로 문자열화)
})
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP 오류! 상태: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log("Fetch POST 응답 데이터 (생성된 게시물):", data);
    })
    .catch(error => {
        console.error("Fetch POST 요청 실패:", error);
    });

주요 init 객체 옵션

  • method: GET, POST, PUT, DELETE 등 HTTP 메서드 (GET이 기본값).
  • headers: 요청 헤더를 설정하는 객체 (Content-Type, Authorization 등).
  • body: POST, PUT 등과 함께 전송할 데이터. 문자열(JSON.stringify), FormData 객체 등이 될 수 있습니다.
  • mode: cors (기본값), no-cors, same-origin. CORS 정책과 관련됩니다.
  • cache: default, no-store, reload, force-cache 등 캐시 정책.
  • credentials: omit, same-origin, include. 인증 정보(쿠키, HTTP 인증) 전송 여부.
  • referrer: 요청의 Referer 헤더.
  • signal: AbortController와 함께 요청을 취소할 때 사용.

async/await와 함께 Fetch 사용하기

우리는 7장에서 async/await 문법을 학습했습니다. fetch() 함수가 Promise를 반환하므로, async/await와 함께 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있습니다.

async function fetchUserData(userId) {
    try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

        if (!response.ok) {
            throw new Error(`사용자 데이터를 불러오는 데 실패했습니다: ${response.status}`);
        }

        const userData = await response.json();
        console.log("사용자 데이터:", userData);
        return userData;
    } catch (error) {
        console.error("데이터 로딩 중 오류 발생:", error);
        return null;
    }
}

fetchUserData(1); // ID가 1인 사용자 데이터 가져오기
fetchUserData(999); // 존재하지 않는 사용자 (오류 처리 테스트)

async/await를 사용하면 then().catch() 체인보다 에러 처리 및 코드 흐름을 파악하기 훨씬 용이합니다.

요청 취소: AbortController

Fetch 요청은 한 번 시작되면 취소하기 어려웠습니다. AbortController는 Fetch 요청을 취소할 수 있는 메커니즘을 제공합니다. 이는 사용자가 페이지를 떠나거나, 검색어가 바뀌어 이전 요청이 불필요해질 때 유용합니다.

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

async function fetchWithCancellation(url) {
    try {
        const response = await fetch(url, { signal });
        if (!response.ok) {
            throw new Error(`HTTP 오류! 상태: ${response.status}`);
        }
        const data = await response.json();
        console.log("데이터 로드 성공:", data);
        return data;
    } catch (error) {
        if (error.name === 'AbortError') {
            console.log("Fetch 요청이 취소되었습니다.");
        } else {
            console.error("Fetch 요청 오류:", error);
        }
        return null;
    }
}

// 5초 후에 요청을 취소하는 예시
const longRunningFetch = fetchWithCancellation('https://jsonplaceholder.typicode.com/posts/1');

setTimeout(() => {
    controller.abort(); // 요청 취소
}, 50); // 아주 짧은 시간 후에 취소하여 AbortError 발생 유도

signal 옵션에 AbortControllersignal 프로퍼티를 전달하고, 필요할 때 controller.abort()를 호출하면 해당 요청이 취소됩니다.


CORS (Cross-Origin Resource Sharing)

Fetch API를 사용하여 다른 도메인의 리소스에 접근할 때 CORS (Cross-Origin Resource Sharing) 정책에 주의해야 합니다. 웹 브라우저는 보안상의 이유로 Same-Origin Policy (동일 출원 정책)을 따릅니다. 이는 한 출원(Origin: 프로토콜, 호스트, 포트가 모두 동일한 경우)에서 로드된 웹 페이지가 다른 출원의 리소스에 접근하는 것을 제한합니다.

  • 동일 출원: http://example.com:8080/pathhttp://example.com:8080/anotherpath는 동일 출원.
  • 다른 출원
    • http://sub.example.com (다른 호스트)
    • https://example.com (다른 프로토콜)
    • http://example.com:3000 (다른 포트)

다른 출원의 리소스에 접근하려면 서버에서 CORS 관련 HTTP 헤더(Access-Control-Allow-Origin 등)를 적절히 설정하여 해당 요청을 허용해야 합니다. Fetch 요청의 mode 옵션을 cors (기본값) 또는 no-cors 등으로 설정하여 CORS 정책에 대한 브라우저의 동작을 제어할 수 있습니다. no-cors는 요청이 나가는 것을 허용하지만, 스크립트가 응답 본문에 접근할 수 없게 합니다.


마무리하며

이번 장에서는 웹 애플리케이션에서 서버와 비동기적으로 데이터를 주고받는 핵심 기술인 AJAX의 개념과, 그 현대적인 구현 표준인 Fetch API에 대해 심도 있게 학습했습니다.

여러분은 XMLHttpRequest의 간략한 역사와 함께, Promise 기반으로 동작하는 fetch() 함수의 기본 사용법을 익혔습니다. 특히 fetch()가 네트워크 오류가 아닌 HTTP 응답 오류에 대해서는 Promise를 거부하지 않는다는 점과 response.ok를 통해 응답 상태를 확인해야 한다는 중요한 점을 기억해야 합니다. 또한, method, headers, body와 같은 다양한 요청 옵션을 설정하는 방법과, async/await를 사용하여 비동기 코드를 더 간결하게 작성하는 방법, 그리고 AbortController를 이용해 요청을 취소하는 방법까지 살펴보았습니다. 마지막으로, 웹 통신에서 중요한 보안 정책인 CORS에 대해서도 간략하게 이해했습니다.

Fetch API는 모던 웹 애플리케이션 개발에서 서버 통신의 핵심 도구입니다. 이 장의 내용을 바탕으로 여러분은 동적이고 상호작용적인 웹 페이지를 구현하는 데 필요한 강력한 기술을 습득했습니다. 다양한 공개 API들을 대상으로 직접 fetch() 요청을 보내보고, 받은 데이터를 웹 페이지에 표시하는 연습을 해보시길 바랍니다.