2-4. 포스트 수정 및 삭제하기

이제 거의 작업을 마쳤습니다. 이번엔 포스트의 수정과 삭제를 구현 할 차례입니다.

헤더에 버튼 보여주기

포스트를 읽는 페이지에서 상단에 새 포스트 좌측에 두개의 버튼을 추가해주겠습니다. 수정 버튼의 경우엔 /editor?postId=ID 링크로 이동하도록 설정하고, 삭제의 경우엔 onRemove 라는 함수를 props 로 받아와서 호출하게 설정하겠습니다.

우선, HeaderContainer 를 만들어서 포스트 페이지인 경우에 포스트 아이디를 전달하도록 설정하세요.

src/containers/common/HeaderContainer.js

import React, { Component } from 'react';
import Header from 'components/common/Header';
import { withRouter } from 'react-router-dom';

class HeaderContainer extends Component {
  handleRemove = () => {
    //  미리 만들어두기
  }

  render() {
    const { handleRemove } = this;
    const { match } = this.props;

    const { id } = match.params;

    return (
      <Header
        postId={id}
        onRemove={handleRemove}
      />
    );
  }
}

export default withRouter(HeaderContainer);

아직은 리덕스와 연결되어있지 않지만, 나중에 우리가 관리자 기능을 로그인 구현을 하게 될 때 connect 를 사용하게 되므로, containers 디렉토리에 이렇게 컨테이너 컴포넌트를 생성해주었습니다.

이 컴포넌트를 만들고, PageTemplate 에서 Header 를 대체해주세요.

src/components/common/PageTemplate/PageTemplate.js

import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
import HeaderContainer from 'containers/common/HeaderContainer';
import Footer from 'components/common/Footer';

const cx = classNames.bind(styles);

const PageTemplate = ({children}) => (
  <div className={cx('page-template')}>
    <HeaderContainer/>
    <main>
      {children}
    </main>
    <Footer/>
  </div>
);

export default PageTemplate;

이제, params 에 id 가 있는 경우 해당 값을 Header 로 전달을 해줍니다. Header 컴포넌트를 열어서, postId 를 전달 받았을 경우에 두 버튼이 보여지도록 설정하세요.

src/components/common/Header/Header.js

(...)

const Header = ({postId, onRemove}) => (
  <header className={cx('header')}>
    <div className={cx('header-content')}>
      <div className={cx('brand')}>
        <Link to="/">reactblog</Link>
      </div>
      <div className={cx('right')}>
        {
          // flex 를 유지하기 위하여 배열 형태로 렌더링 합니다.
          postId && [
            <Button key="edit" theme="outline" to={`/editor?id=${postId}`}>수정</Button>,
            <Button key="remove" theme="outline" onClick={onRemove}>삭제</Button>
          ]
        }
        <Button theme="outline" to="/editor">새 포스트</Button>
      </div>
    </div>
  </header>
);

export default Header;

수정 버튼을 누르면, /editor/?id=ID 페이지로 전환하도록 설정하였습니다. 그리고, 삭제 버튼을 누르면 props 로 전달받은 onRemove 를 호출하도록 설정했습니다. 이 함수는, 추후 구현 될 것이며, 우선 수정 기능부터 구현을 해보겠습니다.

수정 기능 구현하기

에디터에서 포스트정보 불러오기

수정 버튼을 클릭하여 에디터 페이지로 오게 되면 id 라는 쿼리가 설정됩니다. 에디터가 열릴 때, 이 id 값이 존재한다면, 해당 포스트의 내용을 불러와서 editor 의 상태에 넣어주겠습니다.

기존에 만들었던 getPost API 함수를 재활용 하여, editor 모듈에 GET_POST 액션을 만드세요.

src/store/modules/editor.js

(...)
const GET_POST = 'editor/GET_POST';

// action creators
(...)
export const getPost = createAction(GET_POST, api.getPost);

// initial state
const initialState = Map({
  title: '',
  markdown: '',
  tags: '',
  postId: null
});

// reducer
export default handleActions({
  (...)
  ...pender({
    type: GET_POST,
    onSuccess: (state, action) => {
      const { title, tags, body } = action.payload.data;
      return state.set('title', title)
                  .set('markdown', body)
                  .set('tags', tags.join(', ')); // 배열 -> ,로 구분된 문자열
    }
  })
}, initialState)

그 다음엔, EditorHeaderContainer 를 열어서 componentDidMount 부분에서 initialize 를 실행한 다음에 쿼리값이 들어있는 문자열 형태인 location.search 값을 파싱하여 이 안에 id 값이 들어있으면 GET_POST 를 실행하도록 코드를 작성해보세요.

src/containers/editor/EditorHeaderContainer.js

import queryString from 'query-string';

class EditorHeaderContainer extends Component {

  componentDidMount() {
    const { EditorActions, location } = this.props;
    EditorActions.initialize(); // 에디터를 초기화 합니다.

    // 쿼리 파싱
    const { id } = queryString.parse(location.search);
    if(id) { 
      // id 가 존재하는 경우 포스트 불러오기
      EditorActions.getPost(id);
    }
  }

  (...)

코드를 저장하고 나서, 포스트를 수정 할 때, 초기 데이터를 제대로 불러와주는지 확인 해보세요.

수정 API 함수 및 액션 만들기

수정 API 함수는 writePost 함수와 비슷하지만, axios.patch 를 사용하고, id 값을 추가적으로 받습니다.

api 파일을 열어서 다음 함수를 추가하세요

src/lib/api.js

()
export const editPost = ({id, title, body, tags}) => axios.patch(`/api/posts/${id}`, { title, body, tags});

이제 이 함수를 호출하는 액션을 준비해줍시다.

여기서는, 리듀서 부분 (handleActions) 에서 따로 로직을 작성 할 필요가 없습니다. 이미 현재 어떤 포스트를 수정하는지 알고 있으니까요.

src/store/modules/editor.js

import { createAction, handleActions } from 'redux-actions';

import { Map } from 'immutable';
import { pender } from 'redux-pender';
import * as api from 'lib/api';

// action types
(...)
const EDIT_POST = 'editor/EDIT_POST';

// action creators
(...)
export const editPost = createAction(EDIT_POST, api.editPost);
(...)

액션을 작성하셨다면, EditorHeaderContainer 를 마저 구현하겠습니다. handleSubmit 부분에서, id 값이 존재한다면 writePost 가 아닌 editPost 를 호출하도록 설정하세요.

그 다음엔, 렌더링 부분에서 id값이 있는 경우 isEdit 이라는 props를 true 로 설정하겠습니다.

src/containers/editor/EditorHeaderContainer.js

(...)

class EditorHeaderContainer extends Component {
  (...)
  handleSubmit = async () => {
    const { title, markdown, tags, EditorActions, history, location } = this.props;
    const post = {
      title,
      body: markdown,
      // 태그 텍스트를 , 로 분리시키고 앞뒤 공백을 지운 후 중복 되는 값을 제거해줍니다.
      tags: tags === "" ? [] : [...new Set(tags.split(',').map(tag => tag.trim()))]
    };
    try {
      // id 가 존재하는 경우 editPost 호출
      const { id } = queryString.parse(location.search);
      if(id) {
        await EditorActions.editPost({ id, ...post });
        history.push(`/post/${id}`);
        return;
      } 
      await EditorActions.writePost(post);
      // 페이지를 이동시킵니다. 주의: postId 를 상단에서 레퍼런스를 만들지 않고
      // 이 자리에서 this.props.postId 를 조회해주어야합니다. (현재의 값을 불러오기 위함)
      history.push(`/post/${this.props.postId}`);
    } catch (e) {
      console.log(e);
    }
  }


  render() {
    const { handleGoBack, handleSubmit } = this;
    const { id } = queryString.parse(this.props.location.search);
    return (
      <EditorHeader
        onGoBack={handleGoBack}
        onSubmit={handleSubmit}
        isEdit={id ? true : false}
      />
    );
  }
}
(...)

이제 수정하기 기능은 거의 끝났습니다. 사용자가 헷갈리지 않기 위하여, EditorHeader 에서 isEdit 값이 true 라면, 작성하기가 아닌 수정하기 라는 문구를 보여주도록 설정하세요.

src/components/editor/EditorHeader/EditorHeader.js

(...)

const EditorHeader = ({onGoBack, onSubmit, isEdit}) => {
  return (
    (...)
        <Button onClick={onSubmit} theme="outline">{isEdit ? '수정' : '작성'}하기</Button>
      </div>
    </div>
  );
};

export default EditorHeader;

이제, 수정모드 일 때는 다음과 같이 수정하기 버튼이 나타납니다.

버튼의 문구가 잘 바뀌었나요? 내용을 수정하고 이 버튼을 눌러보세요. 변경된 내용을 가진 포스트 페이지로 이동 될 것입니다.

삭제 기능 구현하기

이 프로젝트에서의 마지막 포스트 관련 기능인, 삭제 기능을 구현해봅시다. 이 기능은, 포스트 페이지에서 수정 버튼의 우측에 있는 삭제 버튼을 클릭하면 발생하는데요, 삭제 버튼을 누른다고 해서 바로 삭제가 된다면 안되겠죠? 사용자가 실수로 누를 수도 있으니까요.

따라서, 삭제를 하기 전에 사용자에게 한번 더 물어보는 과정을 거치도록 하겠습니다.

ModalWrapper 와 AskRemoveModal 컴포넌트 생성

포스트 삭제 모달을 구현하기 위하여 우리는 먼저 ModalWrapper 라는 컴포넌트를 만들겠습니다. 이 컴포넌트는, state 가 있는 클래스형 컴포넌트로서, 전체화면에 불투명한 회색 배경을 깔아주고, 그 위에 흰색 박스를 보여주게 되는데, 이 과정에서 모달의 가시성 상태와, 전환 효과를 위한 상태를 관리하게 됩니다.

또한, 이 컴포넌트는 나중에 우리가 비밀번호 로그인을 구현하게 될 때에도, 로그인 모달을 만들 때 재사용 되기도 합니다.

components 디렉토리에 modal 디렉토리를 생성한 뒤, ModalWrapper 라는 컴포넌트를 생성하세요. 컴포넌트는 클래스형으로 만드세요.

src/components/modal/ModalWrapper/ModalWrapper.js

import React, { Component } from 'react';
import styles from './ModalWrapper.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

class ModalWrapper extends Component {
  render() {
    const { children } = this.props;
    return (
      <div>
        <div className={cx('gray-background')}/>
        <div className={cx('modal-wrapper')}>
          <div className={cx('modal')}>
            { children } 
          </div>
        </div>
      </div>
    );
  }
}

export default ModalWrapper;

이 컴포넌트는 children props 를 받아와서 modal 엘리먼트 내부에서 보여줍니다. 상태관리는 나중에 하도록 하고, 지금은 컴포넌트의 틀부터 잡아주겠습니다.

src/components/modal/ModalWrapper/ModalWrapper.scss

@import 'utils';

.gray-background {
  background: rgba(100,100,100,0.5);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
}

.modal-wrapper {
  z-index: 10;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  .modal {
    @include material-shadow(3, 0.5);
    background: white;
  }
}

그 다음엔, 이 컴포넌트를 불러와서 사용 할 AskRemoveModal 컴포넌트를 modal 디렉토리에 생성하세요. 지금은 ModalWrapper 컴포넌트를 불러와서 children 부분엔 “내용” 이라는 텍스트를 넣어서 렌더링하세요.

src/components/modal/AskRemoveModal/AskRemoveModal.js

import React from 'react';
import styles from './AskRemoveModal.scss';
import classNames from 'classnames/bind';
import ModalWrapper from 'components/modal/ModalWrapper';

const cx = classNames.bind(styles);

const AskRemoveModal = () => (
<ModalWrapper>
  내용
  </ModalWrapper>
);

export default AskRemoveModal;

그 다음엔 AskRemoveModal 컴포넌트를 PostPage 에서 불러와서 렌더링하세요.

src/pages/PostPage.js

(...)

const PostPage = ({ match }) => {
  const { id } = match.params;
  return (
    <PageTemplate>
      <Post id={id}/>
      <AskRemoveModal/>
    </PageTemplate>
  );
};

export default PostPage;

그러면, 이렇게 페이지에 페이지의 중앙에 내용이라고 적힌 흰색 박스가 나타나게 됩니다.

AskRemoveModal 컴포넌트 만들기

이제 이 모달에서 보여질 내용을 설정하겠습니다. 이 컴포넌트는 두가지 영역으로 나뉘어져있습니다. 윗 부분엔 모달의 제목, 내용이 있고, 하단에는 사용자가 선택을 할 수 있는 버튼이 있습니다.

src/components/modal/AskRemoveModal/AskRemoveModal.js

import React from 'react';
import styles from './AskRemoveModal.scss';
import classNames from 'classnames/bind';
import ModalWrapper from 'components/modal/ModalWrapper';
import Button from 'components/common/Button';

const cx = classNames.bind(styles);

const AskRemoveModal = () => (
  <ModalWrapper>
    <div className={cx('question')}>
      <div className={cx('title')}>포스트 삭제</div>
      <div className={cx('description')}>이 포스트를 정말로 삭제하시겠습니까?</div>
    </div>
    <div className={cx('options')}>
      <Button theme="gray">취소</Button>
      <Button>삭제</Button>
    </div>
  </ModalWrapper>
);

export default AskRemoveModal;

src/components/modal/AskRemoveModal/AskRemoveModal.scss

@import 'utils';

.question {
  background: white;
  padding: 2rem;
  .title {
    font-size: 1.25rem;
    font-weight: 500;
  }
  .description {
    margin-top: 0.25rem;
  }
}

.options {
  padding: 1rem;
  background: $oc-gray-1;
  text-align: right;
}

이렇게 코드를 작성하고 나면, 이전에 봤던 것 처럼 모달이 보여지게 됩니다.

모달 상태 관리하기

모달은, 기본 상태에선 보여지지 않고, 유저가 삭제 버튼을 눌렀을 때만 보여져야 합니다. 따라서, 우리는 이 컴포넌트가 어떤 상황에 보여져야 할 지 설정을 해주어야 합니다. 이를 구현하기 위해 우선 ModalWrapper 컴포넌트에서 visible 이란 props 를 받아와서 상황에 따라 null 을 구현하도록 렌더링 함수를 수정하세요.

src/components/modal/ModalWrapper/ModalWrapper.js

(...)
class ModalWrapper extends Component {
  render() {
    const { children, visible } = this.props;
    if(!visible) return null;
     (...)

이렇게 하고 나면, 컴포넌트가 visible 값이 true 일 때에만 보여지기 때문에 화면에 나타나지 않을 것 입니다.

그 다음에는, 모달의 가시성 상태를 관리하기 위하여 리덕스 모듈 중 base.js 를 수정하겠습니다. 이 모듈은, 모달의 가시성을 관리하게 되며, 추후 로그인 기능을 구현하게 될 때, 로그인 모달의 상태와, 로그인 상태도 관리하게 됩니다.

src/store/modules/base.js

import { createAction, handleActions } from 'redux-actions';

import { Map } from 'immutable';
import { pender } from 'redux-pender';

// action types
const SHOW_MODAL = 'base/SHOW_MODAL';
const HIDE_MODAL = 'base/HIDE_MODAL';

// action creators
export const showModal = createAction(SHOW_MODAL);
export const hideModal = createAction(HIDE_MODAL);

// initial state
const initialState = Map({
  // 모달의 가시성 상태
  modal: Map({ 
    remove: false,
    login: false // 추후 구현 될 로그인 모달
  })
});

// reducer
export default handleActions({
  [SHOW_MODAL]: (state, action) => {
    const { payload: modalName } = action;
    return state.setIn(['modal', modalName], true);  
  },
  [HIDE_MODAL]: (state, action) => {
    const { payload: modalName } = action;
    return state.setIn(['modal', modalName], false);  
  }
}, initialState)

여기서 우리는 SHOW_MODALHIDE_MODAL 액션을 만들게 됩니다. 이 액션은, 주어진 payload 값에 따라서 modal Map 내부에 있는 값을 true 혹은 false 로 전환해줍니다.

이 과정에서, 굳이 액션을 두개로 나누지 않고, SET_MODAL_VISIBILITY 같은 액션을 만들어서 payload 부분엔 modalName 과 visible 값을 받아와서 구현해도 무방합니다. 어떠한 방식으로 하던, 여러분의 자유입니다.

리덕스 모듈을 만들었다면, 컨테이너 컴포넌트도 만들어주어야겠죠? containers 디렉토리에 modal 디렉토리를 만들고, 그 안에 AskRemoveModalContainer 컴포넌트를 생성하세요.

src/containers/modal/AskRemoveModalContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as baseActions from 'store/modules/base';
import * as postActions from 'store/modules/post';
import AskRemoveModal from 'components/modal/AskRemoveModal';

class AskRemoveModalContainer extends Component {
  handleCancel = () => {

  }

  handleConfirm = () => {

  }

  render() {
    const { visible } = this.props;
    const { handleCancel, handleConfirm } = this;

    return (
      <AskRemoveModal visible={visible} onCancel={handleCancel} onConfirm={handleConfirm}/>
    );
  }
}

export default connect(
  (state) => ({
    visible: state.base.getIn(['modal', 'remove'])
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch),
    PostActions: bindActionCreators(postActions, dispatch)
  })
)(AskRemoveModalContainer);

visible 값을 리덕스에서 받아와서 AskRemoveModal 에게 전달해주었으며, base 와 post 모듈의 액션들을 미리 연결해놓았습니다. 그리고, 확인 버튼을 눌렀을 때 실행되는 handleConfirm 과 취소를 눌렀을 때 실행되는 handleCancel 메소드에 비어있는 함수를 미리 설정해놓고, 이를 onConfirm / onCancel 이라는 이름으로 AskRemoveModal 에 전달해주었습니다.

그리고 이렇게 넣어준 props 를 AskRemoveModal 에서 반영시키세요.

src/components/modal/AskRemoveModal/AskRemoveModal.js

(...)
const AskRemoveModal = ({visible, onConfirm, onCancel}) => (
  <ModalWrapper visible={visible}>
    <div className={cx('question')}>
      <div className={cx('title')}>포스트 삭제</div>
      <div className={cx('description')}>이 포스트를 정말로 삭제하시겠습니까?</div>
    </div>
    <div className={cx('options')}>
      <Button theme="gray" onClick={onCancel}>취소</Button>
      <Button onClick={onConfirm}>삭제</Button>
    </div>
  </ModalWrapper>
);

export default AskRemoveModal;

그 다음엔, PostPage 에서 AskRemoveModal을 방금 만든 컨테이너 컴포넌트로 대체시키세요.

src/pages/PostPage.js

(...)
import AskRemoveModalContainer from 'containers/modal/AskRemoveModalContainer';

const PostPage = ({ match }) => {
  const { id } = match.params;
  return (
    <PageTemplate>
      <Post id={id}/>
      <AskRemoveModalContainer/>
    </PageTemplate>
  );
};

export default PostPage;

모달을 보여줄 준비가 거의 끝났습니다. 이제 HeaderContainer 컴포넌트에서 만들어뒀던 handleRemove 메소드가 호출 될 때, 모달을 띄우도록 코드를 작성하세요.

src/containers/common/HeaderContainer.js

(...)
import * as baseActions from 'store/modules/base';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class HeaderContainer extends Component {
  handleRemove = () => {
    const { BaseActions } = this.props;
    BaseActions.showModal('remove');
  }

  render() {
    (...)
  }
}

export default connect(
  (state) => ({}),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(withRouter(HeaderContainer));

코드를 다 작성하셨다면, 포스트 페이지에서 삭제 버튼을 눌러보세요. 모달이 잘 뜨나요?

삭제 모달 버튼 기능 구현하기

삭제 모달에서 삭제 버튼을 누르면, 현재 보고있는 포스트를 삭제하고, 취소 버튼을 누르면 모달이 종료되도록 설정하겠습니다.

우선, 취소버튼 부터 구현해봅시다.

AskRemoveModalContainer 컴포넌트의 handleCancel 메소드에서 BaseActions.hideModal 를 호출하세요.

src/containers/modal/AskRemoveModalContainer.js - handleCancel

  handleCancel = () => {
    const { BaseActions } = this.props;
    BaseActions.hideModal('remove');
  }

그 다음엔, api.js 파일에 포스트 삭제 API 함수를 생성하고, 이를 위한 액션을 준비하고 나서 handleConfirm 에서 호출하세요.

src/lib/api.js

(...)
export const removePost = (id) => axios.delete(`/api/posts/${id}`);

src/store/modules/post.js

(...)
// action types
const GET_POST = 'post/GET_POST';
const REMOVE_POST = 'post/REMOVE_POST';

// action creators
export const getPost = createAction(GET_POST, api.getPost);
export const removePost = createAction(REMOVE_POST, api.removePost);
(...)

삭제 액션은 post 모듈에서 작성하면 되며, 리듀서에서 상태관리는 따로 해주지 않아도 됩니다.

액션을 작성 한 후엔, AskRemoveModalContainer 를 withRouter 로 감싸서 handleConfirm 에서 방금 만든 액션 생성 함수에 현재 포스트 아이디를 파라미터로 넣어서 호출하고, 삭제요청이 완료 된 후 홈페이지로 주소를 이동시키세요.

src/containers/modal/AskRemoveModalContainer.js

(...)
import { withRouter } from 'react-router-dom';

class AskRemoveModalContainer extends Component {
  (...)

  handleConfirm = async () => {
    const { BaseActions, PostActions, history, match } = this.props;
    const { id } = match.params;

    try {
      // 포스트 삭제 후, 모달 닫고 홈페이지로 이동
      await PostActions.removePost(id);
      BaseActions.hideModal('remove');
      history.push('/');
    } catch (e) {
      console.log(e);
    }

  }

  (...)
}

export default connect(
  (state) => ({
    visible: state.base.getIn(['modal', 'remove'])
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch),
    PostActions: bindActionCreators(postActions, dispatch)
  })
)(withRouter(AskRemoveModalContainer));

자, 여기까지 하고나면 삭제 기능이 완료됩니다. 한번 포스트 삭제를 시도해보세요. 홈 화면으로 이동하고, 기존에 작성했던 포스트가 사라졌나요?

모달 전환 효과 구현하기

모달이 나타나고 사라질 때, 좀 더 자연스럽게 보여지기 위하여 전환 애니메이션 효과를 설정해봅시다. 전환 효과를 만들 때에는, CSS 의 @keyframes 를 이용하여 구현합니다. @keyframes 를 사용하여 전환 효과의 시작 부분, 그리고 마지막 부분의 스타일을 지정해주면 스타일이 서서히 변화하게 되면서 애니메이션 효과가 구현됩니다.

우리는 ModalWrapper.scss 에 총 4가지 종류의 @keyframes 를 만들겠습니다.

  • fadeIn: 투명도가 0% 100%
  • fadeout: 투명도가 100% 0%
  • slideUp: 아래에서 위로 올라오는 효과
  • slideDown: 위에서 아래로 내려가는 효과

ModalWrapper.scss 파일의 상단에 다음 코드를 추가하세요:

src/components/modal/ModalWrapper/ModalWrapper.scss

@import 'utils';

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

@keyframes fadeOut { 
  0% { opacity: 1; }
  100% { opacity: 0; } 
}

@keyframes slideUp {
  0% { transform: translateY(50vh); }
  100% { transform: translateY(0); }
}

@keyframes slideDown {
  0% { transform: translateY(0); }
  100% { transform: translateY(50vh); }
}

(...)

그 다음에는, gray-background 클래스와 modal 클래스 내부에 &.enter 와 &.leave 클래스를 만들어서 각각 알맞는 animation 을 설정하세요.

src/components/modal/ModalWrapper/ModalWrapper.scss

(...)
.gray-background {
  (...)
  &.enter {
    animation: fadeIn 0.25s ease-in both;
  }
  &.leave {
    animation: fadeOut 0.25s ease-in both;
  }
}

.modal-wrapper {
  (...)
  .modal {
    (...)
    &.enter {
      animation: slideUp 0.25s ease-in both;
    }
    &.leave {
      animation: slideDown 0.25s ease-in both;
    }
  }
}

애니메이션은 0.25 초동안 지속되도록 설정했습니다. 자, 이에 맞춰서 ModalWrapper 컴포넌트에서 visible 값이 바뀜에 따라 내부 state 를 설정하여 enter 혹은 leave 애니메이션을 적용시켜보세요.

src/components/modal/ModalWrapper/ModalWrapper.js

(...)
class ModalWrapper extends Component {
  state = {
    animate: false
  }

  startAnimation = () => {
    // animate 값을 true 로 설정 후
    this.setState({
      animate: true
    });
    // 250ms 이후 다시 false 로 설정
    setTimeout(() => {
      this.setState({
        animate: false
      });
    }, 250)
  }

  componentDidUpdate(prevProps, prevState) {
    if(prevProps.visible !== this.props.visible) {
      this.startAnimation();
    }
  }


  render() {
    const { children, visible } = this.props;
    const { animate } = this.state;

    // visible 값과 animate 값이 둘 다 false 일 떄에만
    // null 을 리턴합니다.
    if(!visible && !animate) return null;

    // 상태에 따라 애니메이션 설정
    const animation = animate && (visible ? 'enter' : 'leave');


    return (
      <div>
        <div className={cx('gray-background', animation)}/>
        <div className={cx('modal-wrapper')}>
          <div className={cx('modal', animation)}>
            { children } 
          </div>
        </div>
      </div>
    );
  }
}

export default ModalWrapper;

이 과정에서, startAnimation 메소드를 만들고, componentDidUpdate 에서 visible 값이 바뀔 때 마다 이 메소드를 호출하도록 설정합니다. startAnimation 에서는, 호출 시 내부 state 인 animate 값을 true 로 설정하고, 250ms 초 뒤 false 로 다시 설정합니다.

animate 가 true 일 때에는, visible 값에 따라서 ‘enter’ 혹은 ‘leave’ 를 배경화면과 모달에 클래스로 넣어주며, 애니메이션이 진행되고 있는 동안에는 컴포넌트가 화면에서 사라지지 않도록 visible 값과 animate 값이 둘다 false 일 때에만 null 을 리턴하도록 설정 되었습니다.

자, 이제 삭제 모달의 애니메이션 구현 까지 끝났습니다. 이제 프로젝트 기능 구현의 마지막 단계인, 관리자 로그인 인증을 구현해볼 차례입니다.

results matching ""

    No results matching ""