본문 바로가기

코드잇 스터디

[JS 기초문법] 리퀘스트 보내기

HTTP란?

- OSI 7계층 중 Application 계층은 7계층에서 사용하는 프로토콜 (요청/응답에 대한 정해진 양식)이다.

- 우리는 application 개발자가 될 생각이니 가장 외부 계층 프로토콜인 HTTP를 가장 많이 사용한다. 

    (그 외에도 4계층 Transport TCP와 3계층 Network IP도 알고있긴 해야함)

우리가 브라우저 주소창에 www.example.com을 입력하고 엔터만 치면,
브라우저가 이 주문서를 자동으로 작성해서 서버에 보냅니다.
우리가 직접 GET /index.html HTTP/1.1이라고 타이핑하지 않아도요.

HTTP 요청: 주문서 작성하기

HTTP 응답: 영수증 + 음식 받기

 

HTTP 상태코드 (응답 종류)

- 직원의 대답 유형

 

HTTP 메서드 (요청 종류)

CRUD (crate, read, update, delete)와 관련있다

- 주문의 종류

 

HTTP Header (요청/응답 추가 정보)

- 주문서의 추가 정보

 

REST API (응답 설게 규칙)

- 메뉴판 설계 규칙

 

JSON이란?

- Javascript object notation

- 데이터 주고받을때 사용하는 테스트 형식 (JS object 형식처럼 보임)

{
  "name": "홍길동",        // 키는 큰따옴표로
  "age": 30,               // 숫자
  "married": false,        // 불린
  "children": null,        // null
  "hobbies": ["독서", "운동"]  // 배열
}

JSON.stringify(): 객체를 JSON 문자열로 (왜냐하면 HTTP 프로토콜을 문자열만 통과할 수 있음)

JSON.parse(): JSON 문자열을 객체

const user = {
  name: '홍길동',
  age: 30,
  email: 'hong@example.com'
};
// 객체 → JSON 문자열
const json = JSON.stringify(user);
console.log(json);// '{"name":"홍길동","age":30,"email":"hong@example.com"}'
console.log(typeof json); // "string"

const json = '{"name":"홍길동","age":30}';
// JSON 문자열 → 객체
const user = JSON.parse(json);
console.log(user);        // { name: '홍길동', age: 30 }
console.log(user.name);   // "홍길동"
console.log(typeof user); // "object"

 

(+) Date객체, 함수, undefined인 경우에만 조금 형식이 달라진다. 

// Date
const data = { createdAt: new Date() };
const parsed = JSON.parse(JSON.stringify(data));
console.log(typeof parsed.createdAt); // "string" ← Date가 아님!

// 함수
const data = { greet: () => "안녕" };
const parsed = JSON.parse(JSON.stringify(data));
console.log(parsed.greet); // undefined ← 사라짐!

// undefined
const data = { name: "홍길동", age: undefined };
const parsed = JSON.parse(JSON.stringify(data));
console.log(parsed.age);  // undefined

 

Fetch 기본 문법 

fetch는 JS에서 서버에 HTTP 요청을 보내는 함수이다 .

- 브라우저/NodeJS에 내장되어 있는 함수이다. 

- Promise를 반환한다. (async/await로 처리)

 

GET은 기본 default이다. 

fetch('https://jsonplaceholder.typicode.com/users/1')
    .then(response => {
        console.log('응답 객체:', response);
        return response.json(); // JSON 파싱
    })
    .then(data => {
        console.log('데이터:', data);
    });

async function getUser() {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
    const data = await response.json();
    console.log('사용자:', data);
}

getUser();

데이터를 한꺼번에 보내는게 아니라 쪼개서 보내고 나중에 합쳐진다.

- response.ok가 true면 200번대이고, 아니면 400~500번대이다. 

response.json()은 body를 읽고 JSON으로 변환하는 비동기 작업 -> Promise 리턴

response.status는 HTTP 상태 코드 숫자가 나온다 (200/201/500...)

- new URLSearchParams(param)하면 일반 객체로부터 url의 쿼리파라미터를 부분을 작성해준다

async function searchUsers(params) {
    const searchParams = new URLSearchParams(params);
    const url = `https://api.example.com/users?${searchParams}`;

    const response = await fetch(url);
    const data = await response.json();

    return data;
}
// 사용
searchUsers({
    name: '홍길동',
    age: 30,
    city: '서울'
});
// → https://api.example.com/users?name=%ED%99%8D%EA%B8%B8%EB%8F%99&age=30&city=%EC%84%9C%EC%9A%B8

 

나머지 POST, PATCH, PUT, DELETE는 다음과 같이 요청한다.

PATCH는 수정, PUT은 전체 데이터 교체이다.

PUT으로 하는거 PATCH로 할 수 있으니 대게 그냥 PATCH만 쓴다

DELETE는 보통 body를 안 주고 그냥 상태코드만 주기 때문에 json파싱을 안한다 사용하는 프로토콜 (요청/응답에 대한 정해진 양식)이다.

const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST', // HTTP 메서드
        headers: {
            'Content-Type': 'application/json' // JSON 형식
        },
        body: JSON.stringify(newPost) // 객체를 JSON 문자열로
});
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(updatedPost)
});
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(updates)
});
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'DELETE'
});

Fetch/Axios 함수의 리팩토링

API fetch 함수를 작성하다 보면 코드가 반복되고 있다는 느낌이 들기 시작할 것이다. 

 

DRY: don't repeat yourself
-> 두번 이상 반복하면 리팩토링 해야한다는 소리다!

 

그래서 위의 코드의 반복되는 부분을 또다른 함수를 정의해서 해결해보자

- endpoint: url에서 BASE_URL을 제외한 끝지

const BASE_URL = 'https://jsonplaceholder.typicode.com';

async function get(endpoint) {
    const response = await fetch(`${BASE_URL}${endpoint}`);
    if (!response.ok) {
        throw new Error(`HTTP 에러! 상태: ${response.status}`);
    }
    return response.json();
}

const posts = await get('/posts');
const user = await get('/users/1');
const comments = await get('/comments');

이러면 코드 줄이 훨씬 줄어들고 보기에도 편하다.

 

GET외의 다른 함수들도 리팩토링하면 다음과 같게 생긴다:

const BASE_URL = 'https://jsonplaceholder.typicode.com';

const api = {
    get: async (endpoint) => {
        const response = await fetch(`${BASE_URL}${endpoint}`);
        if (!response.ok) throw new Error(`GET 실패: ${response.status}`);
        return response.json();
    },
    post: async (endpoint, data) => {
        const response = await fetch(`${BASE_URL}${endpoint}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data),
        });
        if (!response.ok) throw new Error(`POST 실패: ${response.status}`);
        return response.json();
    },
    put: async (endpoint, data) => {
        const response = await fetch(`${BASE_URL}${endpoint}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data),
        });
        if (!response.ok) throw new Error(`PUT 실패: ${response.status}`);
        return response.json();
    },
    delete: async (endpoint) => {
        const response = await fetch(`${BASE_URL}${endpoint}`, {
            method: 'DELETE',
        });
        if (!response.ok) throw new Error(`DELETE 실패: ${response.status}`);
        if (response.status === 204) return null;
        return response.json();
    },
};

근데 이거마저 코드가 반복되고 있는 것을 확인할 수 있다. 

fetch -> ok 체크 -> json 파싱 흐름이 계속 똑같다. 

const BASE_URL = 'https://jsonplaceholder.typicode.com';

async function request(endpoint, options = {}) {
    const config = {
        ...options,
        headers: {
            'Content-Type': 'application/json',
            ...options.headers,
        },
    };

    const response = await fetch(`${BASE_URL}${endpoint}`, config);

    if (!response.ok) {
        throw new Error(`HTTP 에러! 상태: ${response.status}`);
    }

    // DELETE는 응답 본문이 없는 경우가 많음
    if (response.status === 204) return null;

    return response.json();
}

const api = {
    get: (endpoint) => request(endpoint),
    post: (endpoint, data) => request(endpoint, {
        method: 'POST',
        body: JSON.stringify(data),
    }),
    put: (endpoint, data) => request(endpoint, {
        method: 'PUT',
        body: JSON.stringify(data),
    }),
    delete: (endpoint) => request(endpoint, {
        method: 'DELETE',
    }),
};

Fetch 오류 처리

try-catch로 에러를 잡으려 해도,

404같은, 백엔드에 요청은 갔지만 HTTP 에러가 난 경우는 fetch가 에러를 내지 않기 때문에

잡기 쉽지 않다. 

그래서 response.ok를 가지고 HTTP에러가 나면 직접 throw new Error해서 

try-catch가 잡을 수 있게 만들어줘야한다. 

async function fetchData(url) {
    try {
        const response = await fetch(url);

        // HTTP 에러 체크 (404, 500 등)
        if (!response.ok) {
            throw new Error(`HTTP 에러! 상태: ${response.status}`);
        }

        const data = await response.json();
        return data;

    } catch (error) {
        // 네트워크 에러 + 위에서 던진 HTTP 에러 모두 여기서 잡힘
        console.error('요청 실패:', error.message);
        throw error;
    }
}

 

상태코드별 처리

에러가 났을때 상태코드에 따라 다른 메세지를 보여주는게 이상적이다. 

async function fetchData(url) {
    try {
        const response = await fetch(url);

        if (!response.ok) {
            const messages = {
                400: '잘못된 요청입니다',
                401: '로그인이 필요합니다',
                403: '접근 권한이 없습니다',
                404: '요청한 데이터를 찾을 수 없습니다',
                500: '서버에 문제가 발생했습니다',
            };

            throw new Error(messages[response.status] || `에러: ${response.status}`);
        }

        return await response.json();

    } catch (error) {
        console.error(error.message);
        throw error;
    }
}

 

타임아웃 - 응답이 너무 느릴때

응답 기다리는 데에 시간 제한을 걸 수 있다.

fetch의 signal 옵션에AbortSignal.timeout(밀리초)를 넣으면 된다. 

// 3초 안에 응답이 안 오면 자동으로 취소
const response = await fetch('/api/data', {
    signal: AbortSignal.timeout(3000),
});

 

재시도 - 실패하면 다시 시도하기

async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (error) {
            console.error(`시도 ${i + 1}/${retries} 실패`);

            // 마지막 시도였으면 포기
            if (i === retries - 1) throw error;

            // 1초 기다렸다가 재시도
            await new Promise((resolve) => setTimeout(resolve, 1000));
        }
    }
}

 

정리