프론트엔드와 백엔드를 같이, 풀스택으로 메모장을 구현해보는 토이 프로젝트를 했다.
프론트엔드는 React + Typescrip를 사용했고, 백엔드는 Node.js로, 미들웨어는 express 구성했다. 데이터는 data.json 파일을 만들어 fs모듈로 I/O작업을 구현 하였다. 통신은 axios를 사용했고 메모장 구현에는 ReactQuill 라이브러리를 사용했다.
모든 데이터는 fs모듈로 I/O 작업을 했기 때문에 데이터를 가져와 파싱작업을 해준 뒤에 삭제가 되지 않은 항목들을 보내주었다.
req.body로 값을 받아서 'content'의 value로 넣어주고 id도 고유한 값이 될 수 있도록 uuid를 사용해서 데이터에 추가해 주었다. 코드는 아래와 같다.
app.post('/', (req, res) => {
const { content } = req.body
if (!content || content.length === 0) res.status(400).json({ msg: "content가 올바르지 않습니다." })
const list = {
id: v4(),
content,
created_at: Date.now(),
updated_at: null,
deleted_at: null
}
data.unshift(list)
res.json(list)
save()
})
메모장에서 작성되는 텍스트들을 body에 담아 보내주는 요청을 받아 변수에 담았다가, read가 필요할 때, 작성되고 있는 텍스트들을 응답 해주어 '자동 임시 저장'기능이 되게 하였다. 코드는 다음과 같다.
let tmp = ""
app.get('/tmp', (req, res) => {
res.json({ result: tmp })
})
app.post('/tmp', (req, res) => {
const { content } = req.body
if (!content || content.length === 0) res.status(400).json({ msg: "content가 올바르지 않습니다." })
tmp = content;
res.json({ result: true })
})
```
const onSubmit = useCallback(async()=>{
const onlyTextContent = edit.replace(/<[/\w\s"=-]*>/gi, "")
if(onlyTextContent.length === 0) {
alert('메모가 비어있있습니다.')
return;
} else {
const {data} = await axios.post('/', {
content: edit
})
setMemoList(prev => [data, ...prev])
setEdit('')
}
},[edit])
```
상세 페이지는 url parameter를 id로 받아서 해당하는 데이터 id로 받을 수 있게 처리했다. 이때, id는 uuid v4를 사용했기 때문에, 거의 고유한 숫자에 가까운 id 값을 받을 수 있다.
상세 페이지는 수정(업데이트) 및 삭제 기능도 있다. 상세 수정 및 삭제 역시, 고유 값을 확인하기 위해 id 값을 parameter로 req를 받아서 data의 'updated_at', 'deleted_at' key 값이 변경되게 한다. 실제로 현업에서는 실제로 DB를 삭제하는 경우는 드물고 이렇게 특정 flag 값을 주어서 구분한다. 코드는 아래와 같다.
function save() {
fs.writeFileSync('data.json', JSON.stringify(data), 'utf-8')
}
app.delete('/:id', (req, res) => {
const { id } = req.params
const dataId = data.find(list => list.id === id)
if (!isNaN(id)) res.status(400).json({ msg: "잘못된 id입니다." })
if (dataId.deleted_at !== null) res.status(404).json({ msg: "이미 제거된 메모입니다." })
dataId.deleted_at = Date.now()
res.json(dataId)
save()
})
app.put('/:id', (req, res) => {
const { id } = req.params
const dataId = data.find(list => list.id === id)
const { content } = req.body
if (!content || content.length === 0) res.status(400).json({ msg: "content가 올바르지 않습니다." })
if (!isNaN(id)) res.status(400).json({ msg: "잘못된 id입니다." })
if (dataId.deleted_at !== null) res.status(404).json({ msg: "이미 제거된 메모입니다." })
dataId.updated_at = Date.now()
dataId.content = content
res.json(dataId)
save()
})
The useParams hook returns an object of key/value pairs of the dynamic params from the current URL that were matched by the . Child routes inherit all params from their parent routes.
[출처] https://reactrouter.com/en/main/hooks/use-params
메모 선택 중복 삭제 기능의 경우에는, 서버에서는 수정 페이지와 마찬가지로 req.params.id 값으로 받아오면 된다. 단, 메모 전체 선택의 경우에는 이 API를 계속 호출해야하는 번거로움이 있기 때문에 하나의 요청을 받는 API를 만들어서 모든 데이터의 값에 deleted_at 값을 넣어주도록 한다.
function save() {
fs.writeFileSync('data.json', JSON.stringify(data), 'utf-8')
}
app.delete('/', (req, res) => {
const deletedArr = []
data.map(list => {
if (!list.deleted_at) {
list.deleted_at = Date.now()
deletedArr.push(list.deleted_at)
}
})
res.json(deletedArr)
save()
})
모든 메모 읽는 기능은 메인 페이지와 마찬가지로 모든 메모를 가져오는 get요청을 하면 되겠다.
첫 페이지 렌더 시에는, 메인페이지와 마찬가지로 useEffect 훅을 사용해서 get 요청으로 모든 메모 READ 기능을 해주면 된다.
선택되고, 제거되는 메모들의 상태관리는 useState 훅을 사용해서 state관리를 해주면 되겠다. 이 때, 다중 선택된 메모 삭제의 경우에는 Promise.all() 메소드를 이용해서 여러 개의 비동기를 병렬로 처리해주어 최적화를 해주었다.
[참고] https://code-masterjung.tistory.com/91
// 선택한 메모들을 배열로 담아 state 관리.
const selectList = useCallback(value=>{
setSelectedMemoList(prev => {
if (prev.includes(value.id))
return prev.filter(v => value.id !== v)
return [...prev, value.id]
})
},[setSelectedMemoList,])
// 다중(1개 이상) 선택된 메모 제거
const removeSelectedList = useCallback(async()=>{
const list = []
for (const id of selectedMemoList) {
// await axios.delete("/"+id)
list.push(axios.delete("/" + id))
}
await Promise.all(list)
await loadMemo()
resetSelectedList()
alert("제거 완료!")
},[selectedMemoList, loadMemo])
// 전체 선택된 메모 제거
const removeAllList = useCallback(async()=>{
if (!window.confirm("정말로 전체 제거를 하시겠습니까?")) {
return;
}
await axios.delete("/")
await loadMemo()
alert("제거 완료!")
resetSelectedList()
},[loadMemo])