3-6. 메모 수정하기 및 삭제하기

이제, 화면에 보여진 메모를 클릭했을 때, 메모의 전체 내용을 보여주고, 또 수정하거나 삭제 할 수 있는 MemoViewer 컴포넌트를 을 구현해보도록 하겠습니다.

MemoViewer 컴포넌트 만들기

이번엔 우리가 메모를 작성할때 만들었던 InputSet 과 SaveButton 을 재사용 할 수 있기때문에 새로 작성할 코드가 그리 많지 않습니다. 이 컴포넌트에서는, 열려있는 메모의 내용, 그리고 4가지 함수:

  • onChange (인풋값 수정)
  • onUpdate (메모 내용 업데이트)
  • onDelete (메모 제거)
  • onClose (뷰어 닫기)

를 전달받습니다.

이 컴포넌트에서, Dimmed 컴포넌트는 뷰어 뒤 화면을 불투명하게 해주는데, 이를 클릭 시 뷰어가 닫힙니다.

삭제 버튼의 경우엔, 아이콘은 react-icons 를 사용합니다.

src/components/MemoViewer.js

import React from 'react';
import { InputSet, SaveButton } from 'components/Shared';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
import { media } from 'lib/style-utils';

import TrashIcon from 'react-icons/lib/io/trash-b';


// 화면을 불투명하게 해줍니다.
const Dimmed = styled.div`
    background: ${oc.gray[3]};
    top: 0px;
    left: 0px;
    bottom: 0px;
    right: 0px;
    position: fixed;
    z-index: 10;
    opacity: 0.5;
`;

const Viewer = styled.div`
    background: white;
    position: fixed;
    height: auto;
    z-index: 15;

    padding: 1rem;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);

    ${media.tablet`
        width: calc(100% - 2rem);
    `}
`;

const TrashButton = styled.div`
    position: absolute;
    bottom: 1rem;
    left: 1rem;
    color: ${oc.gray[6]};
    cursor: pointer;

    &:hover {
        color: ${oc.gray[7]};
    }

    &:active {
        color: ${oc.gray[8]};
    }

    font-size: 1.5rem;
`;

const MemoViewer = ({visible, title, body, onChange, onUpdate, onDelete, onClose}) => {

    // visible 이 아닐경우엔 아무것도 보여주지 않는다
    if(!visible) return null;


    return (
        <div>
            <Dimmed onClick={onClose}/>
            <Viewer>
                <InputSet title={title} body={body} onChange={onChange}/>
                <SaveButton onClick={onUpdate}/>
                <TrashButton onClick={onDelete}>
                    <TrashIcon/>
                </TrashButton>
            </Viewer>
        </div>
    );
};

MemoViewer.propTypes = {
    visible: PropTypes.bool,
    title: PropTypes.string,
    body: PropTypes.string,
    onChange: PropTypes.func,
    onUpdate: PropTypes.func,
    onDelete: PropTypes.func
}

export default MemoViewer;

UI 액션 준비하기

MemoViewer 를 구현함에 있어서 필요한 액션들을 준비해줍시다. ui 모듈과 memo 모듈 둘 다 조금씩 코드를 더 작성해야합니다.

먼저 다룰 것은 뷰어를 열고, 닫는 액션입니다. OPEN_VIEWER 액션은 payload 를 memo 데이터로 받습니다. 이 액션이 실행되었을땐 memo.open 값을 바꾸고, memo.info 값을 클릭한 메모의 데이터로 설정합니다.

CLOSE_VIEWER 의 경우엔 메모를 닫는 기능만 하기 때문에 따로 받는 payload는 없습니다.

src/modules/ui.js

// (...)

const OPEN_VIEWER = 'OPEN_VIEWER';
const CLOSE_VIEWER = 'CLOSE_VIEWER';
const CHANGE_VIEWER_INPUT = 'CHANGE_VIEWER_INPUT';

// (...)

export const openViewer = createAction(OPEN_VIEWER); // memo
export const closeViewer = createAction(CLOSE_VIEWER); 
export const changeViewerInput = createAction(CHANGE_VIEWER_INPUT); // { name, value }

const initialState = Map({
    write: Map({
        focused: false,
        title: '',
        body: ''
    }),
    memo: Map({
        open: false,
        info: Map({
            id: null,
            title: null,
            body: null
        })
    })
});

export default handleActions({
    // (...)
    [OPEN_VIEWER]: (state, action) => state.setIn(['memo', 'open'], true)
                                           .setIn(['memo', 'info'], action.payload),
    [CLOSE_VIEWER]: (state, action) => state.setIn(['memo', 'open'], false),
    [CHANGE_VIEWER_INPUT]: (state, action) => {
        const { name, value } = action.payload;
        return state.setIn(['memo', 'info', name], value)
    }
}, initialState);

MemoViewer 띄우고, 닫기.

이제 UI 쪽 코드는 어느정도 준비가 되었으니 실제로 이 뷰어를 띄우고 닫아보겠습니다.

컨테이너 만들기

먼저 MemoViewerContainer 컴포넌트를 만드세요.

memoAction 의 경우 아직 당장 필요하지는 않지만 추후 필요해지니, 미리 바인딩 하였습니다.

src/containers/MemoViewerContainer.js

import React, { Component } from 'react';
import MemoViewer from 'components/MemoViewer';

import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import * as uiActions from 'modules/ui';
import * as memoActions from 'modules/memo';


class MemoViewerContainer extends Component {

    handleChange = (e) => {
        const { UIActions } = this.props;
        const { name, value } = e.target;

        UIActions.changeViewerInput({
            name, value
        });
    }

    render() {
        const { visible, memo, UIActions } = this.props;
        const { title, body } = memo.toJS();
        const { handleChange } =this;

        return (
            <MemoViewer
                visible={visible}
                title={title}
                body={body}
                onChange={handleChange}
                onClose={UIActions.closeViewer}
            />
        );
    }
}

export default connect(
    (state) => ({
        visible: state.ui.getIn(['memo', 'open']),
        memo: state.ui.getIn(['memo', 'info'])
    }),
    (dispatch) => ({
        UIActions: bindActionCreators(uiActions, dispatch),
        MemoActions: bindActionCreators(memoActions, dispatch)
    })
)(MemoViewerContainer);

그 다음엔 이 컴포넌트를 App 에서 렌더링하세요.

src/containers/App.js

// (...)
import MemoViewerContainer from './MemoViewerContainer';


class App extends Component {

    // (...)

    render() {
        return (
            <Layout>
                <Header/>
                <Layout.Main>
                    <WriteMemo/>
                    <MemoListContainer/>
                </Layout.Main>
                <MemoViewerContainer/>
            </Layout>
        );
    }
}

// (...)

onOpen 함수 지정하기

이전에, MemoList 컴포넌트를 만들 때 onOpen props 가 있던것, 기억나시나요? 이제 이 함수를 지정해주도록 하겠습니다.

먼저 MemoListContainer 에서 다음과 같이 UIActions 를 바인딩하고, UIActions.openViewer 를 onOpen 으로 전달하세요.

src/containers/MemoListContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import MemoList from 'components/MemoList';
import { bindActionCreators } from 'redux';
import * as uiActions from 'modules/ui';

class MemoListContainer extends Component {
    render() {
        const { memos, UIActions } = this.props;

        return (
            <MemoList
                memos={memos}
                onOpen={UIActions.openViewer}
            />
        );
    }
}

export default connect(
    (state) => ({
        memos: state.memo.get('data')
    }),
    (dispatch) => ({
        UIActions: bindActionCreators(uiActions, dispatch)
    })
)(MemoListContainer);

그 다음엔 Memo 컴포넌트에서 클릭이 되었을 때 onOpen 함수에 현재 memo 데이터를 인자로 전달하여 실행하도록 코드를 작성하세요.

src/components/MemoList/Memo.js

// (...)

class Memo extends Component {
    // (...)

    handleClick = () => {
        const { memo, onOpen } = this.props;
        onOpen(memo);
    }

    render() {
        const { title, body } = this.props.memo.toJS();
        const { handleClick } = this;

        return (
            <Sizer>
                <Square onClick={handleClick}>
                    <Contents>
                        { title && <Title>{title}</Title>}
                        <Body>{body}</Body>
                    </Contents>
                </Square>
            </Sizer>
        )
    }
}

export default Memo;

여기까지 작업하시면 메모를 열고, 닫을 수 있게됩니다.

저장, 삭제 기능 구현하기

이제 메모 저장과 삭제 기능을 구현해봅시다!

저장 / 삭제 API 함수 만들기

web-api.js 파일을 열어서 다음 함수들을 추가하세요.

src/lib/web-api.js

// (...)
export const updateMemo = ({id, memo: { title, body }}) => axios.put(`/memo/${id}`, {title, body}); // 메모를 업데이트한다
export const deleteMemo = (id) => axios.delete(`/memo/${id}`); // 메모를 제거한다

updateMemo 의 경우 id 와 memo 객체가 담겨있는 객체형태를 파라미터로 받습니다. deleteMemo 의 경우엔 id 만을 파라미터로 받습니다.

액션, 리듀서 작성하기

이제 위 API 를 호출하는 액션, 액션생성자 그리고 이를 처리하는 리듀서를 작성하도록 하겠습니다.
액션 생성자를 만들땐, createAction() 의 두번째 파라미터로 payload => payload 가 설정이 되는데요. 이 두번째 파라미터는 액션의 payload 의 값에 따라 메타데이터를 정의해줍니다. 지금의 경우엔 payload 값 자체를 메타데이터로 설정하도록 했지요. 이렇게 하고 나면, 액션의 payload 값을, onSuccess 부분에서 action.meta 를 통하여 조회 할 수 있습니다.

현재 액션을 처리 할 때 payload 값을 알아야 하는 이유는, 그 정보에 따라, 현재 스토어가 지니고있는 데이터를 수정해주어야 하기 때문입니다. onSuccess 의 payload 는 요청의 결과정보만 가지고 있고 해당 액션이 시작 될 때, 어떤 payload 가 전달됐었는지는 갖고있지 않기 때문이지요.

src/modules/memo.js

// (...)

const UPDATE_MEMO = 'memo/UPDATE_MEMO';
const DELETE_MEMO = 'memo/DELETE_MEMO';

// (...)

// createAction 의 두번째 파라미터는 meta 데이터를 만들 때 사용됩니다.
export const updateMemo = createAction(UPDATE_MEMO, WebAPI.updateMemo, payload => payload); // { id, memo: {title,body} }
export const deleteMemo = createAction(DELETE_MEMO, WebAPI.deleteMemo, payload => payload); // id


const initialState = Map({
    data: List()
});

export default handleActions({
    // (...)

    // 메모 업데이트
    ...pender({
        type: UPDATE_MEMO,
        onSuccess: (state, action) => {
            const { id, memo: { title, body} } = action.meta;
            const index = state.get('data').findIndex(memo => memo.get('id') === id);
            return state.updateIn(['data', index], (memo) => memo.merge({
                title,
                body
            }))
        }
    }),
    // 메모 삭제
    ...pender({
        type: DELETE_MEMO,
        onSuccess: (state, action) => {
            const id = action.meta;
            const index = state.get('data').findIndex(memo => memo.get('id') === id);
            return state.deleteIn(['data', index]);
        }
    })
}, initialState);

MemoViewerContainer 컴포넌트에서 액션 디스패치하기

리덕스 관련 코드도 완성이 되었으니 이제 이 액션을 디스패치 해볼까요?

// (...)
class MemoViewerContainer extends Component {

    // (...)

    handleUpdate = () => {
        const { MemoActions, UIActions, memo } = this.props;
        const { id, title, body } = memo.toJS();
        MemoActions.updateMemo({
            id,
            memo: { title, body }
        });
        UIActions.closeViewer();
    }

    handleDelete = () => {
        const { MemoActions, UIActions, memo } = this.props;
        const { id } = memo.toJS();
        MemoActions.deleteMemo(id);
        UIActions.closeViewer();
    }


    render() {
        const { visible, memo, UIActions } = this.props;
        const { title, body } = memo.toJS();
        const { handleChange, handleUpdate, handleDelete } =this;

        return (
            <MemoViewer
                visible={visible}
                title={title}
                body={body}
                onChange={handleChange}
                onClose={UIActions.closeViewer}
                onUpdate={handleUpdate}
                onDelete={handleDelete}
            />
        );
    }
}

// (...)

여기까지 하고 나시면 메모 수정과 삭제가 가능해질겁니다!

results matching ""

    No results matching ""