fetch api
- 비동기 http 요청을 좀 더 쓰기 편하게 해주는 API
- XMLHttpRequest를 대체
- Promise 기반으로 동작
fetch api 사용하기
fetch("http request를 날릴 URL")
.then((res) => {
return res.json(); // response는 json이나 text로 바꿔서 사용해야 함
})
.then((data) => { // 조회된 json 데이터가 들어옴
console.log(data);
});
fetch api 특징
1. fetch의 기본 응답 결과는 Response 객체이다.
따라서 Response 객체를 얻은 뒤에는 응답을 json으로 바꾸거나 text로 바꾸는 등의 처리가 필요하다.
+ 참고로 Response에는 json, text 외에도 많은 method가 있다.
ex) blob 메소드는 이미지를 처리하는데 사용할 수 있다.
2. fetch는 HTTP error가 발생하더라도 reject되지 않는다.
네트워크 에러나 요청이 완료되지 못한 경우에만 reject된다.
그러므로 서버 요청 중에 에러가 생겨도 then으로 가므로, response의 status code
나 ok
를 체크해주는 것이 좋다.
fetch("https://.../undefined-api") // 존재하지 않는 api
.then((res) => {
if (res.ok) { // res.ok 체크
return res.json();
}
throw new Error(`${res.status}! 요청 처리 실패`);
})
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e.message);
});
이렇게 해야 api 요청이 실패했을 때의 처리를 할 수 있다.
주의할 점은 res.ok
는 status가 200~299사이인 경우에만 true가 된다.
3. fetch의 두 번째 인자로 옵션을 줄 수 있다.
const headers = new Headers();
headers.append("x-auth-token", "TOKEN");
fetch("https://.../product", {
method: "POST",
headers,
body: JSON.stringify(product),
});
실습
← 폴더 구조
컴포넌트 구조
App - ProductPage - ProductOption
App - ProductPage - Cart
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>fetch api 연습</title>
</head>
<body>
<main id="app"></main>
<script src="./src/main.js" type="module"></script>
</body>
</html>
api.js
const API_END_POINT = "...";
export const request = (url) => {
return fetch(`${API_END_POINT}${url.indexOf("/") === 0 ? url : `/${url}`}`)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error(`${res.status} Error`);
})
.catch((e) => alert(e.message));
};
main.js
import ProductPage from "./ProductPage.js";
const $target = document.querySelector("#app");
new ProductPage({
$target,
initialState: {
productId: 1,
},
});
ProductPage.js
import ProductOptions from "./ProductOptions.js";
import Cart from "./Cart.js";
import { request } from "./api.js";
/** state 구조
* {
* productId:1,
* product: Product,
* optionData: [],
* selectedOption: []
* }
*/
export default function ProductPage({ $target, initialState }) {
const $product = document.createElement("div");
$target.appendChild($product);
this.state = initialState;
const productOptions = new ProductOptions({
$target: $product,
initialState: [],
onSelect: (option) => {
const nextState = { ...this.state };
const { selectedOptions } = this.state;
const selectedOptionIndex = selectedOptions.findIndex(
(selectedOption) => selectedOption.optionId === option.optionId
);
if (selectedOptionIndex > -1) {
nextState.selectedOptions[selectedOptionIndex].ea++;
} else {
nextState.selectedOptions.push({
optionId: option.optionId,
optionName: option.optionName,
optionPrice: option.optionPrice,
ea: 1,
});
}
this.setState(nextState);
},
});
const cart = new Cart({
$target: $product,
initialState: {
productName: "",
basePrice: 0,
selectedOptions: [],
},
onRemove: (selectedOptionIndex) => {
const nextState = { ...this.state };
nextState.selectedOptions.splice(selectedOptionIndex, 1);
this.setState(nextState);
},
});
this.setState = (nextState) => {
if (nextState.productId !== this.state.productId) {
fetchOptionData(nextState.productId);
return;
}
this.state = nextState;
const { product, selectedOptions, optionData } = this.state;
productOptions.setState(optionData);
cart.setState({
productName: product.name,
basePrice: product.basePrice,
selectedOptions: selectedOptions,
});
};
this.render = () => {};
this.render();
const fetchOptionData = (productId) => {
return request(`/products/${productId}`)
.then((product) => {
this.setState({
...this.state,
product,
optionData: [],
selectedOptions: [],
});
return request(`product-options?product.id=${product.id}`);
})
.then((productOptions) => {
return Promise.all([
Promise.resolve(productOptions),
Promise.all(
productOptions
.map((productOption) => productOption.id)
.map((id) => {
return request(`/product-option-stocks?productOption.id=${id}`);
})
),
]);
})
.then((data) => {
const [productOptions, stocks] = data;
const optionData = productOptions.map((productOption, i) => {
const stock = stocks[i][0].stock;
return {
optionId: productOption.id,
optionName: productOption.optionName,
optionPrice: productOption.optionPrice,
stock,
};
});
this.setState({
...this.state,
optionData,
});
});
};
fetchOptionData(this.state.productId);
}
ProductOption.js
export default function ProductOptions({ $target, initialState, onSelect }) {
const $select = document.createElement("select");
$target.appendChild($select);
/**
* 상품 옵션 이름 렌더링시 상품명 + 옵션명 + 재고: n개 이런 형식으로 보여줘야 함
* 재고가 0인 상품의 경우 옵션을 선택하지 못하게 함
* [
* {
* optionId: 1,
* optionName: '옵션 상품',
* optionPrice: 1000,
* stock: 10
* },
* ...
* ]
*/
this.state = initialState;
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
const createOptionFullName = ({ optionName, optionPrice, stock }) => {
return `${optionName}
${optionPrice > 0 ? `(옵션가 ${optionPrice})` : ""} |
${stock > 0 ? `재고 ${stock}` : "재고 없음"}`;
};
$select.addEventListener("change", (e) => {
const optionId = parseInt(e.target.value);
const option = this.state.find((option) => option.optionId === optionId);
if (option) {
onSelect(option);
}
});
this.render = () => {
if (this.state && Array.isArray(this.state)) {
$select.innerHTML = `
<option>선택하세요</option>
${this.state
.map(
(option) =>
`<option ${option.stock === 0 ? "disabled" : ""}
value="${option.optionId}">
${createOptionFullName(option)}</option>`
)
.join("")}
`;
}
};
this.render();
}
Cart.js
/** state 구조
* {
* productName: 상품명,
* basePrice: 상품 기본 가격,
* selectedOptions: [Option]
* }
*/
export default function Cart({ $target, initialState, onRemove }) {
const $cart = document.createElement("div");
$target.appendChild($cart);
this.state = initialState;
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
const calculateTotalPrice = () => {
const { basePrice, selectedOptions } = this.state;
return selectedOptions.reduce(
(acc, option) => acc + (basePrice + option.optionPrice) * option.ea,
0
);
};
const renderOption = (option, index) => {
const { productName, basePrice } = this.state;
return `<li data-index="${index}" class="cartItem">
${productName} - ${option.optionName} | ${basePrice + option.optionPrice},
${option.ea}개 <button class="remove">X</button></li>`;
};
this.render = () => {
const { selectedOptions } = this.state;
$cart.innerHTML = `
<ul>
${
Array.isArray(selectedOptions) &&
selectedOptions
.map((option, index) => renderOption(option, index))
.join("")
}
</ul>
<div>${calculateTotalPrice()} 원</div>
`;
$cart.querySelectorAll(".remove").forEach(($button) => {
$button.addEventListener("click", (e) => {
const $li = e.target.closest(".cartItem");
if ($li) {
const { index } = $li.dataset;
onRemove(parseInt(index));
}
});
});
};
this.render();
}
출처: 프로그래머스 프론트엔드 데브코스
[Day 13] VanillaJS를 통한 자바스크립트 기본 역량 강화 I (5)
'데브코스' 카테고리의 다른 글
[Day 16] 디바운싱, 커스텀 이벤트 (0) | 2022.11.09 |
---|---|
[Day 15] Todo App, 이벤트 위임, 낙관적 업데이트 (0) | 2022.11.07 |
[Day 14] history API (0) | 2022.11.06 |
[Day 12] Module, callback, Promise, async/await (0) | 2022.11.05 |
[Day 11] TodoList 만들기, Local Storage (0) | 2022.11.04 |