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}
/>
);
}
}
// (...)
여기까지 하고 나시면 메모 수정과 삭제가 가능해질겁니다!