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


6편에선 저희 프로젝트의 핵심기능을 모두 완료하였고, 성능을 최적화하는 방법도 가볍게 배웠습니다.

이번 편에서는 (마지막 편입니다! 야호~) 유저 검색기능을 구현해보겠습니다.

강좌 후반부에서는 제가 핵심코드만 제공해드릴테니, 한번 여러분이 직접 구현해보세요.

 

25. 유저 검색기능 구현하기 – 담벼락

우리가 구현 할 기능은 이렇게 생겼습니다:

search

CodePen Link: http://codepen.io/velopert/pen/KrRmjQ

특정유저의 메모 불러오는 API 만들기 (src/routes/memo.js)

/*
    READ MEMO OF A USER: GET /api/memo/:username
*/
router.get('/:username', (req, res) => {
    Memo.find({writer: req.params.username})
    .sort({"_id": -1})
    .limit(6)
    .exec((err, memos) => {
        if(err) throw err;
        res.json(memos);
    });
});


/*
    READ ADDITIONAL (OLD/NEW) MEMO OF A USER: GET /api/memo/:username/:listType/:id
*/
router.get('/:username/:listType/:id', (req, res) => {
    let listType = req.params.listType;
    let id = req.params.id;

    // CHECK LIST TYPE VALIDITY
    if(listType !== 'old' && listType !== 'new') {
        return res.status(400).json({
            error: "INVALID LISTTYPE",
            code: 1
        });
    }
    
    // CHECK MEMO ID VALIDITY
    if(!mongoose.Types.ObjectId.isValid(id)) {
        return res.status(400).json({
            error: "INVALID ID",
            code: 2
        });
    }
    
    let objId = new mongoose.Types.ObjectId(req.params.id);
    
    if(listType === 'new') {
        // GET NEWER MEMO
        Memo.find({ writer: req.params.username, _id: { $gt: objId }})
        .sort({_id: -1})
        .limit(6)
        .exec((err, memos) => {
            if(err) throw err;
            return res.json(memos);
        });
    } else {
        // GET OLDER MEMO
        Memo.find({ writer: req.params.username, _id: { $lt: objId }})
        .sort({_id: -1})
        .limit(6)
        .exec((err, memos) => {
            if(err) throw err;
            return res.json(memos);
        });
    }
});

기존에 구현한 메모 읽기 API 를 조금 수정하기만했습니다 ( find 부분에 { writer: req.params.username } 추가 )

 

Wall 컨테이너 컴포넌트 만들기 (src/containers/Wall.js)

import React from 'react';

class Wall extends React.Component {
    render() {
        return (
            <div>{this.props.params.username}</div>
        );
    }
}

export default Wall;

(Wall 은 페이스북의 담벼락처럼 특정 유저의 게시글만 보여줍니다)

클라이언트 라우팅의 params 는 위와같이 this.props.params.___ 으로 읽어옵니다.

 

컨테이너 인덱스에 Wall 추가 (src/containers/index.js)

import App from './App';
import Home from './Home';
import Login from './Login';
import Register from './Register';
import Wall from './Wall';


export { App, Home, Login, Register, Wall };

 

Index에서 /wall/:username 라우트 추가 (src/index.js)

/* CODES */

// Container Components
import { App, Home, Login, Register, Wall } from 'containers';

/* CODES */
ReactDOM.render(
    <Provider store={store}>
        <Router history={browserHistory}>
            <Route path="/" component={App}>
                /* CODES */
                <Route path="wall/:username" component={Wall}/>
            </Route>
        </Router>
    </Provider>, rootElement
);

/wall/test 라우트에 브라우저로 접속해보세요. 화면에 test 가 보이나요?

이제 저희가 기존에 작성한 actions/memo.js의 memoListRequest 에서 특정 유저의 메모를 불러올 수 있도록 수정해보겠습니다.

 

memoListRequest 수정 (src/actions/memo.js)

export function memoListRequest(isInitial, listType, id, username) {
    return (dispatch) => {
        // to be implemented
        dispatch(memoList());

        let url = '/api/memo';

        if(typeof username === "undefined") {
            // username not given, load public memo
            url = isInitial ? url : `${url}/${listType}/${id}`;
            // or url + '/' + listType + '/' +  id
        } else {
            // load memos of a user
            url = isInitial ? `${url}/${username}` : `${url}/${username}/${listType}/${id}`;
        }

        return axios.get(url)
        .then((response) => {
            dispatch(memoListSuccess(response.data, isInitial, listType));
        }).catch((error) => {
            dispatch(memoListFailure());
        });

    };
}

username 이 주어진다면 요청 할 URL 을 다르게 설정합니다.

다음은 Home 컴포넌트에서 memoListRequest 를 사용 할 때, username 을 parameter 로 전달하도록하겠습니다.

 

Home 컴포넌트 수정 (src/containers/Home.js)

/* CODES */
class Home extends React.Component {
    
    /* CODES */
    
    componentDidMount() {
        /* CODES */
        
       
        // DO THE INITIAL LOADING
        this.props.memoListRequest(true, undefined, undefined, this.props.username).then(
            () => {
                // LOAD MEMO UNTIL SCROLLABLE
                loadUntilScrollable();
                // BEGIN NEW MEMO LOADING LOOP
                loadMemoLoop();
            }
        );
        
        
        /* CODES */
                
                
    }
    
    /* CODES */
    
    loadNewMemo() {
        /* CODES */
        
        // IF PAGE IS EMPTY, DO THE INITIAL LOADING
        if(this.props.memoData.length === 0 )
            return this.props.memoListRequest(true, undefined, undefined, this.props.username);
            
        return this.props.memoListRequest(false, 'new', this.props.memoData[0]._id, this.props.username);
    }
    
    loadOldMemo() {
        /* CODES */
        
        // GET ID OF THE MEMO AT THE BOTTOM
        let lastId = this.props.memoData[this.props.memoData.length - 1]._id;
        
        // START REQUEST
        return this.props.memoListRequest(false, 'old', lastId, this.props.username).then(() => {
            // IF IT IS LAST PAGE, NOTIFY
            if(this.props.isLast) {
                Materialize.toast('You are reading the last page', 2000);
            }
        });
    }
    
    /* CODES */
}

/* CODES */

Home.PropTypes = {
    username: React.PropTypes.string
};

Home.defaultProps = {
    username: undefined
};

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

username 값은 Wall 컴포넌트에서 props 로 전달하도록 할 것 입니다.

username 값이 설정되어있지 않을때는, 일반 로딩을 진행합니다.

 

Wall 컨테이너 컴포넌트에서 Home 컴포넌트 불러와서 렌더링 하기 (src/containers/Wall.js)

import React from 'react';
import { Home } from 'containers';

class Wall extends React.Component {
    render() {
        return (
            <Home username={this.props.params.username}></Home>
        );
    }
}

export default Wall;

여기까지하면, /wall/:username 으로 직접 링크를 쳐서 들어갔을때, 초기 로딩은 잘 됩니다.

그러나, 그 상태에서 나중에 다른사람을 검색해서 또 들어갔을때 (Link 컴포넌트를 통하여 라우팅 했을 경우), 컴포넌트가 unmount 되고 다시 mount 되는게 아니라, update 되기 때문에, 저희가 원하는대로 작동하지 않습니다. 따라서, componentDidUpdate LifeCycle API 를 통하여 username 이 변한것을 감지하고, 변했을 시, componentWillUnmount 와 componentDidMount 메소드를 임의로 실행해주세요 – 이렇게 한다고 다시 Mount 되는것은 아니지만, unmount 될 때와 mount 될 때 저희가 실행하도록 지정한 코드들을 실행 할 수 있습니다.

 

Home 컨테이너 컴포넌트 오류 미리 해결하기 (src/containers/Home.js)

    componentDidMount() {
        /* CODES */
        
       
        // DO THE INITIAL LOADING
        this.props.memoListRequest(true, undefined, undefined, this.props.username).then(
            () => {
                // LOAD MEMO UNTIL SCROLLABLE
                setTimeout(loadUntilScrollable, 1000);
                // BEGIN NEW MEMO LOADING LOOP
                loadMemoLoop();
            }
        );
        
        
        /* CODES */                
                
    }
    
    componentDidUpdate(prevProps, prevState) {
        if(this.props.username !== prevProps.username) {
            this.componentWillUnmount();
            this.componentDidMount();
        }
    }

componentDidMount 부분에 loadUntilScrollable 메소드를, 메모 초기 로딩 후, 1초뒤 실행하도록 하였습니다.

(나중에 담벼락의 유저가 변경되면서, 메모가 사라질 때도 애니메이션이 적용됩니다. 애니메이션이 1초 걸리기때문에, 1초뒤에 스크롤바가 있는지 없는지 확인하고 추가로딩 여부를 정합니다.

이 부분을 미리 해결해놓지 않으면, 나중에 메모를 처음 로딩 하는 부분에서, 최종적으론 스크롤바가 없어도, 스크롤바가 있는것으로 인식 할 수도 있습니다)

 

여기까지 완료하였으면, 담벼락의 헤더를 간단하게 만들어봅시다:

ss

만약에 메모가 존재하지 않는다면 에러메시지를 띄우도록 합시다.

 

 담벼락 헤더를 위한 스타일 추가 (src/style.css)

.empty-page {
    font-size: 30px;
    text-align: center;
    color: #4D4D4D;
}

.wall-info {
    font-size: 30px;
    text-align: center;
}

 

담벼락 헤더 렌더링 (src/containers/Home.js)

    render() {
        /* CODES */
        
        const emptyView = (
            <div className="container">
                <div className="empty-page">
                    <b>{this.props.username}</b> isn't registered or hasn't written any memo
                </div>
            </div>
        );
        
        const wallHeader = (
            <div>
                <div className="container wall-info">
                    <div className="card wall-info blue lighten-2 white-text">
                        <div className="card-content">
                            {this.props.username}
                        </div>
                    </div>
                </div>
                { this.props.memoData.length === 0 ? emptyView : undefined }
            </div>
        );
        
        
        
        return (
            <div className="wrapper">
                { typeof this.props.username !== "undefined" ? wallHeader : undefined }
                { this.props.isLoggedIn && typeof this.props.username === "undefined" ? write : undefined }
                /* CODES */
                 />
            </div>
        );
    }

이렇게 하면, 헤더가 보이긴하는데, 로딩 하는 과정에서 배열의 크기가 0이기 때문에, 아주 짧은 시간동안 메모가 없다는 메시지가 뜹니다.

이 부분을 고치려면 state 를통하여 initiallyLoaded 라는 값을 만들어서, 이 값이 true 일때만 해당 메시지를 보여주도록 설정을 합시다.

컴포넌트가 생성 되었을 때 값은 false 이고,

첫 불러오기 요청이 끝났을때 값을 true로 설정하도록 합시다. 그리고, component가 unmount 될 때는 false로 다시 초기화 합니다.

+ 추가적으로, Wall 에서는 Write 컴포넌트가 보여지지 않도록 했습니다.

 

initiallyLoaded state 만들기 (src/containers/Home.js)

    constructor(props) {
        super(props);
        
        /* CODES */
       
        
        this.state = {
            loadingState: false,
            initiallyLoaded: false
        };
    }

    componentDidMount() {
        /* CODES */
        
       
        // DO THE INITIAL LOADING
        this.props.memoListRequest(true, undefined, undefined, this.props.username).then(
            () => {
                /* CODES */
                this.setState({
                    initiallyLoaded: true
                });
            }
        );
        /* CODES */
    }

    componentWillUnmount() {

        /* CODE */
        
        this.setState({
            initiallyLoaded: false
        });
    }
        const wallHeader = (
            <div>
                <div className="container wall-info">
                    <div className="card wall-info blue lighten-2 white-text">
                        <div className="card-content">
                            {this.props.username}
                        </div>
                    </div>
                </div>
                { this.props.memoData.length === 0 && this.state.initiallyLoaded ? emptyView : undefined }
            </div>
        );

this.state.initiallyLoaded 가 true 일때만 해당 메시지가 보여지도록 합시다.

 

담벼락 라우팅을 완료하였습니다. 마무리로, 메모의 유저네임을 클릭하면 해당 유저의 담벼락으로 이동하도록 해봅시다.

 

Memo 컴포넌트 유저네임 클릭시 담벼락으로 이동 (src/components/Memo.js)

        const memoView = (
            <div className="card">
                <div className="info">
                    <Link to={`/wall/${this.props.data.writer}`} className="username">{this.props.data.writer}</Link> wrote a log · <TimeAgo date={this.props.data.date.created}/> 
                    /* CODES */
        );

 

26. 유저 검색기능 구현하기 – 검색창

유저 검색 API 만들기: GET /api/account/search/:username (server/routes/account.js)

/* 
    SEARCH USER: GET /api/account/search/:username
*/
router.get('/search/:username', (req, res) => {
    // SEARCH USERNAMES THAT STARTS WITH GIVEN KEYWORD USING REGEX
    var re = new RegExp('^' + req.params.username);
    Account.find({username: {$regex: re}}, {_id: false, username: true})
    .limit(5)
    .sort({username: 1})
    .exec((err, accounts) => {
        if(err) throw err;
        res.json(accounts);
    });
});

// EMPTY SEARCH REQUEST: GET /api/account/search
router.get('/search', (req, res) => {
    res.json([]);
});

정규식을 사용하여 주어진 값 :username 으로 시작하는 유저네임 5개를 리스트합니다.

데이터 부분엔 username 만 보여지도록합니다.

 

이 API 는 다음과같은 결과값을 가져다줍니다.

// REQUEST: GET /api/account/search/t
[{"username":"tester"},{"username":"testset"}]

비어있는 키워드를 전했을때를 대비하여 키워드가 없는 /search 도 추가합니다.

키워드가 공백이라면, 비어있는 배열을 리턴합니다.

이 부분을 클라이언트사이드에서 공백일때 검색을하지 않도록 설정을 해도 되지만, 이렇게 하는편이 더 간편합니다.

 

스타일 추가

/* SEARCH */
.search-screen {
    position: fixed;
    overflow-y: none;
    left: 0px;
    top: 0px;
    height:100%;
    width:100%;
    background-color: rgba(0, 0, 0, 0.70);
    z-index: 99;
}

.search-screen input {
    text-align: center;
    font-size: 50px;
    line-height: 80px;
    margin-top: 10vw;
    height: 80px;
    font-weight: 200;
}

.search-screen .btn {
    margin-top: 14px;
    margin-right: 20px;
}

ul.search-results {
    text-align: center;
    font-size: 30px;
    margin-top: 0px;
}

.search-results a {
    padding: 10px;
    display: block;
    color: white;
}

.search-results a + a {
    border-top: 1px solid #5F5F5F;
}

.search-results a:hover {
    background-color: rgba(255, 255, 255, 0.10);
}

Search 컴포넌트 생성

import React from 'react';
import { browserHistory, Link } from 'react-router';

class Search extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            keyword: ''
        };

        this.handleClose = this.handleClose.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.handleSearch = this.handleSearch.bind(this);
        this.handlekeyDown = this.handleKeyDown.bind(this);

        // LISTEN ESC KEY, CLOSE IF PRESSED
        const listenEscKey = (evt) => {
            evt = evt || window.event;
            if (evt.keyCode == 27) {
                this.handleClose();
            }
        };

        document.onkeydown = listenEscKey;

    }

    handleClose() {
        this.handleSearch('');
        document.onkeydown = null;
        this.props.onClose();
    }

    handleChange(e) {
        this.setState({
            keyword: e.target.value
        });
        this.handleSearch(e.target.value);
    }

    handleSearch(keyword) {
        // TO BE IMPLEMENTED
    }

    handleKeyDown(e) {
        // IF PRESSED ENTER, TRIGGER TO NAVIGATE TO THE FIRST USER SHOWN
                if(e.keyCode === 13) {
                    if(this.props.usernames.length > 0) {
                        browserHistory.push('/wall/' + this.props.usernames[0].username);
                        this.handleClose();
                    }
                }
    }

    render() {

        const mapDataToLinks = (data) => {
            // IMPLEMENT: map data array to array of Link components
            // create Links to '/wall/:username'
        };

        return (
            <div className="search-screen white-text">
                <div className="right">
                    <a className="waves-effect waves-light btn red lighten-1"
                        onClick={this.handleClose}>CLOSE</a>
                </div>
                <div className="container">
                    <input placeholder="Search a user"
                            value={this.state.keyword}
                            onChange={this.handleChange}
                            onKeyDown={this.handleKeyDown}></input>
                    <ul className="search-results">
                        { mapDataToLinks(this.props.usernames) }
                    </ul>

                </div>
            </div>
        );
    }
}

Search.propTypes = {
    onClose: React.PropTypes.func,
    onSearch: React.PropTypes.func,
    usernames: React.PropTypes.array
};

Search.defaultProps = {
    onClose: () => {
        console.error('onClose not defined');
    },
    onSearch: () => {
        console.error('onSearch not defined');
    },
    usernames: []
};

export default Search;

react-router 의 browserHistory 와 Link가 import 되었는데요, browserHistory를 불러온 이유는 Link 를 클릭하지 않아도, input 박스에서 엔터를 누르면 맨 위에 보여지는 아이디를 검색하게 하도록 하기 위함입니다.

이 컴포넌트의 prop 으로는 자신을 종료하는 함수 onClose 와 검색을하는 함수 onSearch 가 있습니다.

handle 메소드들은 총 4개인데요, onClose 와 onSearch 를 사용하는 handleClose, handleSearch 메소드와,

input 박스 내용이 수정되면 실행될 handleChange 메소드

input 박스에서 enter 키가 눌려지면 맨 위에있는 유저네임의 ‘담벼락’ 으로 이동하기 위한 handleKeyDown 메소드 입니다.

이 메소드들을 constructor 에서 bind 해주시구요. 그 하단에는 ESC 를 누르면 자신이 종료되도록 document 에 key를 listen 하도록 하였습니다.

그 하단에는 인풋박스 뿐만아니라, 페이지 전체에서 ESC 키를 누르면 종료되도록 설정하였습니다.

종료될때는, document.onkeydown 을 null로 지정하여 리스너를 해제합니다.

또한, 종료될때 공백을 검색함으로서, 검색목록을 비웁니다 (아니면 검색목록을 비우는 액션을 따로 만드세요)

직접 구현 할 부분: mapDataToLinks, (handleSearch 는 나중에 구현)

주의: usernames 은 [ { username: 'a' }, { username: 'b' } ] 이런형식의 배열입니다. 따라서, mapDataToLinks 부분에서 user.username 이런형식으로 이름을 가져와야합니다. 그리고 key 값도 설정하는거 잊지마세요~

추가적으로, Link 컴포넌트를 클릭시 handleClose가 실행되게 하세요.

 

컴포넌트 인덱스에 Search 컴포넌트 추가 (src/components/index.js)

import Header from './Header';
import Authentication from './Authentication';
import Write from './Write';
import Memo from './Memo';
import MemoList from './MemoList';
import Search from './Search';


export { Header, Authentication, Write, Memo, MemoList, Search };

 

Header 에서 Search 컴포넌트 로딩 및 검색버튼 누르면 보여지도록 설정 (src/components/Header.js)

import React from 'react';
import { Link } from 'react-router';
import { Search } from 'components';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

class Header extends React.Component {

    constructor(props) {
        super(props);

        /* IMPLEMENT: CREATE A SEARCH STATUS */
    }

    /* IMPLEMENT: CREATE toggleSearch METHOD THAT TOGGLES THE SEARCH STATE */

    render() {

        const loginButton = (
            <li>
                <Link to="/login"><i className="material-icons">vpn_key</i></Link>
            </li>
        );

        const logoutButton = (
            <li>
                <a onClick={this.props.onLogout}><i className="material-icons">lock_open</i></a>
            </li>
        );

        return (
            <div>
                <nav>
                    <div className="nav-wrapper blue darken-1">
                        <Link to="/" className="brand-logo center">MEMOPAD</Link>

                        <ul>
                            <li><a><i className="material-icons">search</i></a></li>
                        </ul>

                        <div className="right">
                            <ul>
                                { this.props.isLoggedIn ? logoutButton : loginButton }
                            </ul>
                        </div>
                    </div>
                </nav>
                    <ReactCSSTransitionGroup transitionName="search" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
                        { /* IMPLEMENT: SHOW SEARCH WHEN SEARCH STATUS IS TRUE */}
                    </ReactCSSTransitionGroup>
            </div>
        );
    }
}

Header.propTypes = {
    isLoggedIn: React.PropTypes.bool,
    onLogout: React.PropTypes.func
};

Header.defaultProps = {
    isLoggedIn: false,
    onLogout: () => { console.error("logout function not defined");}
};

export default Header;

직접 구현 할 부분:

  • search 라는 status 를 만들어서 이 값이 true 면 검색창을 보여주고 false 면 숨기도록 하세요
  • toggleSearch 라는 메소드를 만들어서 search status 값을 true -> false 혹은 false -> true 로 변경하게하세요
  • 검색버튼이 onClick 되면 toggleSearch 를 실행하도록 하세요
  • Search 컴포넌트에 onClose props 로 toggleSearch를 전달하세요

 

검색창 애니메이션 스타일 (src/style.css)

@-webkit-keyframes search-enter {
    0% {
      opacity: 0;
      height: 0%;
    }
    100% {
      opacity: 1;
      height: 100%;
    }
}

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

.search-enter {
    -webkit-animation-duration: 0.3s;
    animation-duration: 0.3s;
    -webkit-animation-name: search-enter;
    animation-name: search-enter;
}

@-webkit-keyframes search-leave {
    0% {
      opacity: 1;
      height: 100%;
    }
    100% {
      opacity: 0;
      height: 0%;
    }
}

@keyframes search-leave {
    0% {
      opacity: 1;
      height: 100%;
    }
    100% {
      opacity: 0;
      height: 0%;
    }
}

.search-leave {
    -webkit-animation-duration: 0.3s;
    animation-duration: 0.3s;
    -webkit-animation-name: search-leave;
    animation-name: search-leave;
}

컴포넌트쪽 렌더링 하는 부분을 완료하였다면,

나머지 Redux 부분도, 여러분들이 직접 해보세요.

 

Redux

  • ActionType: SEARCH, SEARCH_SUCCESS, SEARCH_FAILURE 추가
  • src/actions/search.js 파일 생성하여 thunk, action creators – searchRequest, search, searchSuccess, searchFailure 추가 및 구현
  • src/reducers/search.js 파일 생성하여 리듀서 구현
  • src/reducers/index.js 인덱스 리듀서에서 search 리듀서 추가
    이 리듀서의 initialState 는 아래에 있습니다.. search 요청을하고 그 값을 $set 을 통하여 그대로 usernames 로 설정하세요.
    에러가 났을 경우엔 usernames 를 빈 배열로 설정하세요.
const initialState = {
    status: 'INIT',
    usernames: []
};
  • src/containers/App.js searchRequest, state.search.usernames 매핑 및 handleSearch 메소드 구현
    그리고 handleSearch 메소드와 searchStatus 를 Header 컴포넌트로 전달 (onSearch, usernames)
  • src/components/Header.js 전달받은 onSearch 와 searchStatus 를 Search 컴포넌트로 전달 (onSearch, usernames)
  • src/components/Search.js 전달받은 onSearch 와 usernames 를 사용하여 Search 컴포넌트 완성 (handleSearch 구현)

 

저희 프로젝트가 완성되었습니다! 이제 프로덕션 빌드를 만드는 방법을 알아보고, 강의를 마무리하도록 하겠습니다.

27. 빌드

webpack.config.js 수정 (src/webpack.config.js)

var webpack = require('webpack');

module.exports = {
    /* CODES */
    
    plugins:[
        new webpack.DefinePlugin({
          'process.env':{
            'NODE_ENV': JSON.stringify('production')
          }
        }),
        new webpack.optimize.UglifyJsPlugin({
          compress:{
            warnings: true
          }
        })
    ]

};

webpack 을 require 하고 plugins: 부분에 위 처럼 설정을 하면 코드가 production 환경으로 컴파일됩니다 (일부 경고 출력 사라짐)

또한, 코드가 Uglify 되어 (불필요한 공백 제거) 코드의 용량이 줄어듭니다.

 

빌드 스크립트 실행

npm run build
npm run start # run the server in production mode

 

28. IE 호환하기

Promise 는 구버전의 브라우저에서 지원하지 않으므로 (링크) babel-polyfill (ES6 기능들을 호환시켜줌) 을 통하여 이를 호환시켜야합니다.

npm install --save babel-polyfill

webpack.config.js / webpack.dev.config.js 수정

var webpack = require('webpack');
var path = require('path');

module.exports = {

    entry: [
        'babel-polyfill',
        './src/index.js',

babel-polyfill 을 entry로 추가해주면 됩니다.

 

Express API Cache-Control 설정 (server/routes/index.js)

크롬에선 문제가 없는데 IE에선 캐시 컨트롤을 이상하게 하게 되면서 새 메모를 불러오지 못하는 버그가있습니다.

해당 코드를 서버의 인덱스 라우터에 추가하여 오류를 해결하세요.

import express from 'express';
import account from './account';
import memo from './memo';

const router = express.Router();

router.use('/*', (req, res, next) => {
    res.setHeader("Expires", "-1");
    res.setHeader("Cache-Control", "must-revalidate, private");
    next();
});

router.use('/account', account);
router.use('/memo', memo);

export default router;

 

Feelzgoodman

끝!

 

어땠나요? 재미있었나요? (그랬었으면 좋겠습니다..)

강의 준비를 조금 서둘러서 했기 때문에, 아마 제가 실수하거나 오타를 낸부분도 있을것입니다.

오탈자 발견 및 질문은 덧글 및 이메일 (public.velopert@gmail.com) 로 해주세요

 

추가적으로, 이 강좌에서 action 부분을 작성할때 비슷한 패턴이 계속 반복됐었죠? 이를 미들웨어를 사용하여 좀 더 편하게 할 수 있는 방법을 조만간 블로그에 포스팅하도록 하겠습니다.

프로젝트를 완성하고 나니, Reducer 부분의 데이터 구조는, 좀 더 깔끔하게 할 수 있었을 것 같네요. 특히, request status 를 한 객체에 집어넣고 사용했었으면 좀 더 깔끔했었을것같습니다. 그러나.. 강좌를 이미 작성했기에.. 수정하기엔 너무 번거로울것 같아서 그대로 뒀습니다.

Redux 의 개발자인 Dan 도 강조한 부분이지만, 리듀서의 데이터구조는 따로 디자인방식이 정해져있지않고 개발자(여러분)가 가장 편한 방식으로 설계를 하시면 됩니다.

이 프로젝트를 서버렌더링 하는방법, 또한 블로그에 조만간 작성을 하도록 하겠습니다.

모두들 수고 많으셨습니다! 앞으로도 즐거운 Reacting 하시길 바랍니다 ㅎㅎㅎ

 


Bug Fix

이 부분은 강의자료 준비가 끝난 다음, 발견된 버그들과 이에 대한 해결 방법입니다
(강의를 수정하면 중간중간 git 저장소의 step branches 도 수정해야하므로 이렇게 포스트 하단에 따로 작성하였습니다)

a. 화면 너비가 작으면 옵션버튼과 텍스트가 곂쳐진다

bug1

style.css 에서 .memo .info 의 padding 수정

해결방법: https://git.io/v6Y8m

 

b. 3줄이상의 메모를 수정 할 때,textarea 사이즈가 조절이 안됨

bug2

보통 textarea의 사이즈조절 부분은 javascript 로 직접 구현하지만, 저희의 경우엔 Materializecss 에서 이미 구현이 되어있기에 프로젝트를 작성하면서 따로 작성하지 않았었는데요
Materializecss 에서는 textarea 에 keyup 이벤트가 실행 될 때, 높이를 조정하는 코드가 실행됩니다.

따라서, ref 와 jQuery를 통하여 해당 textarea 의 keyup 이벤트를 임의로 트리거합니다.

해결방법: https://git.io/v6Y8i

  • 좋은 강의 많은 도움이 될 것 같습니다! 양도 엄청난데요. ㅎㅎ

  • 강의 재밌게 봤습니다~동영상 강의도 시작하셨더군요~재밌는 강의 기대하겠습니다~^^

  • khlman

    강의 잘 보고 있습니다. 근데 actions에 있는 Search의 onKeyDown 이벤트 안에서는 이상하게 props가 undefined로 나오는데, 왜 그런지 이해가 안가네요. App -> Header -> Search 이렇게 props.usernames를 건내주는걸 확인 했고, onKeyDown 외에서는 맵으로 만들어주는 것도 잘 되던데 이해가 잘 안됩니다 ㅠㅠ

    • 코드에 오타가 있었습니다:

      this.handlekeyDown = this.handleKeyDown.bind(this);

      handleKeyDown 이렇게 k가 대문자여야했는데 제가 소문자로 잣성했네요 ㅋㅋ

      수정하시면 아마 잘 될거에요. 게시물은 추후 수정하도록 하겠습니다.

      여기까지 따라해주셔서 감사합니다. 고생 많으셨어요 😉

  • finrir

    좋은 자료 감사합니다.

  • jun

    안녕하세요 VELOPERT님, babel-polyfill 을 사용하면 ie10까지는 호환성이 맞춰 지는거 같은데 ie9에서는 promise가 먹질 않고 있습니다. 혹시 ie9에서 promise 대응하는 방법이 있을까요?

    • jun

      제가 질문을 잘못 드렸네요 promise 패턴이 아니라 ie9에서 ajax통신시 XDomainRequest를 사용하면서 생긴 문제였습니다. 질문을 삭제할수 없어서 답글 남깁니다.

  • guest

    1강 부터 차근차근 보면서 모든 기능 완성하였습니다! 강의가 자세하여 쉽게 이해하고 잘 따라올 수 있었습니다! 정말 감사드립니다.

  • 흐리

    {this.props.params.username} 은 이제 안되고 {this.props.match.params.username}이 되네요. 참고하세요~

  • 흐리

    {this.props.params.username} 은 안되고 이제 {this.props.match.params.username}이 작동하네요~ 참고하세요~