우리가 웹에 존재하는 대부분의 컨텐츠들은 업데이트가 가능하다. 업데이트된 컨텐츠가 여러군데에 분산되어있다면 동시 갱신이 어렵고 비용이 많이 든다. 사용자가 컨텐츠를 직접 관리하면 보안상 문제와 비현실적인 문제임. 그래서 client와 웹 서버로 분리해서 데이터를 요청 및 관리함. 웹 서버에서 데이터의 상태를 효율적으로 관리, 변경된 사항은 클라이언트들에게 공유를 잘하기 위함.
이건 강의에 나와있지 않은 에러이다. 내가 이 에러때문에 리액트로 애를 먹었었지..
에러내용을 살펴 보면 undefined의 속성인 map을 읽을 수 없다는 내용이다.
즉 undefined.map을 했다는 소리이다.
그러면 this.menu[this.currentCategory]
내부에 있는 item
키가 없다는 걸로 유추할 수 있다.
삼항연산자나 물음표를 추가하여 작성해보겠다.
this.menu[this.currentCategory]
?.map((item, index) => {....
물음표의 뜻은 this.menu[this.currentCategory] && this.menu[this.currentCategory].map
과 같은 뜻이다. this.menu[this.currentCategory]
가 있을 때만 뒤의 로직을 수행한다는 것이다.
일단 삼항연산자로 써서 해결하였으나,
에러 내용을 살펴보면 길이를 재고자 하는 변수의 값이 없거나 잘못된 것이라는 것. 길이를 재기 전에 변수의 값이 있는지 없는 지 확인해야한다.
이 또한 마찬가지로 undefined로 인해 발생한 에러이기에 삼항연산자로 처리(?)했는데
this.menu[this.currentCategory]
가 undefined이기 때문에 Array의 protoType인 push method를 쓸 수 없다는 에러이다.
this.menu[this.currentCategory]!.push({name: MenuName});
으로 일단 콘솔창 에러는 없앴으나, 앱 작동이 안되는 문제가 생겼다 !
서버에 데이터를 요청하거나 변경을 할 때는 fetch()
라는 메서드를 사용
요청이 여러번 사용되기 때문에 재사용되는 객체로 만들어서 사용해보는 방식으로 작성
const BASE_URL = 'http://localhost:3000/api';
fetch(`${BASE_URL}/`, 옵션)
상태를 변경해주는 로직부분을 지우고, 서버에 요청하는 로직을 추가
JSON.stringify()
를 사용했던 것, name이라는 키와 vale가 있는 객체를 만들어서 서버에 요청.then
이라는 체이닝을 사용.json()
메서드 사용fetch(`${BASE_URL}/category/${this.currentCategory}/menu`, {// 1.
method: "POST", // 2.
headers: { // 3.
"Content-Type": "application/json", // 4.
},
body: JSON.stringify({ name: MenuName }), // 5.
}).then((response) => { // 6.
return response.json(); // 7.
})
.then((data) => { // 8.
console.log(data);
});
에스프레소 메뉴 전체를 화면에 렌더링 → 기존 코드에서 바로 밑에다가 fetch를 해서 데이터를 불러오게 하는 요청을 할 수 있다고 생각한다
⇒ 하지만 이렇게 데이터 요청을 하면 한 가지 문제가 생긴다.
코드 상 메뉴 추가가 먼저 작성 되었는데, 왜 뒤에 작성된 것이 먼저 찍혔을까..? → 싱글스레드
: 자바스크립트는 한 번에 하나의 일만 수행 가능하다. ‘
예를 들어 카페에 가서 바닐라더블샷을 시켰는데, 주문하고 나면 점원이 바닐라더블샷을 만들고 있는 동안 내 뒤에 있는 사람들은 주문을 못한다.
좀 더 효율적으로 주문 접수와 주문 제조를 할 수 있다면 진동벨을 줘서 뒷 사람 주문을 받을 수 있다. 또한, 내 바닐라 더블샷을 만드는 동안 뒷 사람의 아메리카노가 더 빨리 만들어진다면 뒷 사람의 아메리카노가 먼저 나올 수 있다.
진동벨을 주는 이유는 첫번째 주문받은 거 제조 하느라 뒤에 요청 못받으면 멈춰있게 되는 것 → 실시간 통신이 안됨 → 그래서 진동벨이라는 약속이 필요하게 됨 ⇒ promise 객체
이것이 웹과의 통신에 중요한 로직. 서버에 요청을 하고 나면 진동벨을 받아가는 것, 그래야 여러 클라이언트들이 계속 요청을 했을 때 서버가 바로바로 받을 수 있음. 내가 먼저 요청한 것에 대해 먼저 답변을 받지 못할 수 있다.
⇒ 그럼에도 불구하고 비동기 통신의 불러오는 순서를 조율하고 싶다면 async-await을 사용하면 됨
async와 await이라는 특별한 문법을 사용하면 프로미스를 좀 더 편하게 사용할 수 있다.
async는 function앞에 위치하고, 해당 함수는 항상 프로미스를 반환한다.
프로미스가 아닌 값을 반환하더라도 이행상태의 프로미스로 값을 감싸 이행된 프로미스가 반환되도록 한다.
async function f() {
return 1;
}
f().then(alert); // 1
// 명시적으로 프로미스를 반환하는 것도 가능하다.
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
await는 async 함수 안에서만 동작합니다. 자바스크립트는 await 키워드를 만나면 프로미스가 처리될 때까지 기다리고, 결과는 그 이후 반환된다.
let value = await promise;
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
alert(result); // "완료!"
}
f();
위의 코드는 함수를 호출하고, 함수 본문이 실행되는 도중에 (*)로 표시한 줄에서 실행이 잠시 중단되었다가 프로미스가 처리되면 실행이 재개된다. 이때 프로미스 객체의 result 값이 변수 result에 할당된다. 따라서 위 예시를 실행하면 1초 뒤에 “완료!”가 출력된다.
const addMenuName = async () => {
if ($('#menu-name').value === '') {
alert('값을 입력해주세요.');
return;
}
const menuName = $('#menu-name').value;
// 메뉴 생성
await fetch(`${BASE_URL}/category/${this.currentCategory}/menu`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: menuName }),
})
.then((response) => {
return response.json();
});
// 전체 메뉴 불러오기
await fetch(`${BASE_URL}/category/${this.currentCategory}/menu`)
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
this.menu[this.currentCategory] = data;
render();
$('menu-name').value = '';
});
};
우리 코드에서는 함수 앞에 async를 붙이고, 기다렸으면 하는 로직에 await을 붙인다.
const MenuApi = {
// 모든 메뉴 불러오는 API 요청(GET->default)
async getAllMenuByCategory(category) {
await fetch(`${BASE_URL}/category/${category}/menu`)
.then(async(response) => {
return response.json();
})
.then((data) => {
return data;
});
},
};
this.init = async () => {
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
initEventListeners();
};
getAllMenuByCategory 함수의 전체 리턴값을 넘겨줘야하는데, 함수 체이닝한 곳에서 불러와서 함수 리턴값의 데이터를 직접 return하는 방식으로 고침
⇒ response값에 response객체를 받아와서 바로 데이터를 리턴해줌(해결)
const MenuApi = {
// 모든 메뉴 불러오는 API 요청(GET->default)
async getAllMenuByCategory(category) {
const response = await fetch(`${BASE_URL}/category/${category}/menu`);
return response.json();
},
};
...
// addMenuName에도 적용
const addMenuName = async() => {
...
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
...
}
const MenuApi = {
// 모든 메뉴 불러오는 API 요청(GET->default)
async getAllMenuByCategory(category) {
const response = await fetch(`${BASE_URL}/category/${category}/menu`);
return response.json();
},
// 메뉴 생성하는 API요청(POST)
async createMenu(category, name) {
const response = await fetch(`${BASE_URL}/category/${category}/menu`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
};
response.ok
: 서버에서 성공한 객체인지만 확인할 때 쓰는 속성const addMenuName = async () => {
if ($('#menu-name').value === '') {
alert('값을 입력해주세요.');
return;
}
const menuName = $('#menu-name').value;
await MenuApi.createMenu(this.currentCategory, menuName);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
$('menu-name').value = '';
};
const initEventListeners = () => {
....
$('nav').addEventListener('click', async (e) => {
const isCategoryButton =
e.target.classList.contains('cafe-category-name');
if (isCategoryButton) {
const categoryName = e.target.dataset.categoryName;
this.currentCategory = categoryName;
$('#category-title').innerText = `${e.target.innerText} 메뉴 관리`;
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
}
});
};
const MenuApi = {
.....
// 메뉴 수정하는 API요청(PUT)
async updateMenu(category, name, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
return response.json();
},
};
const updateMenuName = async (e) => {
const menuId = e.target.closest('li').dataset.menuId;
const $menuName = e.target.closest('li').querySelector('.menu-name');
const updatedMenuName = prompt('메뉴명을 수정하세요', $menuName.innerText);
await MenuApi.updateMenu(this.currentCategory, updatedMenuName, menuId);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
};
API요청해서 수정 → 수정하고 나서 데이터 리스트를 받아와준다.
const MenuApi = {
.....
// 메뉴 품절 토글(PUT)
async toggleSoldOutMenu(category, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}/soldout`,
{
method: 'PUT',
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
};
soldOutMenu에 로컬 스토리지 저장 부분 지우고, 직접 토글하는 부분도 지워준다.
토글하는 요청 진행되고 > 변경된 상태를 포함해서 메뉴 리스트를 불러와준다.
const soldOutMenu = async(e) => {
const menuId = e.target.closest('li').dataset.menuId;
await MenuApi.toggleSoldOutMenu(this.currentCategory, menuId);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
};
const MenuApi = {
// 메뉴 삭제(DELETE)
async deleteMenu(category, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}`,
{
method: 'DELETE',
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
};
removeMenuName함수에서 기존의 splice로 메뉴를 삭제하고 localStorage에 세팅하는 코드 지워준다.
const removeMenuName = async (e) => {
if (!confirm('정말 삭제하시겠습니까?')) return;
const menuId = e.target.closest('li').dataset.menuId;
await MenuApi.deleteMenu(this.currentCategory, menuId);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(
this.currentCategory,
);
render();
};
현재 한 파일에 function App과 API 객체가 같이 있으므로,
src/api/index.js에 API 호출해주는 코드를 따로 분리해준 뒤 import 해준다.
// src/api/index.js
const BASE_URL = 'http://localhost:3000/api';
const MenuApi = {
// 모든 메뉴 불러오는 API 요청(GET->default)
async getAllMenuByCategory(category) {
const response = await fetch(`${BASE_URL}/category/${category}/menu`);
return response.json();
},
// 메뉴 생성하는 API요청(POST)
async createMenu(category, name) {
const response = await fetch(`${BASE_URL}/category/${category}/menu`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
// 메뉴 수정하는 API요청(PUT)
async updateMenu(category, name, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
return response.json();
},
// 메뉴 품절 토글(PUT)
async toggleSoldOutMenu(category, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}/soldout`,
{
method: 'PUT',
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
// 메뉴 삭제(DELETE)
async deleteMenu(category, menuId) {
const response = await fetch(
`${BASE_URL}/category/${category}/menu/${menuId}`,
{
method: 'DELETE',
},
);
if (!response.ok) {
console.error('에러가 발생했습니다.');
}
},
};
export default MenuApi;
// src/js/index.js
import MenuApi from "../api/index.js"
function App() { ........ }
서버에서 처리가 되어있는 부분이 있어서, 중복된 메뉴를 입력하면 error메시지를 보내줌.
서버로 요청을 보내기 전에 프론트에서도 사용자가 잘못된 요청을 아예 하지 않게끔, 중복 메뉴 추가하지 못하게끔 기능 추가
const addMenuName = async () => {
if ($('#menu-name').value === '') {
alert('값을 입력해주세요.');
return;
}
const duplicatedItem = this.menu[this.currentCategory].find(
(menuItem) => menuItem.name === $('#menu-name').value,
);
console.log(duplicatedItem);
if (duplicatedItem) {
alert('이미 등록된 메뉴입니다. 다시 입력해주세요.');
$('#menu-name').value = '';
return;
}
const menuName = $('#menu-name').value;
await MenuApi.createMenu(this.currentCategory, menuName);
render();
$('#menu-name').value = '';
};
**this.menu[this.currentCategory]**
에 사용자가 입력한 메뉴가 없어야 하는데, 그것을 찾는 객체⇒ 같은 게 없다면 담기는 것이 없고, 같은 게 있다면 담기는 것이 있을 것
const HTTP_METHOD = {
POST(data) {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
},
PUT(data) {
return {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : null,
// body값에 데이터가 있으면 보내고, 없으면 null
};
},
DELETE() {
return {
method: 'DELETE',
};
},
};
1) 요청에 대해 response 데이터가 있을 때
const request = async (url, option) => {
const response = await fetch(url, option);
if (!response.ok) {
alert('에러가 발생했습니다.');
console.error('에러가 발생했습니다.');
}
return response.json();
};
2) 요청에 대해 response 데이터가 없을 때
const requestWithoutJson = async (url, option) => {
const response = await fetch(url, option);
if (!response.ok) {
alert('에러가 발생했습니다.');
console.error(e);
}
return response;
};
const MenuApi = {
// 모든 메뉴 불러오는 API 요청(GET->default)
// 불러오는 요청 => option이 따로 필요 없음
// fetch가 알아서 get으로 인식
async getAllMenuByCategory(category) {
return request(`${BASE_URL}/category/${category}/menu`);
},
// 메뉴 생성하는 API요청(POST)
async createMenu(category, name) {
return request(
`${BASE_URL}/category/${category}/menu`,
HTTP_METHOD.POST({ name }),
);
},
// 메뉴 수정하는 API요청(PUT)
async updateMenu(category, name, menuId) {
return request(
`${BASE_URL}/category/${category}/menu/${menuId}`,
HTTP_METHOD.PUT({ name }),
);
},
// 메뉴 품절 토글(PUT)
async toggleSoldOutMenu(category, menuId) {
return request(
`${BASE_URL}/category/${category}/menu/${menuId}/soldout`,
HTTP_METHOD.PUT(),
);
},
// 메뉴 삭제(DELETE)
async deleteMenu(category, menuId) {
return requestWithoutJson(
`${BASE_URL}/category/${category}/menu/${menuId}`,
HTTP_METHOD.DELETE(),
);
},
};
이렇게 바닐라 자바스크립트로 문벅스 메뉴관리 끝! 시간 되면 React로 재구성 해보는 것도 나쁘지 않은 것 같다는 생각이 든다..!