게시판이지만 아직은 허접해 사실상 todo의 확장판 느낌이 나기는 하지만,
REST API(GET, POST, PUSH, DELETE)를 모두 사용했다는 것에서 의의를 두기로 했다.
게시판의 특징을 담고 있다.(약간의 열화판)
각각의 게시물들을 [수정, 삭제, 생성, 읽기]
할 수 있다.
사용한 기술은 redux, redux-thunk, rest api 이며,
서버와 통신하기 위해서 json-server를 임의로 만들어 주었다.
리덕스로 todo 구현하기: https://shyunju7.tistory.com/35?category=924416
building a todo app: https://pamit.medium.com/building-a-todo-app-using-react-redux-and-rails-fa260ebbdc44
actin, reducer를 담고 있는 items.js파일 소개
import { createAction, handleActions } from 'redux-actions';
const ITEM_INSERT = 'items/ITEM_INSERT';
const ITEM_REMOVE = 'items/ITEM_REMOVE';
const ITEM_UPDATE = 'items/ITEM_UPDATE';
const ITEM_LOAD = 'items/ITEM_LOAD';
// GET 전체 items 가져오기
export const loadItem = createAction(ITEM_LOAD, items => items)
// POST
export const insertItem = createAction(ITEM_INSERT, (id, content) => ({
id: id,
content
}))
// DELETE
export const removeItem = createAction(ITEM_REMOVE, id => id)
// PUT
export const updateItem = createAction(ITEM_UPDATE, (id, content) => ({
id: id,
content: content
}))
const items = handleActions(
{
[ITEM_LOAD]: (state, action) => ({
items: action.payload
}),
[ITEM_INSERT]: (state, action) => ({
items: state.items.concat(action.payload),
id: action.id
}),
[ITEM_REMOVE]: (state, { payload: id }) => ({
...state,
items: state.items.filter(item => item.id !== id)
}),
[ITEM_UPDATE]: (state, { payload: id, content }) => ({
...state,
items: state.items.map((item) =>
item.id === id ? { ...item, content: content } : item)
})
},
{
items: []
}
)
export default items;
4개의 액션과 리듀서를 생성했고, 각각의 액션들은 게시판의 아이템을
생성, 삭제, 수정, 데이터 받아오기
에 관여하고 있다.
redux를 기존보다 편하게 사용하기 위해서 이번에는 redux-actions
라이브러리를 사용했다.
redux-actions
createAction
: 액션 함수
handleActions
: 리듀서 함수
- 주의할 점은
handleActions
가 아닌handleAction
을 사용한다면 에러가 발생하는데, 이것때문에 코드를 전부 뜯어보다가 구글링으로 겨우 알아냈다,,
리듀서 함수를 작성하면서 항상 들었던 의문점이 state가 없을 경우를 대비해 보통 initialState 객체를 통해 초기화된 프로퍼티 값을 받아오는데,
이로 인해 항상 원치 않던 첫번째 요소가 생성되어 존재하게 되었다.
생각보다 간단했다. initialState 객체에 큰 틀만 잡아두고 프로퍼티를 사용하지 않으면 된다는 것이였다. 따라서 위의 코드에서 보다싶이, initialState 자리를 { items:[] }
로 대체함으로써 해결하였다.
redux-thunk
redux-thunk 는 미들웨어로, 객체 대신 함수를 생성하는 액션 생성함수를 작성 할 수 있게 해준다. 즉, 비동기 처리가 가능하다.
리덕스에서는 기본적으로는 액션 객체를 디스패치하기에 한계가 있었는데, 이를redux-thunk
로 보완 가능하다.
import React, { useEffect } from 'react';
import { loadItem } from '../redux/items';
import { useSelector, useDispatch } from 'react-redux';
import BoardItem from './BoardItem';
import ItemInput from './ItemInput';
import axios from 'axios';
const BoardList = () => {
const dispatch = useDispatch()
const items = useSelector(state => state.items.items);
// get으로 서버에 있는 게시글 모두 불러오기
useEffect(() => {
const fetchItem = async () => {
axios.get('/items')
.then(response => {
dispatch(loadItem(response.data));
})
.catch(err => console.log(err))
};
fetchItem();
}, [dispatch])
return (
<div className="BoardList">
<ItemInput />
{items.map(item => (
<BoardItem key={item.id} content={item.content} id={item.id} />
))}
</div>
)
}
export default BoardList;
이 코드를 짜면서 가장 많이 해맸던 것 같다.
게시글이 삭제되거나 수정되면 이러한 정보들을 멈추지 않고 계속해서 GET으로 받아와야 하기 때문이다. 그러던 참에 useEffect라는 Hook을 발견했다.
useEffect
: sHook을 이용하여 우리는 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말한다.
이러한 useEffect
를 사용해 렌더링이 일어날 때마다 게시글이 불러와지게 만들었다.
import React, { useState, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { insertItem } from '../redux/items';
import axios from 'axios';
const ItemInput = () => {
const dispatch = useDispatch();
const onInsert = (id, content) => dispatch(insertItem(id, content));
const onfocus = useRef();
const [text, setText] = useState('');
const onChange = e => setText(e.target.value);
const onSubmit = async e => {
e.preventDefault();
if (text === '') {
return alert('내용을 입력하세요');
}
let data = {
content: text
}
axios.post('/items', data)
.then(response => {
onInsert(response.data.id, text);
onfocus.current.focus();
})
setText('');
};
return (
<div className="ItemInput">
<form onSubmit={onSubmit}>
<input
value={text}
placeholder="글 입력"
onChange={onChange}
ref={onfocus}
/>
<button type="submit">확인</button>
</form>
</div>
)
}
export default ItemInput
지속적으로 게시물을 작성할 수 있도록, useRef
를 사용해 form
이 onSubmit 될때마다 input
태그에 포커스가 가도록 만들었다.
post를 사용해 content: text 값을 item에 넣어주었다.
import React, { useState, useRef } from 'react'
import moment from 'moment';
import 'moment/locale/ko';
import { useDispatch } from 'react-redux';
import { removeItem, updateItem } from '../redux/items';
import axios from 'axios';
let date = moment().format('YYYY. MM. DD. HH:mm:ss');
const BoardItem = ({ id, content }) => {
const dispatch = useDispatch();
const [readOnly, setReadOnly] = useState(true);
const [updateText, setUpdateText] = useState(content);
const onfocus = useRef();
const onChangeText = (e) => {
const { value } = e.target;
setUpdateText(value);
}
const editContent = async () => {
setReadOnly(!readOnly)
let data = {
content: updateText
}
console.log(updateText); //test
axios.put(`/items/${id}`, data)
.then(onfocus.current.focus())
}
const deleteContent = async () => {
dispatch(removeItem(id))
axios.delete(`/items/${id}`)
}
return (
<div className="BoardItem">
<h3 className="number">{id}</h3>
<input
name="content"
readOnly={readOnly}
defaultValue={content}
onChange={onChangeText}
onBlur={() => dispatch(updateItem(id, updateText))}
ref={onfocus}
/>
<h3>{date}</h3>
<button onClick={editContent}>readonly</button>
<button onClick={deleteContent}>삭제</button>
</div>
)
}
export default BoardItem
코드가 긴데, 이해하기 위해서는 item에 어떤 것들이 들어가있는지 확인할 필요가 있다.
id
(index 값) 표시input 태그
onBlur
. 포커스를 잃었을 시에, updateITem 액션을 dispatch한다.date
표시readOnly
값이 토글되면서 작성이 가능해진다.삭제 버튼을 눌렀을 시에, id값으로 index를 매기다보니, (1,2,5) 이런식으로 index에 구멍이 생긴다.
뭔가 확실치는 않지만 redux-thunk를 썼음에도 크게 활용을 못한 느낌..?
컴포넌트 파일 이름이 마음에 안든다.
date 를 잘못 썼는지, 입력 시간이 제대로 업데이트 되지 않음.
추후 이러한 부분을 고쳐나가며, 전에 만들었던 사용자 인증과 결합할 예정에 있다.
코드를 자세히 보고 싶다면, 깃허브 링크를 참고하세요:
https://github.com/OseungKwon/practice-react/tree/main/board