React 작고 소중한 CRUD 구현.. (with axios)
데이터베이스는 없음.
서버에 데이터 두고
axios로 GET, POST, PUT, DELETE 요리조리 해봤다.
서버부터 복기해봅시다.
서버
app.js
const express = require('express');
const app = express();
const cors = require('cors');
const morgan = require('morgan');
app.use(morgan('tiny'));
app.use(cors());
app.use(express.json());
const port = 4000;
const discussionsRouter = require('./router/discussions');
const { addHook } = require('dompurify');
app.use('/discussions', discussionsRouter);
app.get('/', (req, res) => {
res.status(200).send('fe-sprint-my-agora-states-server');
});
const server = app.listen(port, () => {
console.log(`[RUN] My Agora States Server... | http://localhost:${port}`);
});
module.exports.app = app;
module.exports.server = server;
이제 그냥 이건 복붙해서 써도 될 것 같다. 서버를 뚝딱 만들어주는 고마운 코드들..
그래도 알아야 할 건 있다. 라우터를 써서 localhost:4000/discussions/
로 접속할 시에 이루어질 동작들을 따로 빼줬다는 것.
discussions.js
const { discussionsController } = require('../controller/discussionController');
const { findAll, findById, create, update, deleteById } = discussionsController;
const express = require('express');
const router = express.Router();
router.get('/', findAll);
router.get('/:uuid', findById);
router.post('/', create);
router.put('/:uuid', update);
router.delete('/:id', deleteById);
module.exports = router;
모든 데이터를 불러오는 GET 요청 'findAll'
요청의 params에 적힌 id에 해당하는 값을 불러오는 'findById'
POST 요청 'create'
PUT 요청 'update'
DELETE 요청 'deleteById'
총 다섯 가지의 메서드를 통해 서버와 통신한다.
discussionController.js
const { v4: uuid } = require('uuid');
const { agoraStatesDiscussions } = require("../repository/discussions");
const discussionsData = agoraStatesDiscussions;
const discussionsController = {
findAll: (req, res) => {
res.status(200).send(discussionsData);
},
findById: (req, res) => {
const { uuid } = req.params;
if (uuid) {
const uuidFiltered = discussionsData.find((el) => // 하나만 찾을 때는 find로. 찾은 요소를 리턴
el.id === Number(uuid)
)
if(!uuidFiltered) return res.status(404).send();
return res.status(200).send(uuidFiltered);
}
},
create: (req, res) => {
console.log(req.body);
const discussion_uuid = uuid();
const { title } = req.body;
const { author } = req.body;
const newDiscussion = {
"id": discussion_uuid,
"createdAt": new Date(),
"title": title,
"url": "https://github.com/codestates-seb/agora-states-fe/discussions/4",
"author": author,
"answer": null,
"avatarUrl": "https://avatars.githubusercontent.com/u/12145019?s=64&u=5c97f25ee02d87898457e23c0e61b884241838e3&v=4",
}
agoraStatesDiscussions.unshift(newDiscussion);
res.status(201).json(agoraStatesDiscussions);
},
update: (req, res) => {
console.log(req.body);
const { uuid } = req.params;
const bodyData = req.body;
const index = agoraStatesDiscussions.findIndex((el) => String(el.id) === uuid);
const updatedDiscussion = { ...agoraStatesDiscussions[index], ...bodyData };
agoraStatesDiscussions.splice(index, 1, updatedDiscussion)
return res.status(200).json(agoraStatesDiscussions);
},
deleteById: (req, res) => {
console.log(req.params);
const { id } = req.params;
const index = agoraStatesDiscussions.findIndex((el) => String(el.id) === id);
agoraStatesDiscussions.splice(index, 1);
return res.status(200).json(agoraStatesDiscussions);
}
};
module.exports = {
discussionsController,
};
복잡하다.. 하나씩 뜯어서 살펴보자
findAll: (req, res) => {
res.status(200).send(discussionsData);
},
findById: (req, res) => {
const { uuid } = req.params;
if (uuid) {
const uuidFiltered = discussionsData.find((el) => // 하나만 찾을 때는 find로. 찾은 요소를 리턴
el.id === Number(uuid)
)
if(!uuidFiltered) return res.status(404).send();
return res.status(200).send(uuidFiltered);
}
},
findAll
localhost:4000/discussions
가 입력되면, (입력된다는 표현이 맞나?)
discussionsData(모든 데이터)를 보내준다.
findById
localhost:4000/discussions/:uuid
가 입력되면,
discussionsData 중에서 params의 id 값과 같은 것을 찾아서 응답한다.
el.id는 숫자지만, uuid라는 변수는 params에서 온 거라 문자열이기 때문에 Number 함수를 써서 변환시켜준다.
find 메서드를 써서 찾은 값이 하나도 없을 경우에는 404를 보낸다. page not found
create: (req, res) => {
const discussion_uuid = uuid();
const { title } = req.body;
const { author } = req.body;
const newDiscussion = {
"id": discussion_uuid,
"createdAt": new Date(),
"title": title,
"url": "https://github.com/codestates-seb/agora-states-fe/discussions/4",
"author": author,
"answer": null,
"avatarUrl": "https://avatars.githubusercontent.com/u/12145019?s=64&u=5c97f25ee02d87898457e23c0e61b884241838e3&v=4",
}
agoraStatesDiscussions.unshift(newDiscussion);
res.status(201).json(agoraStatesDiscussions);
},
create
POST 요청이 올 경우 수행하는 내용.
newDiscussion 이라는 객체를 만든다.
이건 공부용으로 짠 코드이기 때문에 중간 중간 기본값으로 넣어준 데이터들이 있지만, 실무에서는 저따구로 하면 안되겠죵?
실제 코드에 쓰이는 값, 화면에 보이는 값들 위주로 data를 post할 수 있도록 한다.
(id, createdAt, title, author, answer)
서버에 있는 데이터에 새로 만들어진 객체를 unshift해주고
클라이언트에게는 전체 객체를 리턴한다.
나중에 클라이언트 코드를 보면 알겠지만
받은 다음에 다시 GET 요청을해서 state를 초기화한다. (이렇게 하는 방식은 좀 비효율적인 것 같기도?)
update: (req, res) => {
console.log(req.body);
const { uuid } = req.params;
const bodyData = req.body;
const index = agoraStatesDiscussions.findIndex((el) => String(el.id) === uuid);
const updatedDiscussion = { ...agoraStatesDiscussions[index], ...bodyData };
agoraStatesDiscussions.splice(index, 1, updatedDiscussion)
return res.status(200).json(agoraStatesDiscussions);
},
update
PUT 요청이 올 경우 수행하는 내용.
findIndex 메서드를 사용해서 id가 일치하는 놈의 인덱스를 찾고
그 인덱스에 해당하는 요소에 bodyData를 덮어씌운다. (이렇게 하면 키값이 같은 놈들이 갱신된다.)
바보같이 여기서는 또 String 함수를 써서 일치시켜줬네. 통일 좀 하자 통일 좀. 하지만 우리의 소원은 통일이 아니다. 챗 지피티를 무찌르자?
deleteById: (req, res) => {
const { id } = req.params;
const index = agoraStatesDiscussions.findIndex((el) => String(el.id) === id);
agoraStatesDiscussions.splice(index, 1);
return res.status(200).json(agoraStatesDiscussions);
}
deleteById
DELETE 요청이 올 경우 수행하는 내용.
딱히 다른 건 없고 findIndex로 해당 인덱스 찾은 후에 splice로 빼준다.
클라이언트
이제부터가 진짜 우리들의 싸움이다.. 한 번 가볼까?..
styled-components를 포함해 잡다한 걸 많이 시도해봤지만,
서버 통신과 관련된 부분만 짚도록 하겠다.
절대 귀찮아서가 아니다. 개발자에게 중요한건 명료함과 가독성.
App.js
import './App.css';
import Header from './components/Header';
import Submit from './components/Submit';
import Discussions from './components/Discussions';
import axios from 'axios';
import { StyledScreen } from './StyledScreen';
import { useState, useEffect } from 'react';
function App() {
const [ agora, setAgora ] = useState([]);
useEffect(() => {
const getAgora = async () => {
const response = await axios.get('http://localhost:4000/discussions');
const copy = response.data;
setAgora(copy);
};
getAgora();
}, [])
return (
<StyledScreen>
<Header />
<Submit agora={agora} setAgora={setAgora}/>
<Discussions agora={agora} setAgora={setAgora}/>
</StyledScreen>
);
}
export default App;
useEffect를 쓰는 이유?
클라이언트가 렌더링되자마자 데이터를 불러오기 위해서. (좀 더 명확한 설명을 할 수 있으면 좋겠다. 체크체크해보자)
useEffect 안에 getAgora 라는 함수를 만들고 axios를 통해 데이터를 요청한다. (GET)
요청한 데이터가 오면, copy라는 변수에 그 데이터를 할당하고,
agora라는 state를 갱신한다.
왜 copy로 했나요? 라고 물어본다면 대답을 해야겠죠.. ㅎㅎ?
오늘 왜 이렇게 혼잣말로 쓰냐고 물어보신다면 그것도 대답을 해야하나요? 싫소
useState로 상태를 관리할 때,
참조 자료형인 경우 갱신이 안 될 때가 있다.
예를 들어,
agora라는 state의 초기값이 [1, 2, 3] 이고 agora.push('4')를 해준 후에 setAgora(agora)하면
상태가 갱신되지 않는다. 주소가 같아서 똑같은 놈으로 생각하기 때문.
그래서 나는 그냥 갱신될지 안될지 따지기 귀찮아서 무조건 copy 뜨고 set 한다. 이건 코딩애플 강의에서 배운 것이니 따라하기 싫으면 자신만의 방법을 찾도록 하자.
Discussions.js
JSX 문법은 티스토리에서 하이라이팅이 제대로 안 되기 때문에 캡처로 승부하겠다.
어줍짢은 문법 표시는 우리를 더 힘들게 할 뿐이기 때문이다.
isEditing과 editedTitle은 이름부터 느껴지겠지만,
PUT 요청을 위한 state다. 일단 이런 걸 선언해놨다는 것 정도만 생각해놓자.
맵핑되면서 요소 하나 당 저 세가지가 생성된다. (간략하게 설명해서 그렇단 거임)
프사가 들어갈 박스, 제목과 저자가 들어갈 박스, 답변된 질문인지 확인할 수 있는 표시 박스.
EditButton, DeleteButton은 각각
PUT 요청, DELETE 요청 시 필요한 버튼이다.
진짜 엄청나게 머리 터질 정도로 노력해서 짠 코드인데,
막상 블로그에 몇 줄 적을라고 보니 적을 말이 딱히 없을 정도로 간단하네요?
이게 내가 앞으로 갈 길인 것인가? 참나.. 웃긴다 웃겨~
이제 맵핑되는 것까지 봤으니까 POST 요청 어떻게 진행되는지 살펴보자.
아까 말했던 부분이 이거다.
POST 요청한 후에 바로 다시 GET 요청하는 거.
이렇게 안하고 그냥 POST 요청에 대한 응답을 가지고 state를 갱신할 수도 있을 것 같다.
예를 들어, 서버 쪽에서
res.status(201).json(agoraStatesDiscussions) 해준 것이 저 요청에 대한 응답이므로,
axios.post 문을 하나의 변수에 담고 그걸로 setAgora(변수명) 해주는 거지. 이게 될까? 되겠지? 한 번 해볼게요
항상 이렇다. 이거 해결되면 글 수정하겠사옵니다.
이렇게 POST 요청도 끝! 이제 DELETE 한 번 보자
DELETE 요청을 보낸 후에 GET 요청해서 state 갱신한다.
다시 느끼네,, 아까는 진짜 머리 빠개지는 줄 알았는데 이렇게 멋진 척 하려고 코드 캡처해놓고 보니까 너무 간단해서 쓸 것도 없습니다.
하지만.. update는 다르지.. 아주 쓸 말이 많다
이걸로 'redux를 써야하는 이유.png' 로 짤 만들어도 되겠다.
수많은 props들이 보이는가,,
내가 클린 코드를 못 짜서 저렇게 많이 넘긴 것일 수도 있겠지만
진짜 최소 한 3~4개 정도는 보내줘야 제대로 작동할 것이다. 아마도.....
String(el.id) !== String(isEditing) 이거 왜 비교하게?
혼자 한 번 생각해보고 스크롤 내려라
우리는 isEditing이라는 state에다가 update 하고 싶은 요소의 id를 넣어줄 것이다.
그래서 저 두개가 서로 일치하면 HTML에 변동을 주는 것이지..
저런 식으로 작성하지 않고 처음에는 상태값을 true / false 로 뒀었는데,
그럼 하나를 수정하려고 하면 모든 요소의 HTML이 다 바뀐다. 곤란하지 그건!
아무튼 저~~~ 밑에 있는 EditButton을 누르면 그 즉시 해당 요소의 id를 따서
그 요소가 있는 부분(HTML)을 바꿔준다. 이건 캡처 한 번 해볼게요 어케 생긴 건지
POST된 요청이 위와 같이 떠 있다. 안녕하세요1111. 왜 항상 저런 거 테스트할 때는 절묘하게 숫자를 섞을까?
아무튼 저기서 edit 버튼을 누르면?
이런 식으로 타이틀이 사라지고 textarea가 생성된다.
그 밑에는 edit 완료하겠다는 버튼과 취소하겠다는 버튼.
잘 수정된다. 홍식님 잘 계시죠? 보고싶습니다.
코드로 돌아가보자.
PUT 요청에는 크게 두가지 분기점이 있다.
1. edit을 눌러서 타이틀 사라지고 textarea 나오는 것
2. edit / cancel 골라서 누르는 것
1번은 위에서 설명했고,
2번 두가지 경우를 살펴보자
edit 누르면 어떻게 돼야 할까?
서버에 PUT 요청 들어가고, textarea 사라지고 바뀐 제목으로 수정되면 되겠죠?
그리고 isEditing 을 빈 문자열로 바꿔줘야 해요..
왜 그럴까요?? 안 그러면 edit 했는데도 영원히 textarea가 화면에 떠있겠죠
cancel 누르면 어떻게 돼야 할까?
isEditing을 빈 문자열로 갱신시켜서 모든 요소의 제목이 똑바로 나오게 해줘야겠죠
PUT 요청은 위와 같이 title만 갱신해주는 걸로 했습니다. 이게 뭐 회원가입해서 내 글만 수정할 수 있는 그런 게시판이 아니라 가지고,,
단순히 공부용으로 만든 것이기 때문에 제목만 바꿀 수 있게 한 번 비벼봤어요
이쯤하면 끝인 것 같아요 잘가요
코드 깔끔하게 정리되면 다시 찾아올게요