You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
지난 시간에 이어 XMLHttpRequest 객체를 통해 서버에 요청을 보내고 응답을 받아 처리해보자.
AJAX GET 복습
XMLHttpRequest 요청 코드
GET 요청을 하는 비동기함수 get은 다음과 같다.
엔드포인트를 url로 받아 서버로부터 정보를 가져오게 한다.
서버로부터 받은 응답을 파싱하여 콜백함수의 인자로 넘기도록 한다.
이 함수는 fetchTodos 함수에서 호출하면서, '/todos' 엔드포인트와 setTodos 콜백 함수를 인자로 넘긴다.
// GET '/todos'constget=(url,callback)=>{constxhr=newXMLHttpRequest()xhr.open('GET',url);xhr.send();xhr.onload=()=>{if(xhr.status===200){callback(JSON.parse(xhr.response));}else{console.log(xhr.status,xhr.statusText);}}}
POST
새로운 데이터를 넣는 addTodo 함수는 post를 호출한다.
XMLHttpRequest 요청 코드
get과 동일하게 xhr 객체를 만들고 요청을 날리지만, 이번에는 payload가 있다는 점이 다르다.
payload의 매개변수 순서는 어디가 될까? 보통은 callback을 마지막에 넣어주니까 그 앞에 넣어주자.
비동기 처리 성공 콜백과 실패 콜백 두개가 올 수도 있고, 콜백이 중간에 있으면 함수라고 인지하지 못할 수도 있으니.
그러나 node.js 진영에는 콜백을 맨 앞에 넣어주는 것이 좋다고 하는 사람들도 있다.
payload에 대한 메타데이터 또한 요청의 header에 세팅해줘야 한다. (xhr.setRequestHeader({헤더에 담길 키와 값}))
응답 상태코드도 200 뿐 아니라 201(created)이 넘어올 수 있다. (post한 데이터를 무사히 만들었다는 뜻)
// POST '/todos'constpost(url,payload,callback){constxhr=newXMLHttpRequest();xhr.open('POST',url);xhr.setRequestHeader('content-type','application/json');xhr.send(JSON.stringify(payload));xhr.onload=()=>{if(xhr.status===200||xhr.status===201){callback(JSON.parse(xhr.response));}else{console.log(xhr.status,xhr.statusText);}}}
서버사이드 응답 코드
서버에는 다음과 같이 post 요청에 대한 응답 로직을 마련해둔다.
요청 Request 객체의 body에는 payload가 들어있다. 이를 todos에 넣어준 후 todos 배열을 다시 보내준다.
// server.jsconstexpress=require('express');constapp=express();// mock datalettodos=[{...},{...},{...}];app.use(express.static('public'));app.use(express.json());// 'GET' 요청에 대한 응답 로직app.get('/todos',(req,res)=>{res.send(todos)});// 'POST' 요청에 대한 응답 로직app.post('/todos',(req,res)=>{constnewTodo=req.body;todos=[newTodo, ... todos];res.send(todos);});
server에서는 id를 가지고 들어오는 patch요청('/todos/:id')과 아닌 요청('/todos/')을 달리 처리한다.
id는 payload가 아닌 endpoint로 들어온다. 엔드포인트의 parameter로 넘어오는 정보는 키와 값으로 짝지어져있다
endpoint로 들어오는 값은 request 객체의 params라는 프로퍼티를 통해 참조할 수 있으며 이 때 params는 app.patch의 첫 인수로 들어온 endpoint에서 : 뒤에 기재한 매개변수명을 키로, 그리고 실제 요청의 엔드포인트에서 해당 위치에 실제로 들어오는 값을 값으로 갖는 객체를 준다.
// server.js// (1) 모든 completed 값을 변경하는 경우app.patch('/todos',(req,res)=>{const{ completed }=req.bodytodos=todos.map(todo=>({ ... todo, completed }));res.send(todos);});// (2) id로 들어온 값의 completed나 content만 변경하는 경우app.patch('/todos/:id',(req,res)=>{const{ id }=req.params;constpayload=req.body;// payload는 { completed: true }나 { content: React } 등의 형태로 되어있다. todos=todos.map(todo=>todo.id===+id ? { ... todo, ... payload} : todo);res.send(todos);});
요청 함수 호출 코드
응답로직까지 준비된 xhr의 patch 함수를 이제 toggleAll과 toggleTodoCompleted, updateTodoContent에서 호출.
completed 변경은 toggle이므로 payload 보낼 필요 없이 매 요청마다 서버에서 상태로 두고 있는 completed 값을 반전하기만 하면 된다.
그러나 content 변경과 일관된 로직 및 함수 재사용을 위해 적용될 completed값을 payload로 인수로 전달하며 호출하자.
toggleAll은 해당 checkbox input의 checked 프로퍼티의 값을 반전시킨 completed 값을 이벤트핸들러에서 받아 그대로 payload로 보낸다.
toggleTodoCompleted는 state.js가 가진 todos 데이터에서 해당 id의 completed 값을 취득하여, 반전시켜 payload로 보낸다.
content는 id값을 endpoint로, 변경할 값을 payload로 보낸다.
// state.jsconsttoggleAll=completed=>{patch('/todos',{ completed },setTodos);};// id를 가지고 해당 todo 데이터의 completed 값을 찾는다.consttoggleTodoCompleted=id=>{const{ completed }=state.todos.find(todo=>todo.id===+id);patch(`/todos/${id}`,{completed: !completed},setTodos);};constupdateTodoContent=(id,content)=>{patch(`/todos/${id}`,{ content },setTodos);};
DELETE 처리 코드
DELETE 요청 또한 모든 completed된 데이터를 지우는 요청과 특정 id에 해당하는 데이터만 지우는 요청으로 나뉜다.
'/todos/:id'로 오는 DELETE 요청은 해당 id를 갖는 데이터를 삭제하는 것으로 한다.
'/todos/completed'라는 엔드포인트로 오는 DELETE 요청에 대해 모든 completed 데이터를 삭제
원래는 엔드포인트에 completed를 쓰기보다 queryString으로 보내는 게 더 바람직
XMLHttpRequest 요청 코드
delete는 프로퍼티를 지우기 위해 JS에서 이미 가지고 있는 예약어이기 때문에 아쉽지만 remove라는 함수로 만들자.
다음과 같이 id를 받는 경우와 completed라는 엔드포인트까지 가진 경우의 응답 코드를 각각 작성한다.
// DELETE '/todos/:id' (id로 온 todo를 삭제, 즉 id가 같지 않은 todo만 남긴다.)app.delete('/todos/:id',(req,res)=>{const{ id }=req.params;todos=todos.filter(todo=>todo.id!==+id);res.send(todos);})// DELETE '/todos/completed' (completed 값이 false인 todo만 남긴다.)app.delete('/todos/completed',(req,res)=>{todos=todos.filter({ completed }=>!completed);res.send(todos);})
근데 이렇게 하면 completed를 가지고 한 요청들이 다 첫 번째 app.delete를 호출한다.
/todos/completed로 들어온 요청을 /todos/:id에 'completed'라는 문자열을 id로 하는 요청으로 인식하기 때문
이를 위해서는 /todos/completed를 엔드포인트로 받는 delete 함수를 상단으로 옮긴다.
그러나 순서는 언제나 바뀔 수 있으니 여전히 안정적인 해결책은 아니다.
/todos/:id를 받을 때 엔드포인트의 매개변수에 이어 소괄호 안에 정규표현식을 / 없이 적으면 해당 정규표현식에 맞는 url만 요청으로 인식하여 호출된다.
app.delete('/todos/completed',(req,res)=>{todos=todos.filter({ completed }=>!completed);res.send(todos);})app.delete('/todos/:id([0-9]+)',(req,res)=>{const{ id }=req.params;todos=todos.filter(todo=>todo.id!==+id);res.send(todos);})