React/게시판만들기 v2.

[React][CRUD] 게시판 만들기 All in One (10). 설정 만들기 (총 복습용), redux, action, redux-saga, redux-toolkit, axios, get, post, put, delete

binaryJournalist 2021. 4. 15. 09:32
반응형

 

목차 돌아가기:

binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

 

이건 원래 계획에 없었는데

 

 

근데 깨끗한 db상태에서 시작하려면 필요할 것 같아 결국 만들었다.

 

이제껏 해온 saga, redux, CRUD의 총집합이다. (react-router가 없군..!)

 

 

board와 code만 db에 남기면 되기에 필요없는 사람들은

articles와 comments 만 빈 배열로 남기면 된다.

 

 

그리고 만들어야 할 컴포넌트는 4개이고 slice와 saga는 codeSlice, codeSaga, 수정이 필요한 곳은 boardSlice, boardSaga인데 만들면서도 왔다갔다한 과정이 많아서

 

최종 소스를 갔다놓고 설명하려 한다.

 

만드는 과정은 이전 과정과 거의 비슷해서 useState를 리스트에 적용한 부분만 빼면 나머지 부분은 스스로 만들기 어렵지 않을 것이다.

오히려 지금의 소스보다 더 깔끔하게 만들 수도 있을 것이다.

 

 

1. slice

 

// boardSlice.js

import { createSlice } from "@reduxjs/toolkit";

const name = "board";

const initialState = {
    boardList: [],
    status: 0,
    statusText: "Loading"
};

const reducers = {
    getBoardList: (state, action) => {},
    getBoardListSuccess: (state, action) => {
        state.boardList = action.payload?.data ?? [];
        state.status = action.payload?.status;
        state.statusText = action.payload?.statusText ?? "Success";
    },
    getBoardListFail: (state, action) => {
        state.boardList = initialState.boardList
        state.status = action.payload?.status ?? 500;
        state.statusText = action.payload?.statusText ?? "Network Error";
    },

    getBoard: (state, action) => {},  //추가됐으나 사용 안함
    getBoardSuccess: (state, action) => {},  //추가됐으나 사용 안함
    getBoardFail: (state, action) => {},  //추가됐으나 사용 안함

    postBoard: (state, action) => {}, //추가 - 게시판 신규 등록
    postBoardSuccess: (state, action) => {}, //추가
    postBoardFail: (state, action) => {}, //추가

    putBoard: (state, action) => {}, //추가 - 게시판 개별 수정
    putBoardSuccess: (state, action) => {}, //추가
    putBoardFail: (state, action) => {}, //추가

    deleteBoard: (state, action) => {}, //추가 - 게시판 개별 삭제
    deleteBoardSuccess: (state, action) => {}, //추가
    deleteBoardFail: (state, action) => {}, //추가
};

const boardSlice = createSlice({
    name,
    initialState,
    reducers
});

export const boardReducer = boardSlice.reducer;
export const boardActions = boardSlice.actions;

 

설정 만들면서 의도하던 건 게시판 목록 전체를 불러와서 수정된 게시판들을 한꺼번에 저장하려 했으나 배열 put을 이용하여 수정하는 부분에서 실패하여 결국 개별 수정이 되었다.

 

다중 건 수정이 불가하다면 삭제도 마찬가지로 다중 건은 불가능할 거 같아 결국 개별 수정 및 개별 삭제 방향으로 바뀌게 됐다.

 

 

// codeSlice.js

import { createSlice } from "@reduxjs/toolkit";

const name = "Code";

const initialState = {
    codeList: [],
    status: 0,
    statusText: "Loading"
};

const reducers = {
    getCodeList: (state, action) => {},
    getCodeListSuccess: (state, action) => {
        state.codeList = action.payload?.data ?? [];
        state.status = action.payload?.status;
        state.statusText = action.payload?.statusText ?? "Success";
    },
    getCodeListFail: (state, action) => {
        state.codeList = initialState.codeList
        state.status = action.payload?.status ?? 500;
        state.statusText = action.payload?.statusText ?? "Network Error";
    },

    getCode: (state, action) => {}, // 추가됐으나 사용 안 함
    getCodeSuccess: (state, action) => {}, // 추가됐으나 사용 안 함
    getCodeFail: (state, action) => {}, // 추가됐으나 사용 안 함

    postCode: (state, action) => {}, // 추가
    postCodeSuccess: (state, action) => {}, // 추가
    postCodeFail: (state, action) => {}, // 추가

    putCode: (state, action) => {}, // 추가
    putCodeSuccess: (state, action) => {}, // 추가
    putCodeFail: (state, action) => {}, // 추가

    deleteCode: (state, action) => {}, // 추가
    deleteCodeSuccess: (state, action) => {}, // 추가
    deleteCodeFail: (state, action) => {}, // 추가
};

const codeSlice = createSlice({
    name,
    initialState,
    reducers
});

export const codeReducer = codeSlice.reducer;
export const codeActions = codeSlice.actions;

 

코드도 게시판과 마찬가지로 개별 수정 및 개별 삭제 방향으로 진행한다.

 

 

2. 컴포넌트

 

 

2. (0) 설정 * 위치는 /src/views 이다.

 

// src/views/Control.js

import React, { useState } from 'react';
import CreateBoard from './components/CreateBoard';
import CreateCode from './components/CreateCode';
import UpdateBoardList from './components/UpdateBoardList';
import UpdateCodeList from './components/UpdateCodeList';

function Control() {
    const [ showCreateCode, setShowCreateCode ] = useState(false);
    const [ showCreateBoard, setShowCreateBoard ] = useState(false);
    const [ showUpdateCodeList, setShowUpdateCodeList ] = useState(false);
    const [ showUpdateBoardList, setShowUpdateBoardList ] = useState(false);
    
    function onClickCreateCodeButton() {
        (showCreateCode) ? setShowCreateCode(false) : setShowCreateCode(true);
    }

    function onClickCreateBoardButton() {
        (showCreateBoard) ? setShowCreateBoard(false) : setShowCreateBoard(true);
    }

    function onClickUpdateCodeList() {
        (showUpdateCodeList) ? setShowUpdateCodeList(false) : setShowUpdateCodeList(true);
    }

    function onClickUpdateBoardList() {
        (showUpdateBoardList) ? setShowUpdateBoardList(false) : setShowUpdateBoardList(true);
    }

    return (
        <div>
            <div>
                <span>설정</span>
            </div>
            <div>
                <div>
                    <div>
                        <div>
                            <button onClick={onClickCreateCodeButton}>새 코드</button>
                        </div>
                        <div>
                            {
                                showCreateCode && <CreateCode setShowCreateCode={setShowCreateCode} />
                            }
                        </div>
                    </div>
                    <div>
                        <div>
                            <button onClick={onClickUpdateCodeList}>코드 목록 수정</button>
                        </div>
                        <div>
                            {
                                showUpdateCodeList && <UpdateCodeList setUpdateCodeList={setShowUpdateCodeList} />
                            }
                        </div>
                    </div>
                </div>
                <div>
                    <div>
                        <div>
                            <button onClick={onClickCreateBoardButton}>새게시판</button>
                        </div>
                        <div>
                            {
                                showCreateBoard && <CreateBoard setShowCreateBoard={setShowCreateBoard} />
                            }
                        </div>
                    </div>
                    <div>
                        <div>
                            <button onClick={onClickUpdateBoardList}>게시판 목록 수정</button>
                        </div>
                        <div>
                            {
                                showUpdateBoardList && <UpdateBoardList setUpdateBoardList={setShowUpdateBoardList} />
                            }
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default Control;

 

설정 내 새게시판, 새코드, 게시판수정, 코드수정은 Link 를 이용하지 않고 Control 화면에서 true/false로 컴포넌트 렌더링 여부를 정해주는 방식으로 만들었다.

 

const [ showCreateCode, setShowCreateCode ] = useState(false);
const [ showCreateBoard, setShowCreateBoard ] = useState(false);
const [ showUpdateCodeList, setShowUpdateCodeList ] = useState(false);
const [ showUpdateBoardList, setShowUpdateBoardList ] = useState(false);

 

그래서 버튼을 누를 때마다 화면에서 입력폼이 show / hide 반복될 것이다.

 

그리고 등록/수정/삭제가 완료된 뒤 입력폼을 다시 안 보이게 할지를 saga에서 설정할 수도 있을 것 같아서

컴포넌트마다 props로 true/false 를 set해주는 메서드를 넘겨주었다.

 

showCreateCode && <CreateCode setShowCreateCode={setShowCreateCode} />
showUpdateCodeList && <UpdateCodeList setUpdateCodeList={setShowUpdateCodeList} />
showCreateBoard && <CreateBoard setShowCreateBoard={setShowCreateBoard} />
showUpdateBoardList && <UpdateBoardList setUpdateBoardList={setShowUpdateBoardList} />

 

 

 

 

2. (1) 게시판 신규 등록 * 위치는 /src/views/components 이다.

// src/views/components/CreateBoard.js

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { boardActions } from '../../slices/boardSlice';
import { codeActions } from '../../slices/codeSlice';

function CreateBoard({ setShowCreateBoard }) {
    const { codeList, codeStatus, codeStatusText } = useSelector(
        (state) => ({
            codeList: state.codeReducer.codeList,
            codeStatus: state.codeReducer.status,
            codeStatusText: state.codeReducer.statusText
    }));
    const [ board, setBoard ] = useState({});
    const dispatch = useDispatch();

    function onChangeArticle(e) {
        setBoard({
            ...board,
            [e.currentTarget.name]: e.currentTarget.value
        });
    }

    function onClickSubmitButton() {
        if (board?.name)
        {
            dispatch(boardActions.postBoard({ board, setShowCreateBoard }));
        } else {
            alert("게시판이름은 필수값입니다.");
        }
    }

    useEffect(() => {
        dispatch(codeActions.getCodeList());
    }, [dispatch]);

    return (
        <>
            {
                codeStatus === 200 ? 
                codeList.length > 0 ?
                    <div>
                        <div>
                            <span>게시판 명: </span>
                            <input name="name" onChange={onChangeArticle} />
                        </div>
                        <div>
                            <span>사용 코드: </span>
                            <select name="code" onChange={onChangeArticle} >
                                <option value="">선택</option>
                                {
                                    codeList.map((code) =>(
                                        <option value={code?.value}>{code?.desc ?? ""}</option>
                                    ))
                                }
                            </select>
                        </div>
                        <div>
                            <button onClick={onClickSubmitButton} >등록</button>
                        </div>
                    </div>
                :
                    <div>
                        코드등록이 필요합니다.
                    </div>
                : 
                <div>
                    <div>
                        <span>{codeStatus}</span>
                    </div>
                    <div>
                        <span>{codeStatusText}</span>
                    </div>
                </div>
                
            }
        </>
    );
}

export default CreateBoard;

 

코드를 보면

 

// CreateBoard.js

const { codeList, codeStatus, codeStatusText } = useSelector(
    (state) => ({
        codeList: state.codeReducer.codeList,
        codeStatus: state.codeReducer.status,
        codeStatusText: state.codeReducer.statusText
}));

 

나중에 board 등록/수정/삭제의 서버 return값을 봐야 할 때 boardReducer의 status와 statusText를 쓰게 되면 codeReducer의 statust와 statusText와 이름이 겹칠 수도 있을 것 같아서 useSelector 부분에서 아예 이름을 바꿔 state를 들고 왔다.

그래서 codeReducer의 status 와 statusText는 CreateBoard 컴포넌트에서 codeStatus, codeStatusText 변수에 담겨 사용된다.

 

 

// CreateBoard.js

useEffect(() => {
    dispatch(codeActions.getCodeList());
}, [dispatch]);

 

 

코드리스트를 목록에서 다시 부르는 이유는 설정부분에서 코드가 실시간으로 수정되거나 신규로 등록되거나 삭제될 수 있기 때문에 이를 반영하고자 함이다.

 

조회된 codeList는 위의 useSelector의 구독기능으로 인해 조회할 때마다 계속 업데이트될 것이다.

 

 

// CreateBoard.js

const [ board, setBoard ] = useState({});

function onChangeArticle(e) {
    setBoard({
        ...board,
        [e.currentTarget.name]: e.currentTarget.value
    });
}

<div>
    <span>게시판 명: </span>
    <input name="name" onChange={onChangeArticle} />
</div>
<div>
    <span>사용 코드: </span>
    <select name="code" onChange={onChangeArticle} >
        <option value="">선택</option>
        {
            codeList.map((code) =>(
                <option value={code?.value}>{code?.desc ?? ""}</option>
            ))
        }
    </select>
</div>

 

게시판 신규 등록 폼의 입력 부분은 useState와 onChange이벤트를 이용하였다.

setBoard로 change value를 반영+set 해주는데

객체형태로도 바로 set할 수 있어서 { ...board, [e.currentTarget.name]: e.currentTarget.value } 를 이용했다.

 

{
    codeStatus === 200 ? 
    codeList.length > 0 ?
        <div>
            <div>
                <span>게시판 명: </span>
                <input name="name" onChange={onChangeArticle} />
            </div>
            <div>
                <span>사용 코드: </span>
                <select name="code" onChange={onChangeArticle} >
                    <option value="">선택</option>
                    {
                        codeList.map((code) =>(
                            <option value={code?.value}>{code?.desc ?? ""}</option>
                        ))
                    }
                </select>
            </div>
            <div>
                <button onClick={onClickSubmitButton} >등록</button>
            </div>
        </div>
    :
        <div>
            코드등록이 필요합니다.
        </div>
    : 
    <div>
        <div>
            <span>{codeStatus}</span>
        </div>
        <div>
            <span>{codeStatusText}</span>
        </div>
    </div>
    
}

 

JSX 의 경우 아무 코드도 등록이 되지 않은 경우 게시판 신규 등록은 불가능하게 만들었다.

그리고 status가 200이 아예 아닐 경우 status와 statusText가 보이도록 해놓았다.

 

 

 

 

2. (2) 코드 신규 등록 * 위치는 /src/views/components 이다.

 

// src/views/components/CreateCode.js

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { codeActions } from '../../slices/codeSlice';

function CreateCode({ setShowCreateCode }) {
    const [ code, setCode ] = useState({});
    const dispatch = useDispatch();

    function onChangeCode(e) {
        setCode({
            ...code,
            [e.currentTarget.name]: e.currentTarget.value
        });
    }

    function onClickSubmitButton() {
        if (code.value !== "" && code.desc !== "")
        {
            dispatch(codeActions.postCode({ code, setShowCreateCode }));
        } else {
            alert("빠짐없이 입력해주세요.");
        }
    }

    return (
        <div>
            <div>
                <span>코드 설명: </span>
                <input name="desc" onChange={onChangeCode} value={code?.desc ?? ""} />
            </div>
            <div>
                <span>코드 설정값: </span>
                <input name="value" onChange={onChangeCode} value={code?.value ?? ""} />
            </div>
            <div>
                <button onClick={onClickSubmitButton}>등록</button>
            </div>
        </div>
    );
}

export default CreateCode;

 

코드 신규등록의 경우 가장 초기설정값을 등록하는 화면이므로 게시판 신규 등록 화면보다 간단하게 생겼다.

 

따로 조회해올 값도 없어 useEffect가 안 쓰였다.

 

useState로 코드 입력 값을 set해주는 방법은 CreateBoard와 거의 유사하므로 설명은 생략하겠다.

 

 

 

 

2. (3) 게시판 수정  * 위치는 /src/views/components 이다.

 

// src/views/components/UpdateBoardList.js

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { boardActions } from '../../slices/boardSlice';
import { codeActions } from '../../slices/codeSlice';

function UpdateBoardList({ setShowUpdateBoardList }) {
    const { boardList, boardStatus, boardStatusText } = useSelector(
        (state) => ({
            boardList: state.boardReducer.boardList,
            boardStatus: state.boardReducer.status,
            boardStatusText: state.boardReducer.statusText
    }));
    const { codeList, codeStatus, codeStatusText } = useSelector(
        (state) => ({
            codeList: state.codeReducer.codeList,
            codeStatus: state.codeReducer.status,
            codeStatusText: state.codeReducer.statusText
    }));
    const [ updatedBoardList, setUpdatedBoardList ] = useState(boardList ?? []);
    const dispatch = useDispatch();

    function onChangeBoard(e) {
        const copiedBoardList = [ ...updatedBoardList ];
        copiedBoardList[e.target?.dataset?.index] = {
            ...copiedBoardList[e.target?.dataset?.index],
            [e.target?.name]: e.target?.value
        };
        setUpdatedBoardList(copiedBoardList);
    }

    function onClickSubmitButton(updatedBoard) {
        if (!updatedBoard?.name || !updatedBoard.code
            || updatedBoard?.name === "" || updatedBoard.code === "") {
            alert("빠짐없이 입력해주세요.");
        } else {
            dispatch(boardActions.putBoard(updatedBoard));
        }
    }

    function onClickDeleteButton(boardId) {
        if (!window.confirm("삭제하시겠습니까?")) return false;
        dispatch(boardActions.deleteBoard(boardId));
    }

    useEffect(() => {
        dispatch(boardActions.getBoardList());
        dispatch(codeActions.getCodeList());
    }, [dispatch]);

    useEffect(() => {
        setUpdatedBoardList(boardList);
    }, [boardList]);
    
    return (
        <div>
            {
                boardStatus === 200 ?
                updatedBoardList.length > 0 ?
                    updatedBoardList.map((updatedBoard, index) => 
                        <>
                            <div>
                                <span>게시판 이름: </span>
                                <input
                                    name="name"
                                    value={updatedBoard?.name ?? ""}
                                    data-index={index}
                                    onChange={onChangeBoard}
                                />
                            </div>
                            <div>
                                <span>게시판 코드값: </span>
                                {
                                    codeStatus === 200 ?
                                    codeList.length > 0 ?
                                        <select
                                            name="code"
                                            value={updatedBoard?.code ?? ""}
                                            data-index={index}
                                            onChange={onChangeBoard}
                                        >
                                            <option value={""}>선택</option>
                                            {
                                                codeList.length > 0 &&
                                                codeList.map((code) => (
                                                    <option value={code?.value}>{code?.desc ?? ""}</option>
                                                ))
                                            }
                                        </select>
                                    :
                                        <div>
                                            코드등록이 필요합니다.
                                        </div>
                                    :
                                    <>
                                        <div>
                                            <div>
                                                <span>{codeStatus}</span>
                                            </div>
                                            <div>
                                                <span>{codeStatusText}</span>
                                            </div>
                                        </div>
                                    </>
                                }
                            </div>
                            <div>
                                <button onClick={() => onClickSubmitButton(updatedBoard)}>저장</button>
                            </div>
                            <div>
                                <button onClick={() => onClickDeleteButton(updatedBoard?.id ?? 0)}>삭제</button>
                            </div>
                        </>
                    )
                : 
                    <div>
                        수정할 게시판이 없습니다.
                    </div>
                :
                    <div>
                        <div>
                            <span>{boardStatus}</span>
                        </div>
                        <div>
                            <span>{boardStatusText}</span>
                        </div>
                    </div>
            }
        </div>
    );
}

export default UpdateBoardList;

 

 

게시판 수정 컴포넌트 흐름에 따라 코드를 보면

 

const { boardList, boardStatus, boardStatusText } = useSelector(
    (state) => ({
        boardList: state.boardReducer.boardList,
        boardStatus: state.boardReducer.status,
        boardStatusText: state.boardReducer.statusText
}));
const { codeList, codeStatus, codeStatusText } = useSelector(
    (state) => ({
        codeList: state.codeReducer.codeList,
        codeStatus: state.codeReducer.status,
        codeStatusText: state.codeReducer.statusText
}));
const [ updatedBoardList, setUpdatedBoardList ] = useState(boardList ?? []);

useEffect(() => {
    dispatch(boardActions.getBoardList());
    dispatch(codeActions.getCodeList());
}, [dispatch]);

 

우선 첫번째 useEffect 에서 getBoardList, getCodeList 액션을 호출하여 게시판 수정 전 가장 최신상태값을 들고 온다.

그리고 가져온 최신 게시판과 코드 리스트들은 useSelector의 구독기능으로 state에도 업데이트 된다.

그리고 boardList의 값이 바뀔 때마다 

 

useEffect(() => {
    setUpdatedBoardList(boardList);
}, [boardList]);

 

화면에서 쓸 state인 updatedBoardList 에 boardList를 셋해준다

 

원래 useEffect 한 개로 쓰려 했는데 조회가 멈추질 않아서 결국 두 개로 나눴다.

 

 

들어온 boardList 값을 map을 이용하여 화면에 뿌려주었고

들어온 codeList도 게시판 안의 코드값 설정 부분의 option에 map으로 뿌려주었다.

 

updatedBoardList.map((updatedBoard, index) => 
    <>
        <div>
            <span>게시판 이름: </span>
            <input
                name="name"
                value={updatedBoard?.name ?? ""}
                data-index={index}
                onChange={onChangeBoard}
            />
        </div>
        <div>
            <span>게시판 코드값: </span>
            {
                codeStatus === 200 ?
                codeList.length > 0 ?
                    <select
                        name="code"
                        value={updatedBoard?.code ?? ""}
                        data-index={index}
                        onChange={onChangeBoard}
                    >
                        <option value={""}>선택</option>
                        {
                            codeList.length > 0 &&
                            codeList.map((code) => (
                                <option value={code?.value}>{code?.desc ?? ""}</option>
                            ))
                        }
                    </select>
                :
                    <div>
                        코드등록이 필요합니다.
                    </div>
                :
                <>
                    <div>
                        <div>
                            <span>{codeStatus}</span>
                        </div>
                        <div>
                            <span>{codeStatusText}</span>
                        </div>
                    </div>
                </>
            }
        </div>
        <div>
            <button onClick={() => onClickSubmitButton(updatedBoard)}>저장</button>
        </div>
        <div>
            <button onClick={() => onClickDeleteButton(updatedBoard?.id ?? 0)}>삭제</button>
        </div>
    </>
)

 

 

 

setUpdatedBoardList 를 이용한 onChange 이벤트 함수를 만들었다.

 

 

function onChangeBoard(e) {
    const copiedBoardList = [ ...updatedBoardList ];
    copiedBoardList[e.target?.dataset?.index] = {
        ...copiedBoardList[e.target?.dataset?.index],
        [e.target?.name]: e.target?.value
    };
    setUpdatedBoardList(copiedBoardList);
}

 

 

리스트 마다 value가 다르게 들어가야 하므로 index 를 이용하였다.

 

 

왜 onChange 를 리듀서 액션으로 안 만들었냐고 하시면,, 그냥이다.

만약 만들려면 게시판 조회해올 때마다 보여주는 게시판리스트와 수정용 게시판리스트 로 값을 같이 넣어줘야 한다.

그러기엔 boardSlice에 더 손대기 싫었다.

같은 boardList쓰면 되지 않냐는 물음에는 sidebar도 같은 state를 이용하므로 입력할 때마다 왼쪽 사이드바에 있는 게시판도 같이 값이 바뀔 것이고 Board 컴포넌트의 렌더링도 계속 같이 일어날 것이다.

 

더 깔끔한 코딩방식을 알게 되면 그 때 수정하거나

 

아니면 v3을 들고오거나 그럴 수도..

 

 

 

 

2. (4) 코드 수정  * 위치는 /src/views/components 이다.

 

// src/views/components/UpdateCodeList.js

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { codeActions } from '../../slices/codeSlice';

function UpdateCodeList({ setShowUpdateCodeList }) {
    const { codeList, status, statusText } = useSelector((state) => state.codeReducer);
    const [ updatedCodeList, setUpdatedCodeList ] = useState(codeList ?? []);
    const dispatch = useDispatch();

    function onChangeCode(e) {
        const copiedCodeList = [ ...updatedCodeList ];
        copiedCodeList[e.target?.dataset?.index] = { ...copiedCodeList[e.target?.dataset?.index], [e.target?.name]: e.target?.value };
        setUpdatedCodeList(copiedCodeList);
    }

    function onClickSubmitButton(updatedCode) {
        if (!updatedCode?.value || !updatedCode.desc
            || updatedCode?.value === "" || updatedCode.desc === "") {
            alert("빠짐없이 입력해주세요.");
        } else {
            dispatch(codeActions.putCode(updatedCode));
        }
    }

    function onClickDeleteButton(codeId) {
        if (!window.confirm("삭제하시겠습니까?")) return false;
        dispatch(codeActions.deleteCode(codeId));
    }

    useEffect(() => {
        dispatch(codeActions.getCodeList());
    }, [dispatch]);

    useEffect(() => {
        setUpdatedCodeList(codeList);
    }, [codeList]);

    return (
        <div>
            {
                status === 200 ?
                updatedCodeList.length > 0 ?
                    updatedCodeList.map((updatedCode, index) => 
                        <>
                            <div>
                                <span>코드 설명: </span>
                                <input
                                    name="desc"
                                    value={updatedCode?.desc ?? ""}
                                    data-index={index}
                                    onChange={onChangeCode}
                                />
                            </div>
                            <div>
                                <span>게시판 코드값: </span>
                                <input
                                    name="value"
                                    value={updatedCode?.value ?? ""}
                                    data-index={index}
                                    onChange={onChangeCode}
                                />
                            </div>
                            <div>
                                <button onClick={() => onClickSubmitButton(updatedCode)}>저장</button>
                            </div>
                            <div>
                                <button onClick={() => onClickDeleteButton(updatedCode?.id ?? 0)}>삭제</button>
                            </div>
                        </>
                    )
                :
                    <div>
                        수정할 코드가 없습니다.
                    </div>
                :
                    <div>
                        <div>
                            <span>{status}</span>
                        </div>
                        <div>
                            <span>{statusText}</span>
                        </div>
                    </div>
            }
        </div>
    );
}

export default UpdateCodeList;

 

 

목록 조회부분과

 

useEffect(() => {
    dispatch(codeActions.getCodeList());
}, [dispatch]);

 

 

컴포넌트 state 값 설정부분

 

const { codeList, status, statusText } = useSelector((state) => state.codeReducer);
const [ updatedCodeList, setUpdatedCodeList ] = useState(codeList ?? []);

useEffect(() => {
    setUpdatedCodeList(codeList);
}, [codeList]);

 

 

그리고 onChange 부분은 게시판과 거의 유사하므로 설명은 생략하겠다.

 

function onChangeCode(e) {
    const copiedCodeList = [ ...updatedCodeList ];
    copiedCodeList[e.target?.dataset?.index] = {
        ...copiedCodeList[e.target?.dataset?.index],
        [e.target?.name]: e.target?.value
    };
    setUpdatedCodeList(copiedCodeList);
}

 

 

 

 

 

3. saga

 

// boardSaga.js

import { all, call, fork, getContext, put, take } from 'redux-saga/effects';
import { boardActions } from '../slices/boardSlice';
import axios from '../utils/axios';

// api 서버 연결 주소
function apiGetBoard(boardId) {
    return axios.get(`boards/${boardId}`);
}

function apiGetBoardList() {
    return axios.get(`boards`);
}

function apiPostBoard(requestBody) {
    return axios.post(`boards`, requestBody);
}

function apiPutBoard(requestBody) {
    return axios.put(`boards/${requestBody?.id}`, requestBody);
}

function apiDeleteBoard(boardId) {
    return axios.delete(`boards/${boardId}`);
}

// api 서버 연결 후 action 호출
function* asyncGetBoardList() {
    try {
        const response = yield call(apiGetBoardList);
        if (response?.status === 200) {
            yield put(boardActions.getBoardListSuccess(response));
        } else {
            
            yield put(boardActions.getBoardListFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.getBoardListFail(e.response));
    }
}

function* asyncGetBoard(action) {
    try {
        const response = yield call(apiGetBoard, action.payload);
        if (response?.status === 200) {
            yield put(boardActions.getBoardSuccess());
        } else {
            yield put(boardActions.getBoardFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.getBoardFail(e.response));
    }
}

function* asyncPostBoard(action) { // 추가
    try {
        const response = yield call(apiPostBoard, {
            ...action.payload.board,
            id: 0,
            insertDate: Date.now(),
            updateDate: Date.now()
        });
        if (response?.status === 201) {
            yield put(boardActions.postBoardSuccess());
            alert("등록되었습니다!");
            yield call(action.payload?.setShowCreateBoard, false);
            yield put(boardActions.getBoardList());
        } else {
            yield put(boardActions.postBoardFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.postBoardFail(e.response));
        yield alert(`등록 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

function* asyncPutBoard(action) { // 추가
    try {
        const response = yield call(apiPutBoard, { ...action.payload, updateDate: Date.now() });
        if (response?.status === 200) {
            yield put(boardActions.putBoardSuccess());
            alert("저장되었습니다!");
            yield put(boardActions.getBoardList());
        } else {
            yield put(boardActions.putBoardFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.putBoardFail(e.response));
        yield alert(`저장 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

function* asyncDeleteBoard(action) { // 추가
    try {
        const response = yield call(apiDeleteBoard, action.payload);
        if (response?.status === 200) {
            yield put(boardActions.deleteBoardSuccess());
            alert("삭제되었습니다!");
            yield put(boardActions.getBoardList());
        } else {
            yield put(boardActions.deleteBoardFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.deleteBoardFail(e.response));
        yield alert(`삭제 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}


// action 호출을 감시하는 watch 함수
function* watchGetBoardList() {
    while(true) {
        yield take(boardActions.getBoardList);
        yield call(asyncGetBoardList);
    }
}

function* watchGetBoard() {
    while(true) {
        const action = yield take(boardActions.getBoard);
        yield call(asyncGetBoard, action);
    }
}

function* watchPostBoard() { // 추가
    while(true) {
        const action = yield take(boardActions.postBoard);
        yield call(asyncPostBoard, action);
    }
}

function* watchPutBoard() { // 추가
    while (true) {
        const action = yield take(boardActions.putBoard);
        yield call(asyncPutBoard, action);
    }
}

function* watchDeleteBoard() { // 추가
    while (true) {
        const action = yield take(boardActions.deleteBoard);
        yield call(asyncDeleteBoard, action);
    }
}

export default function* boardSaga()
{
    yield all([fork(watchGetBoardList), fork(watchGetBoard), fork(watchPostBoard), fork(watchPutBoard), fork(watchDeleteBoard)]);
}

 

 

게시판 신규 등록의 경우 CreateBoard.js 에서 seShowCreateBoard (컴포넌트 보여줄지 말지 결정하는 set메서드)를 action.payload로 넘겨준 것을 호출하였다.

 

function* asyncPostBoard(action) {
    try {
        const response = yield call(apiPostBoard, {
            ...action.payload.board,
            id: 0,
            insertDate: Date.now(),
            updateDate: Date.now()
        });
        if (response?.status === 201) {
            yield put(boardActions.postBoardSuccess());
            alert("등록되었습니다!");
            yield call(action.payload?.setShowCreateBoard, false);
            yield put(boardActions.getBoardList());
        } else {
            yield put(boardActions.postBoardFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(boardActions.postBoardFail(e.response));
        yield alert(`등록 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

 

그래서 등록 성공시 입력폼 접히는 효과가 난다. put과 delete 부분에는 안 넣었다. 그래서 저장이나 삭제가 완료되면 게시판 목록을 다시 조회해줄 뿐 입력폼은 그대로 펼쳐져 있다.

게시판 수정 입력폼을 열어놓은 채로 게시글을 등록하면 새로운 입력폼이 밑에 생기는 것이 보일 것이다.

UpdateBoardList 컴포넌트에서 boardList 가 업데이트 될 때마다 setUpdatedBoardList가 동작하므로 컴포넌트도 바로 렌더링 된다.

 

 

 

 

// codeSaga.js

import { all, call, fork, put, take } from 'redux-saga/effects';
import { codeActions } from '../slices/codeSlice';
import axios from '../utils/axios';

// api 서버 연결 주소
function apiGetCode(codeId) {
    return axios.get(`codes/${codeId}`);
}

function apiGetCodeList() {
    return axios.get(`codes`);
}

function apiPostCode(requestBody) {
    return axios.post(`codes`, requestBody);
}

function apiPutCode(requestBody) {
    return axios.put(`codes/${requestBody?.id}`, requestBody);
}

function apiDeleteCode(codeId) {
    return axios.delete(`codes/${codeId}`);
}

// api 서버 연결 후 action 호출
function* asyncGetCodeList() {
    try {
        const response = yield call(apiGetCodeList);
        if (response?.status === 200) {
            yield put(codeActions.getCodeListSuccess(response));
        } else {
            yield put(codeActions.getCodeListFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.getCodeListFail(e.response));
    }
}

function* asyncGetCode(action) {
    try {
        const response = yield call(apiGetCode, action.payload);
        if (response?.status === 200) {
            yield put(codeActions.getCodeSuccess());
        } else {
            yield put(codeActions.getCodeFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.getCodeFail(e.response));
    }
}

function* asyncPostCode(action) {
    try {
        const response = yield call(apiPostCode, {
            ...action.payload.code,
            id: 0,
            insertDate: Date.now(),
            updateDate: Date.now()
        });
        if (response?.status === 201) {
            yield put(codeActions.postCodeSuccess());
            alert("등록되었습니다!");
            yield call(action.payload?.setShowCreateCode, false);
            yield put(codeActions.getCodeList());
        } else {
            yield put(codeActions.postCodeFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.postCodeFail(e.response));
        yield alert(`등록 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

function* asyncPutCode(action) {
    try {
        const response = yield call(apiPutCode, { ...action.payload, updateDate: Date.now() });
        if (response?.status === 200) {
            yield put(codeActions.putCodeSuccess());
            alert("저장되었습니다!");
            yield put(codeActions.getCodeList());
        } else {
            yield put(codeActions.putCodeFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.putCodeFail(e.response));
        yield alert(`저장 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

function* asyncDeleteCode(action) {
    try {
        const response = yield call(apiDeleteCode, action.payload);
        if (response?.status === 200) {
            yield put(codeActions.deleteCodeSuccess());
            alert("삭제되었습니다!");
            yield put(codeActions.getCodeList());
        } else {
            yield put(codeActions.deleteCodeFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.deleteCodeFail(e.response));
        yield alert(`삭제 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

// action 호출을 감시하는 watch 함수
function* watchGetCodeList() {
    while(true) {
        yield take(codeActions.getCodeList);
        yield call(asyncGetCodeList);
    }
}

function* watchGetCode() {
    while(true) {
        const action = yield take(codeActions.getCode);
        yield call(asyncGetCode, action);
    }
}

function* watchPostCode() {
    while(true) {
        const action = yield take(codeActions.postCode);
        yield call(asyncPostCode, action);
    }
}

function* watchPutCode() {
    while (true) {
        const action = yield take(codeActions.putCode);
        yield call(asyncPutCode, action);
    }
}

function* watchDeleteCode() {
    while (true) {
        const action = yield take(codeActions.deleteCode);
        yield call(asyncDeleteCode, action);
    }
}

export default function* codeSaga()
{
    yield all([fork(watchGetCodeList), fork(watchGetCode), fork(watchPostCode), fork(watchPutCode), fork(watchDeleteCode)]);
}

 

 

codeSaga 는 boardSaga와 거의 똑같다. 이름만 바뀐 것이라고 생각될 정도이다.

그래서 설명도 복붙하여 명사만 바꾸겠다.

 

코드신규 등록 도 CreateCode.js 에서 seShowCreateCode (컴포넌트 보여줄지 말지 결정하는 set메서드)를 action.payload로 넘겨준 것을 호출하였다.

 

function* asyncPostCode(action) {
    try {
        const response = yield call(apiPostCode, {
            ...action.payload.code,
            id: 0,
            insertDate: Date.now(),
            updateDate: Date.now()
        });
        if (response?.status === 201) {
            yield put(codeActions.postCodeSuccess());
            alert("등록되었습니다!");
            yield call(action.payload?.setShowCreateCode, false);
            yield put(codeActions.getCodeList());
        } else {
            yield put(codeActions.postCodeFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(codeActions.postCodeFail(e.response));
        yield alert(`등록 실패 Error: ${e?.response?.status}, ${e?.response?.statusText}`);
    }
}

 

그래서 등록 성공시 입력폼 접히는 효과가 난다. put과 delete 부분에는 안 넣었다. 그래서 저장이나 삭제가 완료되면 게시판 목록을 다시 조회해줄 뿐 입력폼은 그대로 펼쳐져 있다.

코드 수정 입력폼을 열어놓은 채로 코드를 등록하면 새로운 입력폼이 밑에 생기는 것이 보일 것이다.

UpdateCodeList 컴포넌트에서 codeList 가 업데이트 될 때마다 setUpdatedCodeList가 동작하므로 컴포넌트도 바로 렌더링 된다.

 

 

 

설정 화면의 컴포넌트를 다 펼쳐(true) 놓으면 이렇게 된다.

 

 

 

 

 

목차 돌아가기:

binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

반응형