본문으로 건너뛰기

안동민 개발노트

본문 시작

Fetch API와 AJAX

로컬 Mock API에 XHR과 fetch 요청을 보내고 HTTP 메서드·JSON·오류·취소·CORS를 비동기로 처리합니다.

우리는 8장에서 최신 ECMAScript 문법을 통해 더 견고하고 간결한 자바스크립트 코드를 작성하는 방법을 배웠습니다.

이제 다음 단계는 언어 자체를 넘어, 브라우저가 제공하는 웹 API(Web APIs)를 실전적으로 쓰는 것입니다.

DOM, 이벤트, 스토리지, 네트워크 통신 같은 기능은 모두 웹 API 위에서 동작합니다.

9장 웹 API와 브라우저 기능의 시작점은 서버 통신의 핵심인 AJAXFetch API입니다.

사용자 경험을 부드럽게 만드는 가장 중요한 기술 중 하나입니다.

웹 페이지는 처음 로드될 때 서버에서 HTML, CSS, JavaScript를 받아옵니다.

하지만 이후 상호작용에서는 페이지 전체를 다시 받을 필요가 없는 경우가 많습니다.

필요한 데이터만 비동기로 주고받는 방식이 바로 AJAX입니다.

AJAX는 이름에 XML이 들어가지만, 실무에서는 JSON 같은 형식을 더 자주 사용합니다.

핵심은 전체 새로고침 없이 필요한 데이터만 교환한다는 점입니다.

과거에는 XMLHttpRequest가 중심이었고, 현재는 Promise 기반의 Fetch API가 기본 선택지가 되었습니다.

Fetch API와 AJAX

Fetch API는 브라우저에서 HTTP 요청을 만들고 응답을 Promise 흐름으로 처리하는 기준입니다. 메서드, 헤더, 본문, 오류 처리를 함께 봐야 합니다.

  1. XMLHttpRequest (XHR)

    XMLHttpRequest 객체는 AJAX의 전통적인 구현체입니다.

  2. Fetch API: AJAX의 현대적인 표준

    Fetch API는 Promise를 기반으로 비동기 네트워크 요청을 수행하는 현대적인 인터페이스입니다.

  3. CORS (Cross-Origin Resource Sharing)

    Fetch API를 사용해 다른 도메인의 리소스에 접근할 때는 CORS (Cross-Origin Resource Sharing) 정책을 이해해야 합니다.


실습 API 준비 (로컬 Mock 서버)

외부 공개 API 상태에 영향받지 않도록, 이 절의 실습 URL은 http://localhost:4000 기준으로 맞춥니다.

포트 정책은 4000=Mock API, 4100=별도 백엔드/실시간 서버로 분리해 두면 트랙 병행 학습 시 충돌을 줄일 수 있습니다.

npx json-server --watch db.json --port 4000

db.json에는 최소한 posts, users 배열을 준비해 두면 예제를 그대로 재현할 수 있습니다.


XMLHttpRequest (XHR)

XMLHttpRequest 객체는 AJAX의 전통적인 구현체입니다.

복잡하고 콜백 지옥에 빠지기 쉬웠지만, 웹 개발의 중요한 한 획을 그었습니다.

// // fetch API가 없을 때의 XMLHttpRequest 사용 예시 (간략)
// function fetchDataXHR() {
//     const xhr = new XMLHttpRequest();
//     xhr.open('GET', 'http://localhost:4000/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('http://localhost:4000/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('http://localhost:4000/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와 함께 요청을 취소할 때 사용.

Fetch 옵션은 단순한 부가 설정이 아니라 요청의 목적, 데이터 형식, 인증 범위, 취소 조건을 서버와 맞추는 설계표입니다.

Fetch 옵션 설계 지도

method, headers, body, credentials, signal은 서버 통신의 약속을 코드로 드러냅니다.

  1. 1
    method

    GET은 조회, POST/PUT/PATCH/DELETE는 변경 요청에 씁니다.

  2. 2
    body

    JSON 문자열, FormData 등 요청 본문을 담습니다.

  3. 3
    signal

    검색어 변경이나 화면 이동 시 오래된 요청을 취소합니다.

async/await와 함께 Fetch 사용하기

우리는 7장에서 async/await 문법을 학습했습니다.

fetch() 함수가 Promise를 반환하므로, async/await와 함께 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있습니다.

async function fetchUserData(userId) {
    try {
        const response = await fetch(`http://localhost:4000/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는 네트워크를 빠르게 만드는 장치가 아니라, 현재 화면과 맞지 않는 비동기 결과를 끊는 안전장치다.

단계하는 일확인할 것실패 신호
createAbortController 생성요청마다 새 controller인가여러 요청이 같은 signal 공유
connectfetch 옵션에 signal 전달실제 요청과 연결됐는가abort를 해도 요청이 계속됨
abortunmount나 새 요청에서 취소cleanup에서 호출되는가stale 응답이 state 갱신
catchAbortError와 진짜 오류 분기취소는 조용히 정리취소를 실패 toast로 표시

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('http://localhost:4000/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 등으로 설정해 브라우저 동작을 제어할 수 있으며, no-cors는 요청 전송은 허용하지만 스크립트의 응답 본문 접근은 제한합니다.

Fetch 응답 판단 순서

fetch는 HTTP 오류를 자동 예외로 만들지 않으므로, 응답 객체를 단계적으로 확인해야 합니다.

  1. 1

    URL, method, headers, body, signal을 준비합니다.

  2. 2

    연결 실패와 취소는 catch 블록에서 다룹니다.

  3. 3

    response.ok와 status로 4xx, 5xx를 분기합니다.

  4. 4

    json, text, blob 중 실제 응답 형식에 맞게 읽습니다.


Fetch 코드는 요청만 보내는 함수로 끝나지 않고, 화면의 로딩/성공/빈 결과/오류 상태와 함께 설계해야 실전에서 안정적입니다.

아래 다이어그램은 같은 요청 흐름을 UI 상태 전환 기준으로 다시 묶어 보여줍니다.

Fetch UI 상태 전환

네트워크 요청은 response.ok 확인에서 끝나지 않습니다. 사용자가 보는 화면은 대기, 성공, 빈 결과, 실패, 취소 상태를 모두 가져야 합니다.

  1. idle
    요청 전

    URL, method, headers, body, signal을 만들고 이전 요청 키와 비교합니다.

  2. loading
    대기 중

    버튼 중복 클릭을 막고, 화면에는 현재 데이터가 갱신 중임을 표시합니다.

  3. success
    응답 성공

    2xx 응답이면 본문을 파싱하고 최신 요청인지 확인한 뒤 상태를 반영합니다.

  4. empty
    빈 결과

    정상 응답이지만 배열이 비었거나 표시할 값이 없으면 별도 안내를 표시합니다.

  5. error
    실패 처리

    HTTP 오류, 네트워크 실패, AbortError를 구분해 재시도와 무시 조건을 나눕니다.


AJAX·Fetch 정리

이 절에서는 요청-응답 흐름을 오류 처리까지 포함해 재현 가능한 형태로 정리합니다.

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

fetch()Promise 기반으로 동작하며, HTTP 오류 응답만으로는 자동으로 reject되지 않습니다.

따라서 response.ok 확인, 요청 옵션(method, headers, body), async/await, AbortController, CORS 제약을 함께 점검해야 합니다.

Fetch API는 서버 통신을 구현할 때 가장 기본적으로 확인해야 할 브라우저 API입니다.

로컬 Mock API나 스테이징 API로 요청을 보내고, 응답 상태와 데이터를 화면에 표시하는 흐름을 반복해 검증해 보세요.

Fetch는 요청·응답·실패를 분리해 읽는다

fetch는 네트워크 실패만 reject하고 HTTP 404/500은 응답 객체로 돌려준다. 그래서 ok/status 검사가 별도 단계가 된다.

구간확인할 것좋은 처리놓치기 쉬운 점
requestmethod, headers, body, credentials서버 계약을 코드에 드러냄GET body나 Content-Type 누락
responseok, status, headersHTTP 오류를 직접 분기404가 catch로 간다고 착각
bodyjson/text/blob 파싱Content-Type에 맞춰 읽음파싱 오류 처리 누락
lifecycleabort, retry, loadingUI 수명과 요청 수명 연결언마운트 뒤 state 갱신
Fetch 실패는 네트워크와 HTTP 상태를 나눠야 한다

DNS·CORS·연결 실패와 서버가 보낸 400/500 응답은 다른 실패다. 분기 기준을 나누면 사용자 메시지도 달라진다.

경계어디서 보이나처리 기준사용자에게 보일 것
transport failurePromise rejectcatch에서 네트워크/차단 처리연결 확인 또는 재시도
HTTP errorresponse.ok false상태 코드와 오류 body 확인검증 오류 또는 서버 오류
parse failurebody 읽기 단계Content-Type과 JSON 형식 확인응답 형식 오류
auth/corscredentials, preflight쿠키 포함과 서버 허용 확인로그인 필요 또는 권한 문제
Fetch 요청은 네 단계로 점검한다

엔드포인트와 옵션을 정하고, HTTP 상태를 확인하고, 본문을 파싱하고, 화면 상태로 분기해야 안정적인 요청 코드가 된다.

단계핵심 질문대표 코드위험 신호
1 요청 구성서버 계약과 맞는가fetch(url, options)인증/헤더/body 누락
2 상태 확인성공 응답인가if (!res.ok) throw404를 성공처럼 처리
3 본문 읽기어떤 형식인가await res.json()빈 body 파싱 오류
4 UI 반영성공/오류/취소가 나뉘나setState, catch, finallyloading이 남음