[React] 게시판 만들기(with redux, redux-thunk)

자몽·2021년 7월 19일
3

Toy-Project

목록 보기
6/13
post-thumbnail

소개

게시판이지만 아직은 허접해 사실상 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파일 소개

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로 보완 가능하다.

BoardList.js

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를 사용해 렌더링이 일어날 때마다 게시글이 불러와지게 만들었다.

ItemInput.js

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에 넣어주었다.

BoardItem.js

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 값) 표시

  • inputItem을 통해 넣어주었던 값이 들어있는 input 태그

    input 태그로 만들어 놓은 이유는, 텍스트 위에서 바로 수정할 수 있게 하기 위해서이다.
    onBlur. 포커스를 잃었을 시에, updateITem 액션을 dispatch한다.
  • 각 게시물의 작성 날짜 date 표시

  • 수정 버튼

    수정 버튼을 누르면 readOnly값이 토글되면서 작성이 가능해진다.
    게시글 수정 이후 다시 이 버튼을 누르면, 수정된 값이 put을 통해 업데이트된다.
  • 삭제 버튼

    삭제 버튼을 누르면, delete를 통해 서버에서 해당 게시글이 삭제된다.

완성

개선이 필요한 부분

  1. 삭제 버튼을 눌렀을 시에, id값으로 index를 매기다보니, (1,2,5) 이런식으로 index에 구멍이 생긴다.

  2. 뭔가 확실치는 않지만 redux-thunk를 썼음에도 크게 활용을 못한 느낌..?

  3. 컴포넌트 파일 이름이 마음에 안든다.

  4. date 를 잘못 썼는지, 입력 시간이 제대로 업데이트 되지 않음.
    추후 이러한 부분을 고쳐나가며, 전에 만들었던 사용자 인증과 결합할 예정에 있다.

코드를 자세히 보고 싶다면, 깃허브 링크를 참고하세요:
https://github.com/OseungKwon/practice-react/tree/main/board

profile
꾸준하게 공부하기

0개의 댓글