[프로젝트] 게시판 front 리펙토링 (6) 게시글 삭제, 추가 요청하기 + 로그아웃

rin·2020년 6월 9일
1

React

목록 보기
14/16
post-thumbnail
post-custom-banner

목표
1. 게시글 삭제 후 새로운 데이터로 갱신한다.
2. 게시글 추가 후 새로운 데이터로 갱신한다.
3. 로그아웃을 추가한다.

게시글 삭제 요청

게시글 삭제 시 현재 노출되고 있는 데이터 목록이 갱신되어야한다. (한 페이지의 목록만 가지고 있기때문에 수정 요청과 다르게 추가적인 데이터를 가져올 수 밖에 없다.)

따라서 "게시글 삭제 요청 → 성공 시 changePage 호출 → 재랜더링"의 과정을 거칠 것이다.

🔎 boardApi

export function deleteOne(id) {
    return axios.delete(DOMAIN+'/api/boards/'+id);
}

🔎 BoardContainer
ContentsModal로 handleDelete이라는 함수를 전달할 것이다. 비동기 요청이 성공하면 changePage 액션 생성 객체를 호출한다.

모달을 클릭했기 때문에 modalData가 생긴된 상태이므로 받아올 인자는 없다. 바로 modalData state에 접근해 id를 가져올 것이다.

    const handleDelete = () => {
        deleteOne(modalData.id)
            .then(response => {
                changePage(pageNumber, pageSize);
                closeModal();
            });
    }
    
    
    return (
        <div>
            <Board
                pageNumber={pageNumber}
                pageSize={pageSize}
                selectedData={selectedData}
                handleChangePageNumber={handleChangePageNumber}
                handleChangePageSize={handleChangePageSize}
                handleRowClick={clickRow}
                columns={columns}
                data={selectedData}
                accountId={accountId}
            />
            { isModalOpen ?
            <ContentsModal
                userLoggedIn={accountId}
                isModalOpen={isModalOpen}
                modalData={modalData}
                handleClose={closeModal}
                handleDelete={handleDelete} // 추가된 부분
                handleModify={handleModify}
            />
            : null}
        </div>
    );

🔎 ContentsModal
handleDelete를 인수로 받아 버튼을 클릭하면 호출되도록한다.

read 객체의 수정 버튼이 있던 부분을 다음처럼 변경한다.

{typeof userLoggedIn != 'undefined' && userLoggedIn == modalData.writer.accountId ?
   <buttons>
        <Button color="primary" onClick={() => setIsModify(true)}>Modify</Button>
        <Button color="secondary" onClick={() => handleDelete()}>Delete</Button>
   </buttons>
: null}

하나의 태그로 묶어줘야하기 때문에 임의로 <buttons>라는 태그를 사용했다. (딱히 의미는 없다.)

수정 버튼 옆에 삭제 버튼이 생긴 것을 확인할 수 있다. 데이터가 잘 삭제된 뒤 게시판 리스트가 갱신되는지 확인하자.

게시글 추가 요청

ContentsModal 분리

이전에 게시글을 수정할 때, ContentsModal내에서 모달 내부에 보여질 값인 read와 modify로 출력할 html 코드를 각각 오브젝트화하였다. ContentsModal의 고유 상태값인 isModify를 이용해 둘 중 어떤 값을 출력할지 결정했는데, 게시글 추가에도 이와 같은 방식을 사용하다가 문제가 생겼다. 🤔

한 행을 클릭함과 동시에 modalData를 초기화하고 ContentsModal 컴포넌트를 랜더링하기 때문에 아래와 같은 구문에 문제가 없었다. 하지만 글작성 버튼을 클릭하는 경우에는 modal에 나타날 데이터가 "로그인된 사용자 아이디" 뿐이기 때문에 modalData는 갱신되지 않고, 초기값인 빈 객체 {} 상태이다. 따라서 modalData.writer.accountId를 하면 modalData.writer가 "null"이기 때문에 npe이 발생한다.

뿐만 아니라 거의 동일한 구조가 write, read, modify에서 반복되고 있기때문에 컴포넌트화 시켜 상황에 맞게 필요한 데이터를 받는 것으로 변경하도록 하겠다.

ContentsModal에 새롭게 들어올 상태값 중에 isWriteModal이라는 boolean값이 있다. 글쓰기 버튼을 클릭했을 때, 이 값은 true가 되고 글쓰기 모달이 닫힐 때 false가 된다.
또한 이전 코드에서 handleModify라는 이름으로 들어오던 함수를 새로운 글을 작성할 때와, 수정할 때 모두 쓰일 수 있도록 handleSave라는 이름으로 바꿔주었다.

// 수정 전
export default function ContentsModal({isModalOpen, modalData, userLoggedIn, handleClose, handleModify, handleDelete}) {
  ..
}

// 수정 후
export default function ContentsModal({isModalOpen, modalData, userLoggedIn, isWriteModal, handleClose, handleSave, handleDelete}) {
  ..
}

isWriteModal : 글쓰기 버튼을 눌렀는지 판단
isModify : 게시글을 클릭한 뒤 수정 버튼을 눌렀는지 판단

위의 두가지 상태값에 따라서 다른 컴포넌트를 만들어 낼 수 있도록 코드를 작성하였다.
modalData.writer.accountId가 npe이 되지 않는 것이 중요하기 때문에, 그럴 가능성이 있는 부분은 내부 컴포넌트로 분리하여 modalData.writer가 null이 아닐때만 렌더링 할 수 있도록 하였다.

function ModalButtons({writer, userLoggedIn, setIsModify, handleDelete}) {
    return (
        <span>
            {writer.accountId == userLoggedIn ?
                <Grid>
                    <Button color="primary" onClick={() => setIsModify(true)}>Modify</Button>
                    <Button color="secondary" onClick={() => handleDelete()}>Delete</Button>
                </Grid>
                : null
            }
        </span>
    );
}

function ModalContentsWriter({writer}) {
    if (typeof writer == 'object') {
        writer = writer.accountId
    }
    return (
        <Typography variant="body2" gutterBottom>작성자 : {writer}</Typography>
    );
}

위 두 가지가 분리한 컴포넌트이며 컴포넌트 파일로 분리하지 않고 ContentsModal 내부에 만들어주었다. 두 컴포넌트의 내부 로직을 보면 writer.accountId가 사용되고 있지만 그 값이 존재하는 경우에만 해당 로직을 타기때문에 문제가 되지않는다.

컴포넌트를 분리하고 중복되는 부분은 합치는 과정을 일일히 나열할 순 없지만 다음과 같은 순서로 진행하였다.

  1. modify, delete, write로 중복을 포함하여 태그를 분해한다.
  2. isWriteModalisModify 을 사용해 분기처리한다.
  3. modalData.writer.accountId를 포함하는 부분은 분리하여 컴포넌트로 만든다.
  4. 중복되는 분기문이 있으면 합쳐준다. (중복을 최소화한다.)

결과적으론 아래와 같은 코드가 된다.

// .. 생략

export default function ContentsModal({isModalOpen, modalData, userLoggedIn, isWriteModal, handleClose, handleSave, handleDelete}) {
    const classes = useStyles();
    // getModalStyle is not a pure function, we roll the style only on the first render
    const [modalStyle] = React.useState(getModalStyle);

    const [isModify, setIsModify] = useState(false);
    const [title, setTitle] = useState(modalData.title);
    const [contents, setContents] = useState(modalData.contents);

    const combineData = () => {
        return {
            title: title,
            contents: contents
        }
    }

    return (
        <div>
            <Modal
                open={isModalOpen}
                onClose={handleClose}
                aria-labelledby="simple-modal-title"
                aria-describedby="simple-modal-description"
            >
                <div style={modalStyle} className={classes.paper}>
                    <form className={classes.form} noValidate onSubmit={(event) => {
                        event.preventDefault();
                        handleSave(combineData());
                    }}>
                        {(isWriteModal || isModify) ?
                            <div>
                                <TextField
                                    id="outlined-full-width"
                                    label="제목"
                                    placeholder="제목을 입력하세요."
                                    fullWidth
                                    InputLabelProps={{
                                        shrink: true,
                                    }}
                                    variant="outlined"
                                    defaultValue={modalData.title}
                                    margin="normal"
                                    onChange={event => setTitle(event.target.value)}
                                    required
                                />
                                <TextField
                                    id="outlined-multiline-static"
                                    label="내용"
                                    placeholder="내용을 입력하세요."
                                    fullWidth
                                    multiline
                                    rows={4}
                                    defaultValue={modalData.contents}
                                    variant="outlined"
                                    margin="normal"
                                    onChange={event => setContents(event.target.value)}
                                    required
                                />
                            </div>
                            :
                            <div>
                                <h2 id="simple-modal-title">{modalData.title}</h2>
                                <Typography variant="body1" gutterBottom>{modalData.contents}</Typography>
                            </div>
                        }
                        <hr/>
                        {isWriteModal ?
                            <div>
                                <ModalContentsWriter writer={userLoggedIn}/>
                                <Grid container>
                                    <Button type="submit" color="primary" className={classes.submit}>Save</Button>
                                    <Button onClick={() => {
                                        setIsModify(false);
                                        handleClose();
                                    }}>Close</Button>
                                </Grid>
                            </div>
                            :
                            <div>
                                <ModalContentsWriter writer={modalData.writer}/>
                                <Typography variant="body2" gutterBottom>작성일 : {modalData.createdAt}</Typography>
                                <Grid container>
                                    {isModify ?
                                        <Grid>
                                            <Button type="submit" color="primary"
                                                    className={classes.submit}>Save</Button>
                                            <Button onClick={() => setIsModify(false)}>Back</Button>
                                        </Grid>
                                        :
                                        <ModalButtons
                                            setIsModify={setIsModify}
                                            handleDelete={handleDelete}
                                            writer={modalData.writer}
                                            userLoggedIn={userLoggedIn}
                                        />
                                    }
                                    <Button onClick={() => {
                                        setIsModify(false);
                                        handleClose();
                                    }}>Close</Button>
                                </Grid>
                            </div>
                        }
                    </form>
                </div>
            </Modal>
        </div>
    );
}


function ModalButtons({writer, userLoggedIn, setIsModify, handleDelete}) {
    return (
        <span>
            {writer.accountId == userLoggedIn ?
                <Grid>
                    <Button color="primary" onClick={() => setIsModify(true)}>Modify</Button>
                    <Button color="secondary" onClick={() => handleDelete()}>Delete</Button>
                </Grid>
                : null
            }
        </span>
    );
}

function ModalContentsWriter({writer}) {
    if (typeof writer == 'object') {
        writer = writer.accountId
    }
    return (
        <Typography variant="body2" gutterBottom>작성자 : {writer}</Typography>
    );
}

이제 인수로 들어와서 사용될 isWriteModalhandleSave를 정의해보자.

Store

store/modules/board 하위의 파일들을 수정한다.

🔎 type.js

CLICK_WRITE_BUTTON: 'BOARD/CLICK_WRITE_BUTTON'

글쓰기 버튼을 클릭할 시에 생성할 액션

🔎 reducer.js

const initialState = {
    pageNumber: 0,
    pageSize: BOARD_PAGE_SIZE,
    selectedData: [],
    isModalOpen: false,
    modalData: {},
    isWriteModal: false, // 새로 추가된 상태
}

export default handleActions({
  
        // ( ... 생략 )
  
        [type.CLICK_WRITE_BUTTON]: (state, action) => ({
            ...state,
            isModalOpen: true,
            isWriteModal: true,
        })
    }, initialState
)

추가한 액션에 맞춰 리듀서도 변경해준다.

type.CLOSE_MODAL 액션이 호출될 때의 반환값에도 isWriteModal에 대한 갱신을 추가한다.

수정 전수정 후

🔎 action.js

export const clickWriteButton = () => ({
    type: type.CLICK_WRITE_BUTTON,
})

BoardContoller

글쓰기 버튼은 게시판 툴바를 오버라이딩하여 만들 것이다. 따라서 버튼을 클릭하는 액션을 만드는 생성 함수는 BoardController의 글쓰기 버튼이 사용한다.

import MaterialTable, {MTableToolbar} from 'material-table';

툴바를 오버라이딩 하기위해서 components props를 사용할 것인데, 상단의 title과 search는 유지하기 위해서 MtableToolbar를 추가한다.

로그인 여부에 따라서 다른 버튼을 보여준다.

<Grid>
   <Button color="primary" onClick={()=>handleWriteButtonClick()}>글쓰기</Button>
   <Button>로그아웃</Button>
</Grid>

로그인 한 경우에 위와같이 글쓰기 버튼이 나타나고, onClick에 실행하는 함수가 액션 생성 함수인 clickWriteButton이다.

전체 코드는 아래와 같다.

// ( .. 생략 )

export default function Board({pageNumber, pageSize, selectedData, columns, data, accountId, handleChangePageNumber, handleChangePageSize, handleRowClick, handleWriteButtonClick}) {
    const history = useHistory();
    
    return (
        <MaterialTable
            onChangePage={handleChangePageNumber}
            onChangeRowsPerPage={handleChangePageSize}
            icons={tableIcons}
            title={"게시판" + (typeof accountId != 'undefined' && accountId != null ? " (login user : " + accountId + ")" : "")}
            columns={columns}
            data={selectedData}
            options={{
                paginationType: "stepped",
                pageSize: BOARD_PAGE_SIZE
            }}
            onRowClick={(event, rowData) => {
                handleRowClick(rowData);
            }}
            components={{
                Toolbar: props => (
                    <div>
                        <MTableToolbar {...props} />
                        <Grid container  alignItems="flex-start" justify="flex-end" direction="row">
                            {typeof accountId != 'undefined' && accountId != null ?
                                <Grid>
                                    <Button color="primary" onClick={()=>handleWriteButtonClick()}>글쓰기</Button>
                                    <Button>로그아웃</Button>
                                </Grid>
                                :
                                <Grid>
                                    <Button color="secondary" onClick={()=>history.push("/")}>로그인하기</Button>
                                </Grid>
                            }
                        </Grid>
                    </div>
                )
            }}
        />
    );
}

BoardContainermapStateToPropsmapDispatchToProps에도 새로만든 상태와 액션 생성 함수를 추가하고 Board 컴포넌트와 ContentsModal 컴포넌트에 인자로 전달해주자.

handleSave에 전달되는 handleWrite 함수는 아직 내용없는 함수이다.
이는 ContentsModal의 폼에서 데이터 수정을 요청하거나 새로운 글 작성을 요청할 경우 동일한 폼에서 사용할 수 있도록 하기 위함이다.

잘 되는지 실행해보자 🙋🏻

게시글 클릭수정하기글쓰기

저장버튼 활성화

🔎 boardApi

export function post(writeData) {
    return axios.post(DOMAIN+'/api/boards', writeData);
}

서버로 데이터 추가 요청을 보낼 함수를 작성한다.

🔎 BoardContainer
빈 함수였던 handleWrite를 다음과 같이 바꿔준다.

const handleWrite = (writeData) => {
       post(writeData)
          .then(response => {
             changePage(0, pageSize);
             closeModal();
       })
}

현재 페이지에 상관없이 글쓰기가 가능하고, 글 작성에 성공했는데 첫페이지로 돌아가게 만들기 위해 changePage의 인수 pageNumber는 1로 해주었다.

글쓰기 테스트를 진행해보자.

글쓰기저장된 모습

로그아웃 요청

로그아웃 버튼을 추가한 김에, 실제로 작동하는 코드도 만들어보자.

🔎 userApi

export async function logoutApi() {
    try {
        await axios.get(DOMAIN+'/logout');
    } catch (error) {
        /* error control */
    }
}

로그아웃은 서버에 Controller로 구현되어있고 리턴값이 서버사이드 렌더링을 위한 index 페이지므로 반환값을 돌려주지 않는다.

🔎 Board

import {logoutApi} from "../store/api/userApi"; // api 요청 함수 추가

<Grid>
      <Button color="primary" onClick={()=>handleWriteButtonClick()}>글쓰기</Button>
      <Button onClick={async () => {
          await logoutApi();
          history.push("/")
      }}>로그아웃</Button>
</Grid>

로그아웃 버튼을 클릭하면 api 요청을 전송하고 로그인 페이지로 이동하도록 한다.

전체 코드는 github에서 확인 할 수 있습니다.

profile
🌱 😈💻 🌱
post-custom-banner

0개의 댓글