얼마 전 백엔드와 통신을 하던 중 이상한 일이 발생하였다.
사건을 설명하자면...
나와 백엔드 한 분은 장바구니 기능을 담당하고 있었는데 백엔드에서 보내온 데이터(장바구니에 담긴 상품 정보)를 받는 get 메서드, 내가 수량을 조절해서 그 데이터를 보내는 patch 메서드, 그리고 상품을 삭제할 때 보내는 delete 메서드...
get 과 patch 메서드를 이용할 땐 데이터가 정상적으로 통신이 되었는데 이상하게 delete 에서만 문제가 발생되었다...
어떤 상황이냐면... 삭제 버튼을 누르면 백엔드쪽에서는 데이터가 성공적으로 삭제되었지만, 보여지는 쪽(프론트단)에서는 아무런 반응이 일어나지 않았다...
근데 새로고침하면 삭제 되있다... 이게 무슨 의미....????? 일단 확인해보니 delete 메서드를 쓰니 204 통신이 떴었다.
백엔드분은 204 에러는 성공적인 의미라고 하시고 나는 이게 어떻게 성공이 된 것이냐고 하고 서로 니 잘못 아니냐 투닥투닥... (물론 장난으로..)
그리고 자리에 돌아와서
몇시간 동안 내 코드를 점검해보고 또 점검해봤지만, 아무리 봐도 내가 실수한 곳은 없었다고 생각했다. 그래서 멘토님에게 들고갔더니 멘토님이 이건 블로그감이라고 하였다...? 에???
일단 delete 메서드를 이해하기 위해서는
fetch 의 기본 문법을 잘 이해하고 넘어가야 하기 때문에 아래 잘 정리되어있는 블로그를 참고하였다.
▼
https://yeri-kim.github.io/posts/fetch/
✅ API & fetch?
백앤드로부터 데이터를 받아오려면 api를 호출하고 데이터를 응답받습니다. 이 때 자바스크립트 Web API fetch() 함수를 쓰거나 axios 라이브러리를 사용할 수 있습니다.
참고로 Web API는 클라이언트 측에서 사용할 수 있는 자바스크립트 내장함수라고 생각하시면 됩니다.
axios 사용법도 굉장히 쉽습니다. fetch를 쓰다가 axios를 사용하는 것만으로는 사실 학습 의미가 없습니다. 어느 라이브러리를 더 써보았는지가 중요한 것이 아니라 http 통신의 요청와 응답에 대한 이해, Promise 개념 공부가 더 중요합니다. 이 글을 읽고 fetch()를 사용하여 코드를 구현해봤다면, Promise 에도 관심을 갖고 공부를 시작해 보세요.
✅ fetch() 함수 기본
fetch('api 주소')
.then(res => res.json())
.then(res => {
// data를 응답 받은 후의 로직
});
// 위의 화살표 함수를 ES5 함수 선언식으로 바꿔서 써보면..
fetch('api 주소')
.then(function(res) {
return res.json();
})
.then(function(res) {
// data를 응답 받은 후의 로직
});
첫 번째 코드에서 res를 보며 드는 생각은 무엇이었나요? 3개의 res가 모두 똑같은 변수라고 생각했으면 변수의 scope(스코프) 부터 다시 공부하고 와야 합니다. 위 코드에서 변수의 scope는 각 함수이므로 첫 번째 then와 두 번째 then 안에 있는 res는 서로 다른 것. 당연히 아시죠?
단지 둘다 응답이다 보니 response의 줄임말인 res를 사용했을 뿐입니다. 평소 fetch()를 질문 받을 때 이 것을 헷갈리는 분이 너무 많아서 언급했습니다.
✅ method가 get인 경우
fetch() 함수에서 default method는 get이다. API 명세가 아래와 같다면, 두번째 코드와 같이 호출할 수 있다.
⏩ API명세
설명: 유저 정보를 가져온다.
base url: https://api.google.com
endpoint: /user/3
method: get
응답형태:
{
"success": boolean,
"user": {
"name": string,
"batch": number
}
}
⏩ Get method
fetch('https://api.google.com/user/3')
.then(res => res.json())
.then(res => {
if (res.success) {
console.log(`${res.user.name}` 님 환영합니다);
}
});
✅ method가 post인 경우
fetch() 기본은 get이기 때문에 아무것도 작성하지 않아도 get으로 호출했는데, post인 경우에는 fetch() 함수에 method 정보를 인자로 넘겨주어야 한다.
아! 호출해야할 api가 get인지, post인지 모른다구요? 당연히 api를 개발한 백앤드 개발자에게 물어보셔야 합니다.
⏩ API명세
이번에는 아래와 같은 api 명세를 받았다고 합시다.
설명: 유저를 저장한다.
base url: https://api.google.com
endpoint: /user
method: post
요청 body:
{
"name": string,
"batch": number
}
응답 body:
{
"success": boolean
}
⏩ Get method
fetch('https://api.google.com/user', {
method: 'post',
body: JSON.stringify({
name: "yeri",
batch: 1
})
})
.then(res => res.json())
.then(res => {
if (res.success) {
alert("저장 완료");
}
})
- body는 JSON형태로 보내기 위해 JSON.stringfy() 함수에 객체를 인자로 전달하여 JSON형태로 변환했다.
- post로 데이터를 보낼 때 JSON.stringfy를 항상 하다보니 axios는 굳이 감싸주지 않고 객체만 작성해도 되는 편리한 점이 있다. 이렇듯 axios는 소소하게 편한한 설정을 제공해주고, 요청과 응답에 대한 확장성 있는 기능을 만들 수 있습니다.
- ✨ 실제로 POST 방식으로 API를 호출하는 경우에는 API 명세를 꼼꼼하게 살펴보고, 모르는 부분이 있으면 해당 API를 개발한 개발자에게 물어봐야 한다.(API에 대한 정보는 그걸 개발한 백엔드 개발자만이 알 수 있다...) 보내줄 데이터의 형식, 키 등이 맞지 않으면 에러가 나기 때문이다.
✅ method가 get인데 parameter를 전달해야 하는 경우
위의 get 예제에서 3이라는 user id를 path로 넘겨주었습니다. 그런데 path 말고 query string으로 넘겨줘야 할 수도 있습니다.
언제는 path로 언제는 query string이고 할 수 있다는 말은 아니고, 예제이다 보니 같은 api를 사용했습니다. 데이터를 전달하는 방식 또한 백앤드 개발자에게 물어봐야 합니다.
설명: 유저 정보를 가져온다.
base url: https://api.google.com
endpoint: /user
method: get
query string: ?id=아이디
응답형태:
{
"success": boolean,
"user": {
"name": string,
"batch": number
}
}
사실 특별한 것은 없습니다. api 주소 뒤에 그냥 붙여주면 됩니다.
fetch('https://api.google.com/user?id=3')
.then(res => res.json())
.then(res => {
if (res.success) {
console.log(`${res.user.name}` 님 환영합니다);
}
});
✅ fetch() res.json()의 의미
첫 번째 then 함수에 전달된 인자 res는 http 통신 요청과 응답에서 응답의 정보를 담고 있는 객체이다.
그런데 console을 확인해보면 백앤드에서 넘겨주는 응답 body, 즉 실제 데이터는 보이지 않을 것이다. 즉 { success: true } 라는 JSON 데이터는 위의 코드로는 console에 찍히지 않을 것이라는 말이다.
응답으로 받는 JSON 데이터를 사용하기 위해서는 Response Object 의 json 함수를 호출하고, return 해야한다. 그러면 이 값이 두 번째 then 함수의 인자로 온다.(Obj 형태로)
❓ 왜 json.parse가 아닌 json을 쓸까?
response 안에 정보는 너무 많고, 이 모든 데이터를 바꿔줄 필요는 없다. body에 있는 정보들만 꺼내 바꿔주는게 json이다.
✅ fetch() 함수 - 첫 번째 then 함수에 추가되는 로직
위의 내용까지 읽으면 fetch().then().then() 형태만 기억하면 될 것 같은데 왜 저렇게 설명했냐구요? 바로 백앤드에서 응답 body를 안 주는 경우도 많기 때문입니다!
응답 body로 JSON 데이터를 주지 않는데 프론트에서 Response Object의 json()을 호출하면 에러가 납니다.
다음과 같은 상황을 생각해보면 됩니다.
설명: 유저를 저장한다.
base url: https://api.google.com
endpoint: /user
method: post
요청 body:
{
"name": string,
"batch": number
}
응답 body:
1. 제대로 저장했으면 status code를 200 전달. 응답 body는 없음
2. 권한 오류가 생기면 status code를 403으로 전달하고. 응답 body는 아래와 같음
{
success: false,
message: "권한이 없습니다"
}
위의 상황일 때 어떻게 fetch() 를 구현하면 되는지 한 번 보겠습니다!
fetch('https://api.google.com/user', {
method: 'post',
body: JSON.stringify({
name: "yeri",
batch: 1
})
})
.then(res => {
if (res.status === 200) {
alert("저장 완료");
} else if (res.status === 403) {
return res.json();
}
})
.then(res => {
console.log("에러 메시지 ->", res.message);
})
어때요! 이제는 왜 then이 두 개 이고, 각각 어떤 역할을 하는지 잘 이해 되셨나요? 오늘도 공부하시느라 수고 많으셨습니다! 이제부터는 Promise를 파봅시다!!!
내가 실수한 부분은 위 블로그의 아랫내용이었다.
이제 장바구니 기능을 fetch 로 주고 받는 코드를 살펴보자. 여기서 주목할 것은 fetch 하고 난 후, .then .then 코드를 눈여겨보면 된다. (delete 와 다른 메서드들)
< GET 메서드 >
장바구니에 담긴 상품 정보를 받아온다.
useEffect(() => {
fetch('http://10.58.3.174:8000/orders/carts', {
method: 'GET',
headers: {
Authorization: localStorage.getItem('access_token'),
},
})
.then(res => res.json())
.then(data => {
setItems(data.cart_items);
});
}, []);
위 블로그에서 첫 번째 then 함수에 전달된 인자 res는 http 통신 요청과 응답에서 응답의 정보를 담고 있는 객체라고 하였다.
이 응답으로 받는 JSON 데이터를 사용하기 위해서는 Response Object 의 json 함수를 호출하고, return 해야한다. 그러면 이 값이 두 번째 then 함수의 인자로 온다.(Obj 형태로)
이제 백엔드에서 받아온 데이터 값(Obj) 을 프론트단에서 useState 변수인 setItems 에 지정을 해주었다.
< PATCH 메서드 >
+ 버튼을 눌렀을때 수량을 1 증가해서 백엔드에게 데이터를 보낸다.
const plusItemNum = index => {
fetch(`http://10.58.3.174:8000/orders/carts/${items[index].cart_id}`, {
method: 'PATCH',
headers: {
Authorization: localStorage.getItem('access_token'),
},
body: JSON.stringify({
count: items[index].count + 1,
}),
})
.then(res => res.json())
.then(data => {
data.message === 'SUCCESS'
? setItems(
items.map((item, i) =>
item.cart_id === items[index].cart_id
? {
...item,
count: item.count + 1,
}
: item
)
)
: alert(data.message);
});
};
아마 명세서 안 응답 body 부분에 message 라는 키 값이 있었을 것이고, 그 값은 SUCCESS 가 있었을 것이다.
< PATCH 메서드 >
- 버튼을 눌렀을때 수량을 1 감소해서 백엔드에게 데이터를 보낸다.
const miusItemNum = index => {
fetch(`http://10.58.3.174:8000/orders/carts/${items[index].cart_id}`, {
method: 'PATCH',
headers: {
Authorization: localStorage.getItem('access_token'),
},
body: JSON.stringify({
count: items[index].count - 1,
}),
})
.then(res => res.json())
.then(data => {
data.message === 'SUCCESS'
? setItems(
items.map((item, i) =>
item.cart_id === items[index].cart_id && item.count !== 1
? {
...item,
count: item.count - 1,
}
: item
)
)
: alert(data.message);
});
};
위와 동일하다.
< DELETE 메서드 >
x 버튼을 눌렀을때 백엔드에게 삭제되었음을 알리는 데이터를 보낸다.
const deleteItem = index => {
fetch(`http://10.58.3.174:8000/orders/carts/${items[index].cart_id}`, {
method: 'DELETE',
headers: {
Authorization: localStorage.getItem('access_token'),
},
}).then(res => {
res.status === 204
? setItems(
items.filter((item, i) => item.cart_id !== items[index].cart_id)
)
: alert('실패');
});
};
delete 메서드에서 .then 의 작성방식이 get, patch 와 다르다.
나는 delete 메서드를 위의 메서드들과 똑같이 적고 이상한 일을 겪었다고 했었는데 res.status 로 바꾸고 나니 x 버튼을 누를 때 상품이 정상적으로 지워지기 시작했다.
그래서 아무튼 응답 body가 없는데 .json 을 써주어서 204 에러가 떴던 것이었다. 백엔드는 데이터가 성공적으로 즉시 지워지지만 프론트에서는 에러가 났던 것....
그리고 메서드들 중에 DELETE 메서드는 응답 body 를 안주는 것이 특징이다...!!!! (중요중요)
장바구니를 맡으며 백엔드의 API 명세서를 참고해야 한다는 생각을 단 1도 못했었는데, 위 블로그를 보니 내가 작성한 코드가 잘 돌아간 것은 운이 좋았던 거였구나...를 깨달았다.
이번 2차 프로젝트에서는 백엔드와 API 명세서에 대해서 소통을 해야해야겠다는 생각이 많이 들었다.
'💎 React' 카테고리의 다른 글
styled-components (0) | 2021.12.13 |
---|---|
리액트 토큰 유무에 따른 UI 변경 (0) | 2021.12.09 |
장바구니 창 조절 (0) | 2021.12.03 |
상수데이터, 목데이터 (0) | 2021.12.01 |
리액트 훅, useEffect (0) | 2021.11.25 |