React.js Codelab 2016 – Express 와 React.js 를 사용한 웹 어플리케이션 만들기 (5)


 

4편에서는, 메모를 쓰고 읽는 기능을 구현했습니다.
지금까지 잘 따라와주셨다면, 이번 코드랩의 목표를 이미 달성 한 셈입니다.
여러분들은 React 컴포넌트를 다루는 방법을 어느정도 숙지하였고,
Redux 데이터 흐름 구조에도 익숙해졌으며, 서버와 커뮤니케이션 하는 방법 또한 배웠습니다.

저희가 지금까지 해왔던 지식과 방식을 앞으로 구현 할 수정/삭제/별주기/유저검색에 그대로 적용하면 됩니다.

그럼다시.. 시작해볼까요?

20. 수정기능 구현하기

ff

위에서 보여지는 수정 기능을 한번 구현해봅시다.

수정을 누르면, Write 컴포넌트와 View가 비슷해집니다. 차이점은, POST 버튼 대신 OK 버튼이 있습니다.

우선, Edit 버튼을 눌렀을 때, 수정 창이 보여지도록, 컴포넌트에 state 를 추가하고 메소드도 만들어보겠습니다.

Memo 컴포넌트 수정기능 토글 (src/components/Memo.js)

class Memo extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            editMode: false
        };
        this.toggleEdit = this.toggleEdit.bind(this);
    }
    
    toggleEdit() {
        this.setState({
            editMode: !this.state.editMode
        });
    }
    
    render() {
        const dropDownMenu = (
            /* CODES */
                <ul id={`dropdown-${this.props.data._id}`} className='dropdown-content'>
                    <li><a onClick={this.toggleEdit}>Edit</a></li>
                     /* CODES */
        );
        
        const memoView = (
             /* CODES */
        );
        
        const editView = (
            <div>EDIT</div>
        );
        
        return (
            <div className="container memo">
                { this.state.editMode ? editView : memoView }
            </div>
        );
    }
    
    
     /* CODES */

 

Memo 컴포넌트 editView 완성하기 (src/components/Memo.js)

        const editView = (
            <div className="write">
                <div className="card">
                    <div className="card-content">
                        <textarea
                            className="materialize-textarea"></textarea>
                    </div>
                    <div className="card-action">
                        <a onClick={this.toggleEdit}>OK</a>
                    </div>
                </div>
            </div>
        );

editView 는 Write 컴포넌트와 거~의 비슷합니다.

 

이제, textarea 의 값을 컴포넌트에 state 에서 불러오도록 하고, 수정될때마다 state 도 변경되게하는 handleChange 메소드를 작성하겠습니다.

또, state 의 초기 값을 메모의 내용으로 설정하도록 하겠습니다.

 

Memo 컴포넌트 textarea state 사용하기 (src/components/Memo.js)

class Memo extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            editMode: false,
            value: props.data.contents
        };
        this.toggleEdit = this.toggleEdit.bind(this);
        this.handleChange = this.handleChange.bind(this);
    }
    
    /* CODES */
    
    handleChange(e) {
        this.setState({
            value: e.target.value
        });
    }
    
    render() {
        /* CODES */
        
        const editView = (
            <div className="write">
                <div className="card">
                    <div className="card-content">
                        <textarea
                            className="materialize-textarea"
                            value={this.state.value}
                            onChange={this.handleChange}></textarea>
                    </div>
                    <div className="card-action">
                        <a onClick={this.toggleEdit}>OK</a>
                    </div>
                </div>
            </div>
        );
        
        /* CODES */

handleChange 메소드 만들고 constructor 에서 this binding 하는거 잊지 마시구요.

이렇게, 수정기능을위한 기본적인 틀이 완성되었습니다.

이제 서버와의 통신을 할 차례입니다.

 

Action Type 추가 (src/actions/ActionTypes.js)

export const MEMO_EDIT = "MEMO_EDIT";
export const MEMO_EDIT_SUCCESS = "MEMO_EDIT_SUCCESS";
export const MEMO_EDIT_FAILURE = "MEMO_EDIT_FAILURE";

 

memo 액션파일 수정 (src/actions/memo.js)

import {
    / * CONSTANTS */
    MEMO_EDIT,
    MEMO_EDIT_SUCCESS,
    MEMO_EDIT_FAILURE
} from './ActionTypes';

/* CODES */

/* MEMO EDIT */
export function memoEditRequest(id, index, contents) {
    return (dispatch) => {
        // to be implemented
    };
}

export function memoEdit() {
    return {
        type: MEMO_EDIT
    };
}

export function memoEditSuccess(index, memo) {
    return {
        type: MEMO_EDIT_SUCCESS,
        index,
        memo
    };
}

export function memoEditFailure(error) {
    return {
        type: MEMO_EDIT_FAILIURE,
        error
    };
}

메모 수정의 경우, 메모 수정이 성공하면, 수정된 메모 객체를 반환하며,
전달받은 객체를 리스트에 있던 데이터가 있던 자리에 갈아끼웁니다.

 

memoEditRequest 구현하기 (src/actions/memo.js)

export function memoEditRequest(id, index, contents) {
    return (dispatch) => {
        dispatch(memoEdit());

        return axios.put('/api/memo/' + id, { contents })
        .then((response) => {
            dispatch(memoEditSuccess(index, response.data.memo));
        }).catch((error) => {
            dispatch(memoEditFailure(error.response.data.code));
        });
    };
}

 

memo 리듀서 수정하기 (src/reducers/memo.js)

const initialState = {
    /* CODES */
    edit: {
        status: 'INIT',
        error: -1,
    }
};

export default function memo(state, action) {
    /* CODES */

    switch(action.type) {
        /* CODES */
        case types.MEMO_EDIT: 
            return update(state, {
                edit: {
                    status: { $set: 'WAITING' },
                    error: { $set: -1 },
                    memo: { $set: undefined }
                }
            });
        case types.MEMO_EDIT_SUCCESS:
            return update(state, {
                edit: {
                    status: { $set: 'SUCCESS' },
                },
                list: {
                    data: {
                        [action.index]: { $set: action.memo }
                    }
                }
            });
        case types.MEMO_EDIT_FAILURE:
            return update(state, {
                edit: {
                    status: { $set: 'FAILURE' },
                    error: { $set: action.error }
                }
            });
        default:
            return state;
    }
}

MEMO_EDIT_SUCCESS에서는 list 의 데이터 중 [action.index] 번째 데이터를 새로운 데이터로 교체합니다.

 

Home 컨테이너 컴포넌트에서 memoEditRequest, editStatus 매핑 (src/containers/Home.js)

/* CODES */
import { 
    memoPostRequest, 
    memoListRequest, 
    memoEditRequest 
} from 'actions/memo';

class Home extends React.Component {
    
    /* CODES */
    
}

const mapStateToProps = (state) => {
    return {
        /* CODES */
        editStatus: state.memo.edit
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        /* CODES */
        memoEditRequest: (id, index, contents) => {
            return dispatch(memoEditRequest(id, index, contents));
        }
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);

 

handleEdit 구현하기  (src/containers/Home.js)

/* CODES */
import { 
    memoPostRequest, 
    memoListRequest, 
    memoEditRequest 
} from 'actions/memo';

class Home extends React.Component {
    
    /* CODES */
    
    constructor(props) {
        super(props);
        /* CODES */
        this.handleEdit = this.handleEdit.bind(this);
        /* CODES */
    }

    handleEdit(id, index, contents) {
        return this.props.memoEditRequest(id, index, contents).then(
            () => {
                if(this.props.editStatus.status==="SUCCESS") {
                    Materialize.toast('Success!', 2000);
                } else {
                    /*
                        ERROR CODES
                            1: INVALID ID,
                            2: EMPTY CONTENTS
                            3: NOT LOGGED IN
                            4: NO RESOURCE
                            5: PERMISSION FAILURE
                    */
                    let errorMessage = [
                        'Something broke',
                        'Please write soemthing',
                        'You are not logged in',
                        'That memo does not exist anymore',
                        'You do not have permission'
                    ];
                    
                    let error = this.props.editStatus.error;
                    
                    // NOTIFY ERROR
                    let $toastContent = $('<span style="color: #FFB4BA">' + errorMessage[error - 1] + '</span>');
                    Materialize.toast($toastContent, 2000);
                
                    // IF NOT LOGGED IN, REFRESH THE PAGE AFTER 2 SECONDS
                    if(error === 3) {
                        setTimeout(()=> {location.reload(false)}, 2000);
                    }
                    
                }
            }
        );
    }

    render() {
        /* CODES */
        
        return (
            <div className="wrapper">
                { this.props.isLoggedIn ? write : undefined }
                <MemoList data={this.props.memoData} 
                     currentUser={this.props.currentUser}
                     onEdit={this.handleEdit}
                 />
            </div>
        );
    }
}

/* CODES */

 

handleEdit 메소드를 만들고 MemoList 로 전달해줍니다.

 

MemoList 컴포넌트에서 onEdit 함수 Memo로 전달 (src/components/MemoList.js)

import React from 'react';
import { Memo } from 'components';

class MemoList extends React.Component {
    
    render() {
        const mapToComponents = data => {
            return data.map((memo, i) => {
                return (<Memo 
                            data={memo}
                            ownership={ (memo.writer === this.props.currentUser) }
                            key={memo._id}
                            index={i}
                            onEdit={this.props.onEdit}
                />);
            });
        };
        
        return (
            <div>
                {mapToComponents(this.props.data)}
            </div>
        );
    }
}

MemoList.propTypes = {
    data: React.PropTypes.array,
    currentUser: React.PropTypes.string,
    onEdit: React.PropTypes.func
};

MemoList.defaultProps = {
    data: [],
    currentUser: '',
    onEdit: (id, index, contents) => { 
        console.error('edit function not defined'); 
        
    }
};

export default MemoList;

Memo 컴포넌트에 onEdit 과 editStatus props 를 전달해주었습니다.
추가적으로,  index 라는 props 도 전달해주었습니다 (값은 매핑 함수에서 i로 지정됩니다)
이 props는 해당 메모가 몇번째 메모인지 알려줍니다.

propTypes 와 defaultProps 도 추가해주었습니다.

 

Memo 컴포넌트 수정기능 완성하기 – toggleEdit (src/components/Memo.js)

import React from 'react';
import TimeAgo from 'react-timeago';

class Memo extends React.Component {

    /* CODES */
    
    toggleEdit() {
        if(this.state.editMode) {
            let id = this.props.data._id;
            let index = this.props.index;
            let contents = this.state.value;
            
            this.props.onEdit(id, index, contents).then(() => {
                this.setState({
                    editMode: !this.state.editMode
                });
            })
        } else {
            this.setState({
                editMode: !this.state.editMode
            });   
        }
    }

    render() {
        /* CODES */
        
        // EDITED info
        let editedInfo = (
            <span style={{color: '#AAB5BC'}}> · Edited <TimeAgo date={this.props.data.date.edited} live={true}/></span>
        );

        
        const memoView = (
            <div className="card">
                <div className="info">
                    <a className="username">{this.props.data.writer}</a> wrote a log · <TimeAgo date={this.props.data.date.created}/> 
                    { this.props.data.is_edited ? editedInfo : undefined }
                    { this.props.ownership ? dropDownMenu : undefined }
                </div>
                /* CODES */
        );
        
        /* CODES */
    }

    /* CODES */
}

Memo.propTypes = {
    data: React.PropTypes.object,
    ownership: React.PropTypes.bool,
    onEdit: React.PropTypes.func,
    index: React.PropTypes.number
};

Memo.defaultProps = {
    data: {
        _id: 'id1234567890',
        writer: 'Writer',
        contents: 'Contents',
        is_edited: false,
        date: {
            edited: new Date(),
            created: new Date()
        },
        starred: []
    },
    ownership: true,
    onEdit: (id, index, contents) => {
        console.error('onEdit function not defined');
    },
    index: -1
}

export default Memo;

toggleEdit 부분에서 props로 받은 onEdit 을 사용하도록합니다.

propTypes 와 defaultProps에 onEdit, index가 추가되었습니다.

 

추가적으로, 수정된 메모들은 수정된 시간을 렌더링하도록 하였습니다.

 

21. 삭제기능 구현하기

삭제기능은 매우 간단합니다. 그냥, 삭제하고 리스트 데이터에서 제거해주면 됩니다. 한번 해볼까요?

gzgz

추가적으로, 우리는 삭제를 할 때 애니메이션도 구현 할 거에요. 허나, 애니메이션은 다음 편에서 완성하도록 하겠습니다.

우선, 기능먼저 완성시켜봅시다.

Action Types 추가하기 (src/actions/ActionTypes.js)

export const MEMO_REMOVE = "MEMO_REMOVE";
export const MEMO_REMOVE_SUCCESS = "MEMO_REMOVE_SUCCESS";
export const MEMO_REMOVE_FAILURE = "MEMO_REMOVE_FAILURE";

 

 

memo action 파일 수정하기 (src/actions/memo.js)

import {
    /* CODES */
    MEMO_REMOVE,
    MEMO_REMOVE_SUCCESS,
    MEMO_REMOVE_FAILURE
} from './ActionTypes';

/* MEMO REMOVE */
/* MEMO REMOVE */
export function memoRemoveRequest(id, index) {
    return (dispatch) => {
        /* TO BE IMPLEMENTED */
    };
}

export function memoRemove() {
    return {
        type: MEMO_REMOVE
    };
}

export function memoRemoveSuccess(index) {
    return {
        type: MEMO_REMOVE_SUCCESS,
        index
    };
}

export function memoRemoveFailure(error) {
    return {
        type: MEMO_REMOVE_FAILURE,
        error
    };
}

삭제하는 action creator 의 구조는 수정 할 때와 비슷합니다.

memoRemoveRequest 구현하기 (src/actions/memo.js)

/* MEMO REMOVE */
export function memoRemoveRequest(id, index) {
    return (dispatch) => {
        dispatch(memoRemove());
        
        return axios.delete('/api/memo/' + id)
        .then((response) => {
            dispatch(memoRemoveSuccess(index));
        }).catch((error) => {
            dispatch(memoRemoveFailure(error.response.data.code));
        });
    };
}

 

memo 리듀서 파일 수정하기 (src/actions/memo.js)

const initialState = {
    /* CODES */
    remove: {
        status: 'INIT',
        error: -1
    }
};

export default function memo(state, action) {
    / * CODES */

    switch(action.type) {
        /* CODES */
        /* MEMO REMOVE */
        case types.MEMO_REMOVE:
            return update(state, {
                remove: {
                    status: { $set: 'WAITING' },
                    error: { $set: -1 }
                }
            });
        case types.MEMO_REMOVE_SUCCESS:
            return update(state, {
                remove:{
                    status: { $set: 'SUCCESS' }
                },
                list: {
                    data: { $splice: [[action.index, 1]] }
                }
            });
        case types.MEMO_REMOVE_FAILURE:
            return update(state, {
                remove: {
                    status: { $set: 'FAILURE' },
                    error: { $set: action.error }
                }
            });
        default:
            return state;
    }
}

 

Home 컨테이너 컴포넌트에서 삭제를 위한 state/dispatch 매핑하기 (src/containers/Home.js)

import { 
    /* CODES */
    memoRemoveRequest,
    memoRemoveFromData
} from 'actions/memo';

class Home extends React.Component {
    /* CODES */
}

const mapStateToProps = (state) => {
    return {
        /* CODES */
        removeStatus: state.memo.remove
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        /* CODES */
        memoRemoveRequest: (id, index) => {
            return dispatch(memoRemoveRequest(id, index));
        }
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);

state.memo.remove 를 removeStatus 로 매핑하였으며

memoRemoveRequest 와 memoRemoveFromData 를 actions/memo 에서 불러와서 매핑하였습니다.

그리고, 위 값들을 MemoList 로 전달해주었습니다.

 

handleRemove 구현하기 (src/containers/Home.js)

class Home extends React.Component {
    
    constructor(props) {
        /* CODES */
        this.handleRemove = this.handleRemove.bind(this);
        /* CODES */
    }
    
    /* CODES */
    
    handleRemove(id, index) {
        this.props.memoRemoveRequest(id, index).then(() => {
            if(this.props.removeStatus.status==="SUCCESS") {
                // LOAD MORE MEMO IF THERE IS NO SCROLLBAR
                // 1 SECOND LATER. (ANIMATION TAKES 1SEC)
                setTimeout(() => { 
                    if($("body").height() < $(window).height()) {
                        this.loadOldMemo();
                    }
                }, 1000);
            } else {
                // ERROR
                /*
                    DELETE MEMO: DELETE /api/memo/:id
                    ERROR CODES
                        1: INVALID ID
                        2: NOT LOGGED IN
                        3: NO RESOURCE
                        4: PERMISSION FAILURE
                */
                let errorMessage = [
                    'Something broke',
                    'You are not logged in',
                    'That memo does not exist',
                    'You do not have permission'
                ];
                
                 // NOTIFY ERROR
                let $toastContent = $('<span style="color: #FFB4BA">' + errorMessage[this.props.removeStatus.error - 1] + '</span>');
                Materialize.toast($toastContent, 2000);


                // IF NOT LOGGED IN, REFRESH THE PAGE
                if(this.props.removeStatus.error === 2) {
                    setTimeout(()=> {location.reload(false)}, 2000);
                }
            }
        });
    }
    
    
    /* CODES */
    
    render() {
        const write = (
            <Write
                onPost={this.handlePost}
            />
        );
        
        
        
        return (
            <div className="wrapper">
                { this.props.isLoggedIn ? write : undefined }
                <MemoList data={this.props.memoData} 
                     currentUser={this.props.currentUser}
                     onEdit={this.handleEdit}
                     onRemove={this.handleRemove}
                 />
            </div>
        );
    }
}

/* CODES */

handleRemove 라는 메소드를 만들고, MemoList 로 전달해주었습니다.

삭제작업이 성공하면, 1초뒤 스크롤바가 있는지 없는지 확인하고 없으면 이전 메모들을 로딩합니다.

(나중에 만들 애니메이션이 1초짜리 입니다.)

 

MemoList 컴포넌트에서 위에서 받은 값 Memo 로 전달해주기 (src/components/MemoList.js)

class MemoList extends React.Component {
    
    render() {
        const mapToComponents = data => {
            return data.map((memo, i) => {
                return (<Memo 
                            /* CODES */
                            onRemove={this.props.onRemove}
                />);
            });
        };
        
        return (
            <div>
                {mapToComponents(this.props.data)}
            </div>
        );
    }
}

MemoList.propTypes = {
    /* CODES */
    onRemove: React.PropTypes.func,
};

MemoList.defaultProps = {
    /* CODES */
    onRemove: (id, index) => { 
        console.error('remove function not defined'); 
    }
};

export default MemoList;

위에서 받은 값을 그대로 하위 컴포넌트인 Memo 컴포넌트로 전달해줍니다.

propTypes 와 defaultProps 도 추가해줍니다.

 

Memo 컴포넌트 삭제 기능 구현하기 (src/components/Memo.js)

class Memo extends React.Component {

    constructor(props) {
        /* CODES */
        this.handleRemove = this.handleRemove.bind(this);
    }
    
    /* CODES */
    
    handleRemove() {
        let id = this.props.data._id;
        let index = this.props.index;
        this.props.onRemove(id, index);
    }
    
    /* CODES */
    
    render() {
        const dropDownMenu = (
            <div className="option-button">
                /* CODES */
                    <li><a onClick={this.handleRemove}>Remove</a></li>
                </ul>
            </div>
        );
        
        /* CODES */
    }
    
    
    /* CODES */
}

Memo.propTypes = {
    /* CODES */
    onRemove: React.PropTypes.func
};

Memo.defaultProps = {
    /* CODES */
    onRemove: (id, index) => { 
        console.error('remove function not defined'); 
    }
}

export default Memo;

handleRemove 라는 메소드를 만들어서 삭제 작업을 하도록 하였습니다.
그리고, constructor 에서 해당 메소드를 this 와 바인딩 해줍니다.
삭제 옵션이 클릭 되면 해당 메소드가 실행하도록 설정합니다.

propTypes 와 defaultProps를 추가해줍니다.

여기까지 하셨다면, 삭제 기능이 성공적으로 구현되었을 것 입니다.
하지만, 아직 애니메이션은 구현하지 않았지요. 다음 섹션에서 애니메이션을 구현 해 볼 것입니다.

 

22. 애니메이션

메모가 로딩 될 때와, 삭제 될 때, 우리들의 눈을 즐겁게 하기 위하여 간단한 애니메이션 효과를 줘 보도록 하겠습니다.

보통, React 컴포넌트에 애니메이션 효과를 줄 때는 ReactCSSTransitionGroup 혹은 velocity-react 같은 라이브러리들을 사용합니다.

하지만, 물론 위 라이브러리를 사용 할 필요 없이, 순수 CSS 와 컴포넌트 활용 만으로도 애니메이션을 구현 할 수도 있습니다.

우리는, ReactCSSTransitionGroup을 사용 해 볼거에요. 이 라이브러리의 역할은 특정 컴포넌트가 로딩 될 때, 저희가 지정한 CSS 클래스를 적용시켜주고,

언로딩 될 때, 언로딩을 저희가 지정한 시간만큼 지연시킨후 CSS 클래스를 적용시킨다음에 애니메이션이 끝나고나서 언로딩시킵니다.

우선 이 링크를 클릭해서 관련정보를 한번 훝어보세요.

npm install --save react-addons-css-transition-group

주의 하실점은, ReactCSSTransitionGroup 컴포넌트를 애니메이션을 적용 할 컴포넌트에 안에서 사용 할 게아니라, 해당 컴포넌트의 부모 컴포넌트에서 사용해야합니다.

저희의 경우엔 Memo 컴포넌트에 애니메이션을 주고 싶으니, MemoList 컴포넌트안에서 ReactCSSTransitionGroup 을 사용하고 ReactCSSTransitionGroup 의 자식으로 Memo 컴포넌트들을 렌더링할겁니다.

직접 사용해보기전엔 이해하기가 좀 복잡합니다.. 한번 직접 코딩을 해봅시다!

 

로딩 될 때 애니메이션 효과 주기

<추후 GIF 이미지 첨부>

위 링크를 보시면 CSS 스타일 클래스를 작성 할 때, 애니메이션이 시작 할 때 클래스와 끝날 때 클래스를 지정 할 수 있는데요:

.example-enter {
  opacity: 0.01;
}

.example-enter.example-enter-active {
  opacity: 1;
  transition: opacity 500ms ease-in;
}

시작할때는 투명도가 0이고 끝날때는 1이죠.

이렇게 해도 되고, 원래 CSS 애니메이션을 구현 할 때 처럼 keyframe을 사용해도됩니다.

우리는 keyframe을 사용하도록 하겠습니다 (위 방식처럼 -enter-active 를 사용하는 방식도 간단하고 편합니다 나중에 한번 사용해보세요!)

 

memo-enter스타일 클래스 추가 (src/style.css)

/* ANIMATION */
@-webkit-keyframes memo-enter {
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
    }
}

@keyframes memo-enter {
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
    }
}

.memo-enter {
    -webkit-animation-duration: 2s;
    animation-duration: 2s;
    -webkit-animation-name: memo-enter;
    animation-name: memo-enter;
}

이렇게 2초짜리 애니메이션 클래스를 만들었습니다.

 

MemoList 컴포넌트에서 ReactCSSTransitionGroup 사용 (src/components/MemoList.js)

import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

/* CODES */

    render() {
        const mapToComponents = data => {
            return data.map((memo, i) => {
                return (<Memo 
                            data={memo}
                            ownership={ (memo.writer === this.props.currentUser) }
                            key={memo._id}
                            index={i}
                            onEdit={this.props.onEdit}
                            onRemove={this.props.onRemove}
                            onStar={this.props.onStar}
                            currentUser={this.props.currentUser}
                />);
            });
        };
        
        return (
            <div>
                <ReactCSSTransitionGroup transitionName="memo" 
                                transitionEnterTimeout={2000}>
                    {mapToComponents(this.props.data)}
                </ReactCSSTransitionGroup>
            </div>
        );
    }

위와같이, ReactCSSTransitionGroup을 import 하고, mapToComponents 부분을 ReactCSSTransitionGroup 컴포넌트를 감싸주고, transitionName 설정 및 enter 애니메이션의 대기시간 2000ms (2초) 를 설정합니다.

주의 { maptoComponents.this.props.data) } 를 다른 엘리먼트로 감싸면 애니메이션이 작동하지 않습니다

// this will not work
       return (
            <div>
                <ReactCSSTransitionGroup transitionName="memo" 
                                transitionEnterTimeout={2000}>
                    <div>{mapToComponents(this.props.data)}</div>
                </ReactCSSTransitionGroup>
                
            </div>
        );

삭제 될 때, 애니메이션 효과 주기

이 부분은 위 애니메이션과 비슷하지만, Fade Out 되면서 오른쪽으로 Slide 됩니다.

그 이후, 바로 사라지면 부자연스럽기때문에, height 를 서서히 0으로 조절하면서 자연스럽게 아래있는게 위로 올라오는것처럼 보이게 설정합니다.

 

memo-leave 스타일 클래스 추가 (src/style.css)

body {
    background-color: #ECEFF1;
    overflow-x: hidden; 
}


/ * CODES */

/* ANIMATION */
@-webkit-keyframes memo-leave {
    0% {
      opacity: 1;
      max-height: 1080px;
    }
    50% {
      opacity: 0;
       -webkit-transform: translateX(100px);
    }
    100% {
        max-height: 0px;
    }
}

@keyframes memo-leave {
    0% {
      opacity: 1;
      max-height: 1080px;
    }
    50% {
      opacity: 0;
      transform: translateX(100px);
    }
    100% {
        max-height: 0px;
    }
}

.memo-leave {
    max-height: 0px;
    opacity: 0;
    -webkit-animation-duration: 1s;
    animation-duration: 1s;
    -webkit-animation-name: memo-leave;
    animation-name: memo-leave;
}

메모가 삭제될때 가로 스크롤바가 페이지에 만들어지지 않도록 style의 최상단에 overflow-x 를 hidden으로 설정합니다.

 

ReactCSSTransitionGroup 부분 수정 (src/components/MemoList.js)

        return (
            <div>
                <ReactCSSTransitionGroup transitionName="memo" 
                                transitionEnterTimeout={2000} 
                                transitionLeaveTimeout={1000}>
                    {mapToComponents(this.props.data)}
                </ReactCSSTransitionGroup>
                
            </div>
        );

이 애니메이션은 1초짜리 애니메이션이기에 transitionLeaveTimeout을 1000으로 설정합니다.

 

자, 저희 어플리케이션의 주요 기능들의 구현이 완료 되었습니다.

 

이제 별 주기, 유저 검색 기능이 남아있습니다.

모두들 힘 내세요! 거의 다 끝났어요~

  • ggoban

    Home.js 에 memoRemoveFromData 는 어디서 나타난건가요? action/memo 에는 export 하는게 없는데..