ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] React.js와 firebase를 사용하여 간단한 메모 앱 만들기[2] - 메모 저장 및 삭제하기
    Frontend 2019. 8. 30. 00:06

     

     

    이제 메모 저장 / 삭제 이벤트도 추가해보자!

     

     

     

    constants/actionTypes.js

    export const ADD_NEW_MEMO_PENDING = 'ADD_NEW_MEMO_PENDING';
    export const ADD_NEW_MEMO_SUCCESS = 'ADD_NEW_MEMO_SUCCESS';
    export const ADD_NEW_MEMO_FAILURE = 'ADD_NEW_MEMO_FAILURE';
    
    export const DEL_MEMO_PENDING = 'DEL_MEMO_PENDING';
    export const DEL_MEMO_SUCCESS = 'DEL_MEMO_SUCCESS';
    export const DEL_MEMO_FAILURE = 'DEL_MEMO_FAILURE';

     

    메모를 저장하고 삭제하는 액션들을 정의한다.

     

     

     

    actions/index.js

    export const addNewMemoPending = () => ({
      type: types.ADD_NEW_MEMO_PENDING
    });
    
    export const addNewMemoSuccess = (newMemo) => ({
      type: types.ADD_NEW_MEMO_SUCCESS,
      newMemo: newMemo
    });
    
    export const addNewMemoFailure = (error) => ({
      type: types.ADD_NEW_MEMO_FAILURE,
      error
    });
    
    export const delMemoPending = () => ({
      type: types.DEL_MEMO_PENDING
    });
    
    export const delMemoSuccess = () => ({
      type: types.DEL_MEMO_SUCCESS
    });
    
    export const delMemoFailure = (error) => ({
      type: types.DEL_MEMO_FAILURE,
      error
    });

     

    Pending과 Success, Failure 이렇게 세 단계로 나누는 이유는

    비동기 작업을 할 때 각 단계를 인지하여

    요청이 완료되기전까지 Loading spinner를 보여주고

    에러가 발생할 경우 에러 처리를 해주기 위함이다.

     

     

     

    reducers/sendReducer.js

    import * as types from '../constants/actionTypes';
    
    const sendInitState = {
      isSendError: false,
      isSending: false
    }
    
    const sendReducer = (state = sendInitState, action) => {
      switch (action.type) {
        case types.ADD_NEW_MEMO_PENDING:
          return {
            isSendError: false,
            isSending: true
          };
        case types.ADD_NEW_MEMO_SUCCESS:
          return {
            isSendError: false,
            isSending: false
          };
        case types.GET_MEMO_LIST_FAILURE:
          return {
            isSendError: true,
            isSending: false
          };
        default:
          return state;
      }
    };
    
    export default sendReducer;

     

    전송 요청하는 부분은 아예 기존의 LoadReducer.js와 분리하여 따로 작업하였다.

    여기선 전송에 따른 Loading과 Error 상태만을 다룬다.

     

     

     

    reducers/delReducer.js

    import * as types from '../constants/actionTypes';
    
    const delInitState = {
      isDelError: false,
      isDeleting: false
    }
    
    const delReducer = (state = delInitState, action) => {
      switch (action.type) {
        case types.DEL_MEMO_PENDING:
          return {
            isDelError: false,
            isDeleting: true
          };
        case types.DEL_MEMO_SUCCESS:
          return {
            isDelError: false,
            isDeleting: false
          };
        case types.DEL_MEMO_FAILURE:
          return {
            isDelError: true,
            isDeleting: false
          };
        default:
          return state;
      }
    };
    
    export default delReducer;

     

    마찬가지로 삭제와 관련된 비동기 작업은 따로 Reducer를 만들어서 관리해준다.

     

     

     

    reducers/reducer.js

    import { combineReducers } from 'redux';
    import loadReducer from './loadReducer';
    import sendReducer from './sendReducer';
    import delReducer from './delReducer';
    
    export default combineReducers({
      loadReducer,
      sendReducer,
      delReducer
    });

     

    세 개의 리듀서를 모두 combine해준다.

     

     

     

    api/api.js

    export const sendMemoApi = newMemo => {
      const {
        id,
        title,
        content,
        createdAt
      } = newMemo;
    
      return new Promise((resolve, reject) => {
        firebase.database().ref('memos/' + id).set({
          title,
          content,
          created_at: createdAt
        })
          .then(() => resolve())
          .catch(() => reject());
      });
    };
    
    export const delMemoApi = memoId => {
      return new Promise((resolve, reject) => {
        firebase.database().ref('memos/' + memoId).set(null)
          .then(() => resolve())
          .catch(() => reject());
      })
    };

     

    이제 firebase로 데이터를 추가해주고 삭제해주는 비동기 함수를 만들자.

    데이터를 write하는 함수는 새로 추가할 메모 데이터 객체를 인자로 받아

    id를 키값으로 하여 데이터를 추가한다.

     

    firebase realtime database에서 데이터를 삭제하는 방법에는 여러 가지가 있겠지만

    그냥 가장 간단한 방법은 원하는 위치의 데이터를 null로 set하는 것이다.

     

    (set은 promise를 리턴할 수 있으므로 then과 catch를 사용할 수 있다.)

     

    데이터 구독을 지난 포스팅에서 on()을 사용하여 구현하였으므로

    데이터가 추가되거나 삭제되는 즉시 on() 메소드가 자동으로 실행되어 데이터 구독이 계속 이루어질 것이다.

    그러므로 데이터를 저장하고 삭제한 후에 변경된 데이터를 다시 불러오는 로직을 쓸 필요는 없다.

    파이어베이스가 알아서 다 해준다.

     

     

     

    containers/MemoWrite.js

     

    import { connect } from 'react-redux';
    import { sendMemoApi } from '../api/api';
    import * as action from '../actions/index';
    import MemoWrite from '../components/MemoWrite';
    
    const mapDispatchToProps = dispatch => ({
      onMemoSubmit: async (id, title, content, time) => {
        try {
          const newMemo = {
            id,
            title,
            content,
            createdAt: time
          };
    
          dispatch(action.addNewMemoPending());
    
          await sendMemoApi(newMemo);
    
          dispatch(action.addNewMemoSuccess(newMemo));
        } catch (error) {
          alert('메모 추가가 실패하였습니다. 잠시 후 다시 시도해주세요.')
          dispatch(action.addNewMemoFailure(error));
        }
      }
    });
    
    export default connect(
      null,
      mapDispatchToProps
    )(MemoWrite);
    

     

    이제 form을 작성하고 submit 버튼을 눌렀을때

    데이터를 전송 요청하는 비동기 함수를 실행할 함수가 필요하다.

     

    그런데 MemoWrite 컴포넌트는 App의 하위에 있는 MemoList 컴포넌트의 하위에 존재한다.

     

    App -> MemoList -> MemoWrite

     

    물론 App container에 함수를 정의하고 MemoList에 prop으로 넘겨준 다음

    MemoWrite로 다시 prop을 넘겨줄 수도 있지만

    MemoWrite Container를 따로 만들어서 함수를 정의한 다음 MemoWrite 컴포넌트에 바로 넘겨줄 수도 있다.

     

     

     

    components/MemoWrite.js

    import React, { useState } from 'react';
    
    const MemoWrite = props => {
      const [ memoTitle, setMemoTitle ] = useState('');
      const [ memoContent, setMemoContent ] = useState('');
    
      const {
        onMemoSubmit
      } = props;
    
      const handleChange = e => {
        if (e.target.name === 'memoTitle') {
          setMemoTitle(e.target.value);
        } else if (e.target.name === 'memoContent') {
          setMemoContent(e.target.value);
        }
      };
    
      const handleSubmit = e => {
        e.preventDefault();
    
        const title = memoTitle;
        const content = memoContent;
        const time = new Date().toISOString();
        const id = '' + new Date().getTime();
    
        if (!title.trim() || !content.trim()) {
          alert('내용을 입력해주세요!');
          return;
        }
    
        onMemoSubmit(id, title, content, time);
    
        setMemoTitle('');
        setMemoContent('');
      };
    
      return (
        <form
          className="memo-form"
          onSubmit={handleSubmit}
          autoComplete="off"
        >
          <input
            type="text"
            name="memoTitle"
            value={memoTitle}
            onChange={handleChange}
            placeholder="Title"
          />
          <textarea
            name="memoContent"
            value={memoContent}
            onChange={handleChange}
          />
          <button
            type="submit"
            className="btn-submit"
          >
            SUBMIT
          </button>
        </form>
      );
    };
    
    export default MemoWrite;
    

     

    이렇게 MemoWrite 컴포넌트는 MemoWrite 컨테이너에서 만들어지는

    onMemoSubmit() 함수를 바로 prop으로 전달받아 사용할 수 있다.

     

    여기서 중요한 점은 form 태그 안의 input이나 textarea의 value는

    해당 컴포넌트 내부의 state로 따로 관리한다는 점이다.

    input의 value값에 접근하기 위해서는 ref()를 이용해서 직접 접근하는 방법도 있지만

    리액트는 직접 돔에 접근하는 것은 최대한 지양하고 있기 때문에 

    state로 관리하는 것이 좀 더 자연스럽다.

     

     

     

    containers/App.js

    const mapStateToProps = state => {
      const {
        memoList
      } = state.loadReducer;
    
      const props = {
        ...state.delReducer,
        ...state.sendReducer,
        ...state.loadReducer
      }
    
      if (!memoList) {
        return props;
      }
    
      return {
        ...props,
        memoList: camelizeKey(memoList)
      };
    }
    
    const mapDispatchToProps = dispatch => ({
      onMemoListLoad: () => {
        getMemoListApi(dispatch);
      },
      onMemoDelete: async (memoId) => {
        try {
          dispatch(action.delMemoPending());
    
          await delMemoApi(memoId);
    
          dispatch(action.delMemoSuccess());
        } catch (error) {
          alert('삭제가 실패했습니다. 잠시 후 다시 시도해주세요.');
          dispatch(action.delMemoFailure());
        }
      }
    });

     

    이제 메모를 작성하고 전송 버튼을 누르면

    메모 내용을 firebase에 저장하기 위한 api 함수가 실행하고

    그러면 action이 발생하여 각 상태에 맞는 state값이 변경되고

    mapStateToProps에 의해 가공되어 Presentational component로 전달되며

    그에 따라서 렌더링이 다시 일어나게 된다.

     

    App container에 메모를 삭제하는 api 요청을 보내는 함수도 추가하였다.

    이제 이 함수는 App컴포넌트에서 MemoList 컴포넌트로 전달되어

    각 List에 있는 삭제 버튼에 onClick이벤트로 달릴 것이다.

     

     

     

    components/MemoList.js

    const MemoList = props => {
      const {
        memoList,
        isSending,
        isDeleting,
        onMemoDelete
      } = props;
    
      const [ isMemoWriteOpened, setMemoWriteStatus ] = useState(false);
    
      const hasMemo = memoList && memoList.length > 0;
    
      return (
        <>
          <div className="chk-write-wrap">
            <label className="chk-write-label">
              <FontAwesomeIcon
                icon={faPen}
              />
              <span className="sr-only">
                Write
              </span>
              <input
                className="chk-write"
                value={isMemoWriteOpened}
                type="checkbox"
                onChange={() => setMemoWriteStatus(!isMemoWriteOpened)}
              />
            </label>
          </div>
    
          {isMemoWriteOpened && <MemoWrite />}
    
          {(isSending || isDeleting) && (
            <div className="sending-bar">
              <FontAwesomeIcon
                icon={faSpinner}
                size="4x"
                pulse
              />
            </div>
          )}
    
          <ul className="memo-list">
            {!hasMemo && (
              <li className="no-memo">
                <div className="align-center">
                  <h2 className="title">No Memos</h2>
                  <p>
                    Write anything you want!
                  </p>
                </div>
              </li>
            )}
            {hasMemo &&
              memoList.map(memo => (
                <li key={memo.id}>
                  <Link to={`/memos/${memo.id}`}>
                    <h2 className="memo-title">{memo.title}</h2>
                    <div className="memo-content">{memo.content}</div>
                    <span className="memo-date">
                      {setTimeForm(memo.createdAt)}
                    </span>
                  </Link>
                  <button
                    type="button"
                    className="btn-delete"
                    onClick={() => onMemoDelete(memo.id)}
                  >
                    <FontAwesomeIcon
                      icon={faTimes}
                    />
                    <span
                      className="sr-only"
                    >
                      delete
                    </span>
                  </button>
                </li>
              ))
            }
          </ul>
        </>
      );
    };

     

    바로 이렇게 말이다!

     

     

     


     

     

    글을 수정한다거나 사진을 첨부한다거나 이런 기능은 하나도 없는

    아주 아주 단순한 연습용 앱이지만 그래도 만들고 나니까 매우 뿌듯하다ㅎㅎ

     

    나중에 시간이 생기면 다른 기능들도 추가해봐야겠다.

     

    끝!

     

    반응형

    COMMENT