5편에서는 메모를 수정하고 삭제하는 기능을 구현해보았습니다.
이번 편에서는, 별 주기 기능을 구현해보겠습니다.
이 작업은 수정 기능과 매우 비슷하므로 쉽게 구현 하실 수 있을겁니다.
23. 별 주기 기능
서버 – 별주기 API 구현하기 (server/routes/memo.js)
/* TOGGLES STAR OF MEMO: POST /api/memo/star/:id ERROR CODES 1: INVALID ID 2: NOT LOGGED IN 3: NO RESOURCE */ router.post('/star/:id', (req, res) => { // CHECK MEMO ID VALIDITY if(!mongoose.Types.ObjectId.isValid(req.params.id)) { return res.status(400).json({ error: "INVALID ID", code: 1 }); } // CHECK LOGIN STATUS if(typeof req.session.loginInfo === 'undefined') { return res.status(403).json({ error: "NOT LOGGED IN", code: 2 }); } // FIND MEMO Memo.findById(req.params.id, (err, memo) => { if(err) throw err; // MEMO DOES NOT EXIST if(!memo) { return res.status(404).json({ error: "NO RESOURCE", code: 3 }); } // GET INDEX OF USERNAME IN THE ARRAY let index = memo.starred.indexOf(req.session.loginInfo.username); // CHECK WHETHER THE USER ALREADY HAS GIVEN A STAR let hasStarred = (index === -1) ? false : true; if(!hasStarred) { // IF IT DOES NOT EXIST memo.starred.push(req.session.loginInfo.username); } else { // ALREADY starred memo.starred.splice(index, 1); } // SAVE THE MEMO memo.save((err, memo) => { if(err) throw err; res.json({ success: true, 'has_starred': !hasStarred, memo, }); }); }); });
이 API 는, 주어진 :id 값을 가진 메모를 찾고, 해당 메모의 starred 데이터 배열에 자신의 아이디가 존재하지 않는다면, 자신의 아이디를 starred 데이터 배열에 추가하고, 이미 존재한다면, starred 데이터 배열에서 제거합니다.
API 실행이 완료되면, 별을 주었는지 가져갔는지에 대한 값 has_star 과 새로운 메모데이터인 memo 객체를 반환합니다 (업데이트를 할 수 있게끔)
Action Types 추가 (src/actions/ActionTypes.js)
export const MEMO_STAR = "MEMO_STAR"; export const MEMO_STAR_SUCCESS = "MEMO_STAR_SUCCESS"; export const MEMO_STAR_FAILURE = "MEMO_STAR_FAILURE";
memo 액션파일 수정 (src/actions/memo.js)
import { /* CODES */ MEMO_STAR, MEMO_STAR_SUCCESS, MEMO_STAR_FAILURE } from './ActionTypes'; /* CODES */ /* MEMO TOGGLE STAR */ export function memoStarRequest(id, index) { return (dispatch) => { // TO BE IMPLEMENTED }; } export function memoStar() { return { type: MEMO_STAR }; } export function memoStarSuccess(index, memo) { return { type: MEMO_STAR_SUCCESS, index, memo }; } export function memoStarFailure(error) { return{ type: MEMO_STAR_FAILURE, error }; }
memoStarRequest 구현 (src/actions/memo.js)
/* MEMO TOGGLE STAR */ export function memoStarRequest(id, index) { return (dispatch) => { // TO BE IMPLEMENTED return axios.post('/api/memo/star/' + id) .then((response) => { dispatch(memoStarSuccess(index, response.data.memo)); }).catch((error) => { dispatch(memoStarFailure(error.response.data.code)); }); }; }
memo 리듀서 파일 수정 (src/reducers/memo.js)
const initialState = { /* CODES */ star: { status: 'INIT', error: -1 } }; export default function memo(state, action) { /* CODES */ switch(action.type) { /* CODES */ case types.MEMO_STAR: return update(state, { star: { status: { $set: 'WAITING' }, error: { $set: -1 } } }); case types.MEMO_STAR_SUCCESS: return update(state, { star: { status: { $set: 'SUCCESS' } }, list: { data: { [action.index]: { $set: action.memo } } } }); case types.MEMO_STAR_FAILURE: return update(state, { star: { status: { $set: 'FAILURE' }, error: { $set: action.error } } }); default: return state; } }
자, 이렇게 리듀서 까지 완성을 하였습니다.
앞으로 뭘 해야 할지는, 대충 감이 잡히지 않나요?
Home 컨테이너 컴포넌트 memoStarRequest, starStatus 매핑하기 (src/containers/Home.js)
import { /* CODES */ memoStarRequest } from 'actions/memo'; class Home extends React.Component { /* CODES */ } const mapStateToProps = (state) => { return { /* CODES */ starStatus: state.memo.star }; }; const mapDispatchToProps = (dispatch) => { return { /* CODES */ memoStarRequest: (id, index) => { return dispatch(memoStarRequest(id, index)); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Home);
handleStar 구현하기 (src/containers/Home.js)
class Home extends React.Component { constructor(props) { /*CODES*/ this.handleStar = this.handleStar.bind(this); /*CODES*/ } handleStar(id, index) { this.props.memoStarRequest(id, index).then( () => { if(this.props.starStatus.status !== 'SUCCESS') { /* TOGGLES STAR OF MEMO: POST /api/memo/star/:id ERROR CODES 1: INVALID ID 2: NOT LOGGED IN 3: NO RESOURCE */ let errorMessage= [ 'Something broke', 'You are not logged in', 'That memo does not exist' ]; // NOTIFY ERROR let $toastContent = $('<span style="color: #FFB4BA">' + errorMessage[this.props.starStatus.error - 1] + '</span>'); Materialize.toast($toastContent, 2000); // IF NOT LOGGED IN, REFRESH THE PAGE if(this.props.starStatus.error === 2) { setTimeout(()=> {location.reload(false)}, 2000); } } } ); } render() { /* CODES */ return ( <div className="wrapper"> { this.props.isLoggedIn ? write : undefined } <MemoList data={this.props.memoData} /* CODES */ onStar={this.handleStar} /> </div> ); } /* CODES */
handleStar를 구현하고 MemoList로 전달해줍니다
MemoList 컴포넌트 수정하기 (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 /* CODES */ onStar={this.props.onStar} currentUser={this.props.currentUser} />); }); }; return ( <div> {mapToComponents(this.props.data)} </div> ); } } MemoList.propTypes = { /* CODES */ onStar: React.PropTypes.func, }; MemoList.defaultProps = { /* CODES */ onStar: (id, index) => { console.error('star function not defined'); } }; export default MemoList;
onStar 과 starStatus 를 그대로 Memo 컴포넌트로 전달해주었습니다.
추가적으로, currentUser props 도 전달되었습니다 (유저가 각 메모를 star 한지 안한지 여부를 확인하는데 사용됩니다)
propTypes 와 defaultProps 도 수정되었습니다.
Memo 컴포넌트 handleStar 구현 (src/components/Memo.js)
class Memo extends React.Component { constructor(props) { /* CODES */ this.handleStar = this.handleStar.bind(this); } /* CODES */ handleStar() { let id = this.props.data._id; let index = this.props.index; this.props.onStar(id, index); } /* CODES */ render() { /* CODES */ // IF IT IS STARRED ( CHECKS WHETHER THE NICKNAME EXISTS IN THE ARRAY ) // RETURN STYLE THAT HAS A YELLOW COLOR let starStyle = (this.props.data.starred.indexOf(this.props.currentUser) > -1) ? { color: '#ff9980' } : {} ; const memoView = ( <div className="card"> /* CODES */ <div className="footer"> <i className="material-icons log-footer-icon star icon-button" style={starStyle} onClick={this.handleStar}>star</i> <span className="star-count">{this.props.data.starred.length}</span> </div> </div> ); /* CODES */ } /* CODES */ } Memo.propTypes = { /* CODES */ onStar: React.PropTypes.func, starStatus: React.PropTypes.object, currentUser: React.PropTypes.string }; Memo.defaultProps = { /* CODES */ onStar: (id, index) => { console.error('star function not defined'); }, starStatus: {}, currentUser: '' } export default Memo;
onStar, currentUser 을 propTypes 와 defaultProps 에 추가하였습니다.
handleStar 메소드를 만든 후, 생성자에서 this 와 binding 해주었습니다.
해당 메모가 유저가 star을 한지 안한지 확인하려면, 배열의 indexOf 메소드를 통하여 starred 데이터에 로그인유저의 username이 적혀있는지 확인하면됩니다.
만약에 해당 메모가 star 한 메모라면, inline style 을 통하여 노란색 스타일을 반환하고, 그렇지 않으면 비어있는 스타일을 반환합니다.
그리고, star 버튼에 스타일을 추가해주고, onClick에 handleStar 메소드를 실행하도록합니다.
이제 메모에 별을 줄 수 있게 되었습니다 !
이제 앞으로 유저검색기능이 남았는데요, 그걸 구현하기 전에, 먼저 성능을 최적화해보겠습니다.
24. 성능 최적화
지금 상태에서, 페이지를 더 이상 메모가 나타나지 않을 때 까지 계속 스크롤을 해보세요.
맨 끝에 도달 했다면, 조금 위로 갔다, 아래로갔다를 반복해보세요. 크게 눈에 띄이진 않지만 렉이 있는걸 발견 할 수 있습니다 (CPU 성능이 좋으면 거의 안느껴집니다. 그래도 스크롤이 살짝 끊기는 느낌이 있을겁니다)
도대체 무슨 일이 일어나고 있을까요? 한번 Memo 컴포넌트의 render() 메소드의 최상단에 console.log(this.props.data)
를 입력하고 개발자도구를 키고 웹어플리케이션을 사용해보세요.
렌더링이 엄청나게 많이 진행되고있다는 사실을 파악 할 수 있을겁니다.
스크롤링 해보세요. 변경되지 않은 이미 렌더링 한 컴포넌트에서 render() 메소드가 실행되죠?
한번 수정을 해보세요. 수정하지 않은 컴포넌트들에서도 render() 메소드가 실행되고 있습니다.
문제를 발견 한 이상, 우리는 이 문제를 해결해야합니다.
이 문제가 발생하는 이유는, redux store 에 API 관련 status 가 업데이트 될 때도 컴포넌트가 업데이트되기 때문입니다.
(컴포넌트가 다시 렌더링 되지는 않지만, render 메소드가 실행 되고, 변화가 있나 없나 계산을 하겠지요)
이 문제점을 해결하기 위해선 먼저 MemoList 컴포넌트를 수정해야합니다.
MemoList 컴포넌트의 렌더링 메소드에선 데이터를 컴포넌트로 매핑하는 나름 ‘복잡한’ 작업을 하는데, 렌더링 메소드가 필요 할 떄만 실행되도록 해야겠죠?
MemoList 컴포넌트에도 render() 메소드의 상단에 console.log('MemoList render method executed');
라는 코드를 입력하세요
(정확한 테스팅을 위해서 아까 Memo 컴포넌트에 적은 console.log
는 잠시 주석처리해주세요)
문제점 1: 5초마다 새 게시물 불러오기 시도를 할 때, 새로운 게시물이 없더라도 렌더링 메소드가 실행 된다, 그것도 두번.
이유: Home 컴포넌트의 listStatus.status 가 ‘WAITING’ 로 변한다음, ‘SUCCESS’ 로 변하면서 두번
문제점 2: 스크롤 해서 이전 게시물을 불러오려고 시도 할 때, 렌더링 메소드가 4번이나 실행 된다.
이유: Home 컴포넌트의 무한스크롤링을 위한 state.loadingStatus 이 토글 되면서 한번, listStatus 때문에 두번, 마지막으로 Home 컴포넌트에서 새 데이터를 갖다줘서 한번
MemoList 컴포넌트가 전달 받은 props 가 변경 될 때 render 메소드가 트리거 됩니다.
그리고, 전달받은 props 에 변화가 없더라도, 패런트 컴포넌트 (Home) 에서 render 메소드가 실행 될 때도, render 메소드가 트리거 됩니다.
이를 막아주려면 LifeCycle API 인 shouldComponentUpdate 를 사용하면됩니다.
MemoList 컴포넌트에 shouldComponentUpdate 메소드 추가 (src/components/MemoList.js)
shouldComponentUpdate(nextProps, nextState) { let update = JSON.stringify(this.props) !== JSON.stringify(nextProps); return update; }
위와 같이, 전달받은 props 값이 달라질때만 render() 메소드를 실행하도록 설정하면 위 문제들이 완화됩니다.
자, 다시 아까 Memo 컴포넌트에 주석처리했던 console.log
를 주석해제해보세요.
그 후, 다시 테스팅을 해보시면, 스크롤링을 할 때 아까처럼 콘솔에 미친듯이 출력되지는 않지만, 변동이 없는 Memo 들도 render() 메소드가 실행되고 있죠.
한번 이 컴포넌트도 shouldComponentUpdate 메소드를 추가해봅시다.
Memo 컴포넌트에 shouldComponentUpdate 메소드 추가 (src/components/Memo.js)
shouldComponentUpdate(nextProps, nextState) { let current = { props: this.props, state: this.state }; let next = { props: nextProps, state: nextState }; let update = JSON.stringify(current) !== JSON.stringify(next); return update; }
이번엔 state 도 비교하게 하였습니다. 이렇게 하면, 새 메모가 추가 될 때, 새로운 메모들만 render 메소드가 실행됩니다.
이렇게 추가해주면, CPU 자원이 낭비되는 문제를 해결 할 있습니다.
작업이 끝났다면 render 메소드에서 입력한 console.log
를 지우세요.
한번 맨끝까지 스크롤을 한 다음, 스크롤을 위로 아래로 왔다갔다 해보세요. 어때요, 아까보다 스크롤이 부드럽지 않나요?
축하드립니다! 저희 프로젝트의 핵심 기능을 모두 완성하였고, 마무리도 어느정도 끝났습니다!
이제 남은 유저 검색기능은, 다음 편에서 제가 핵심 코드들만 제공해드릴테니 여러분이 직접 구현해보세요~