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


2편에서는, 클라이언트 사이드의 초기 구조를 대충 잡았죠, 이번 편에서는 Redux 구조를 설정하도록 하겠습니다.

계속해서 컴포넌트를 뚝딱뚝딱 만들어봅시다.

 

11. Authentication 컴포넌트 만들기

Login 과 Register 라우트는 상당히 비슷하기 때문에, Authentication 이라는 컴포넌트를 만들어서 같이 사용하도록 하겠습니다.

aa

CodePen Link:

Authentication 컴포넌트에 mode 값을 주어서 true 일때는 Login, false 일떄는 Register 를 보여주게 할것입니다.

Authentication 컴포넌트 파일 생성 (src/components/Authentication.js)

import React from 'react';

class Authentication extends React.Component {
    render() {
        return (
            <div>
                Auth
            </div>
        );
    }
}

export default Authentication;

 

컴포넌트 인덱스 수정 (src/components/index.js)

import Header from './Header';
import Authentication from './Authentication';

export { Header, Authentication };

Login, Register 컨테이너 컴포넌트에 Authentication 렌더링 (src/containers/Login.js, Register.js)

import { Authentication } from 'components';
/* ... */
    render() {
        return (
            <div>
                <Authentication />
            </div>
        );
    }

 

Header 컴포넌트에서 열쇠 아이콘 누르면 로그인 페이지로 이동 (src/components/Header.js)

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

class Header extends React.Component {
    render() {

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

/* .... */

a 태그 대신에 react-router 의 Link 컴포넌트를 사용했는데요,

이 컴포넌트는 페이지를 새로 로딩하는것을 막고, 라우트에 보여지는 내용만 변하게 해줍니다

(만약에 a 태그로 이동을하게된다면 페이지를 처음부터 새로 로딩하게됩니다)

 

Authentication 컴포넌트의 propTypes 및 defaultProps 설정하기 (src/components/Authentication.js)

Authentication.propTypes = {
    mode: React.PropTypes.bool,
    onLogin: React.PropTypes.func,
    onRegister: React.PropTypes.func
};

Authentication.defaultProps = {
    mode: true,
    onLogin: (id, pw) => { console.error("login function not defined"); },
    onRegister: (id, pw) => { console.error("register function not defined"); }
};

export default Authentication;

 

Authetication 컴포넌트를 위한 style 추가 (src/styles.css)

/* Authentication */
.auth {
  margin-top: 50px;
    text-align: center;
}

.logo {
    text-align: center;
    font-weight: 100;
    font-size: 80px;
    -webkit-user-select: none;
    /* Chrome all / Safari all */
    -moz-user-select: none;
    /* Firefox all */
    -ms-user-select: none;
    /* IE 10+ */
    user-select: none;
    /* Likely future */
}

a.logo {
    color: #5B5B5B;
}

a {
    cursor: pointer;
}

.auth .card {
    width: 400px;
    margin: 0 auto;
}


@media screen and (max-width: 480px) {
  .auth .card {
    width: 100%;
  }

  .logo {
    font-size: 60px;
  }
}



.auth .header {
    font-size: 18px;
}

.auth .row {
    margin-bottom: 0px;
}

.auth .username {
  margin-top: 0px;
}

.auth .btn {
    width: 90%;
}

.auth .footer {
    border-top: 1px solid #E9E9E9;
    padding-bottom: 21px;
}

 

Authentication 컴포넌트 기본 틀 만들기 (src/components/Authentication.js)

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

class Authentication extends React.Component {
    render() {

        const loginView = (
            <div>loginView</div>
        );

        const registerView = (
            <div>registerView</div>
        );

        return (
            <div className="container auth">
                <Link className="logo" to="/">MEMOPAD</Link>
                <div className="card">
                    <div className="header blue white-text center">
                        <div className="card-content">{this.props.mode ? "LOGIN" : "REGISTER"}</div>
                    </div>
                    {this.props.mode ? loginView : registerView }
                </div>
            </div>
        );
    }
}

/* ... more codes ... */

이미지 18

이 부분이 Login 과 Register 에서 공통적으로 사용하는 부분입니다.

 

Login 과 Register 컨테이너 컴포넌트에 Authentication 컴포넌트의 mode 속성 설정 (src/containers/Login.js, Register.js)

// Login.js
<Authentication mode={true}/>

// Register.js
<Authentication mode={false}/>

주의 하실 점은, true 와 false 를  { bracket } 으로 감싸세요.그래야, 자바스크립트의 값을 전달합니다 (이렇게 안하면 오류발생합니다)

 

loginView 설정하기 (src/components/Authentication.js)

        const loginView = (
            <div>
                <div className="card-content">
                    <div className="row">
                        <div className="input-field col s12 username">
                            <label>Username</label>
                            <input
                            name="username"
                            type="text"
                            className="validate"/>
                        </div>
                        <div className="input-field col s12">
                            <label>Password</label>
                            <input
                            name="password"
                            type="password"
                            className="validate"/>
                        </div>
                        <a className="waves-effect waves-light btn">SUBMIT</a>
                    </div>
                </div>


                <div className="footer">
                    <div className="card-content">
                        <div className="right" >
                        New Here? <Link to="/register">Create an account</Link>
                        </div>
                    </div>
                </div>

            </div>
        );

 

registerView 설정하기 (src/components/Authentication.js)

        const registerView = (
            <div className="card-content">
                <div className="row">
                    <div className="input-field col s12 username">
                        <label>Username</label>
                        <input
                        name="username"
                        type="text"
                        className="validate"/>
                    </div>
                    <div className="input-field col s12">
                        <label>Password</label>
                        <input
                        name="password"
                        type="password"
                        className="validate"/>
                    </div>
                    <a className="waves-effect waves-light btn">CREATE</a>
                </div>
            </div>
        );

loginView와 registerView 를 보시면 다음 코드가 아예 똑같이 반복되죠?

                    <div className="input-field col s12 username">
                        <label>Username</label>
                        <input
                        name="username"
                        type="text"
                        className="validate"/>
                    </div>
                    <div className="input-field col s12">
                        <label>Password</label>
                        <input
                        name="password"
                        type="password"
                        className="validate"/>
                    </div>

코드를 한번 더 깔끔하게 정리해보도록 하겠습니다

 

inputBoxes 분리하기 (src/components/Authentication.js)

        const inputBoxes = (
            <div>
                <div className="input-field col s12 username">
                    <label>Username</label>
                    <input
                    name="username"
                    type="text"
                    className="validate"/>
                </div>
                <div className="input-field col s12">
                    <label>Password</label>
                    <input
                    name="password"
                    type="password"
                    className="validate"/>
                </div>
            </div>
        );
        
        const loginView = (
            <div>
                <div className="card-content">
                    <div className="row">
                        {inputBoxes}
                        <a className="waves-effect waves-light btn">SUBMIT</a>
                    </div>
                </div>


                <div className="footer">
                    <div className="card-content">
                        <div className="right" >
                        New Here? <Link to="/register">Create an account</Link>
                        </div>
                    </div>
                </div>

            </div>
        );

        const registerView = (
            <div className="card-content">
                <div className="row">
                    {inputBoxes}
                    <a className="waves-effect waves-light btn">CREATE</a>
                </div>
            </div>
        );

 

input 의 값을 state 로 설정하기 / 변경시 state 업데이트 (src/components/Authentication)

/* .... */
class Authentication extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            username: "",
            password: ""
        };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e) {
        let nextState = {};
        nextState[e.target.name] = e.target.value;
        this.setState(nextState);
    }

    render() {
        const inputBoxes = (
            <div>
                <div className="input-field col s12 username">
                    <label>Username</label>
                    <input
                    name="username"
                    type="text"
                    className="validate"
                    onChange={this.handleChange}
                    value={this.state.username}/>
                </div>
                <div className="input-field col s12">
                    <label>Password</label>
                    <input
                    name="password"
                    type="password"
                    className="validate"
                    onChange={this.handleChange}
                    value={this.state.password}/>
                </div>
            </div>
        );
/* .... */

자, 이제 Authentication 컴포넌트의 기본적인 뷰가 완성되었습니다.

이제 기능을 구현해야하겠죠?

그 전에, 우선 우리 프로젝트에 Redux를 적용해야합니다

 

12. Redux 초기설정하기

reducers 디렉토리 생성 (src/reducers)

그 안에 authentication.js 파일과 index.js 파일을 생성하세요

 

authentication.js 리듀서 생성 (src/reducers/authentication.js)

export default function authentication(state, action) {
    if(typeof state === "undefined")
        state = {};
    /* To be implemented.. */
    return state;
}

일단 만들기만하고 있다가 구현할거에요.

 

리듀서 인덱스 생성 (src/reducers/index.js)

import authentication from './authentication';

import { combineReducers } from 'redux';

export default combineReducers({
    authentication
});

지금은 리듀서가 하나지만, 우리는 있다가 여러개를 만들것이므로 combineReducers 를 사용해줍니다.

 

Redux 적용하기 (src/index.js)

import React from 'react';
import ReactDOM from 'react-dom';

// Router
import { Router, Route, browserHistory, IndexRoute } from 'react-router';

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

// Redux
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reducers from 'reducers';
import thunk from 'redux-thunk';

const store = createStore(reducers, applyMiddleware(thunk));

const rootElement = document.getElementById('root');
ReactDOM.render(
    <Provider store={store}>
        <Router history={browserHistory}>
            <Route path="/" component={App}>
                <IndexRoute component={Home}/>
                <Route path="home" component={Home}/>
                <Route path="login" component={Login}/>
                <Route path="register" component={Register}/>
            </Route>
        </Router>
    </Provider>, rootElement
);

저희 프로젝트에 Redux 를 적용하였습니다.

여기서, redux-thunk 란것이 좀 생소하죠? (링크: https://github.com/gaearon/redux-thunk)

redux-thunk 는 dispatcher 가 action creator 가 만든 action 객체 외에도, 저희가 만든 함수도 처리 할 수 있게 해줘요.

비동기 처리를 할 때 사용되는 redux 미들웨어인데요,
보통 dispatch() 함수 내부에 들어가는건 action 객체, 혹은 action creator 함수이죠?
action-creator 는 그냥 객체만 반환 할 뿐 거기에서 HTTP 요청을 하거나 할수는 없잖아요,

redux-thunk 를 사용하면, 우리가 함수를 만들어서 (정확히는 함수를 리턴하는 함수에요) 그 함수 내부에서
AJAX 요청을 하고, 그 결과값에 따라 다른 action (ajax 가 성공했다던지 실패했다던지..) 을 또 dispatch 할 수 있게 됩니다.
이렇게 말로 풀어서 설명을 해드리자면 이해하기가 힘든데 한번 직접 사용해보면 아~ 이런거구나 하실거에요.

 

actions 디렉토리 생성 (src/actions)

그 안에 ActionTypes.js 라는 파일과 authentication.js 라는 파일을 만드세요.

 

ActionTypes.js 생성 (src/actions/ActionTypes.js)

/* AUTHENTICATION */

export const AUTH_LOGIN = "AUTH_LOGIN";
export const AUTH_LOGIN_SUCCESS = "AUTH_LOGIN_SUCCESS";
export const AUTH_LOGIN_FAILURE = "AUTH_LOGIN_FAILURE";

우리는 모든 action type 상수들을 모두 이 파일 안에 적어서 사용 할 거에요.

지금은 로그인 관련 Action type 만 있지만 앞으로 계속해서 추가해나갈거에요..

 

authentication.js 생성 (src/actions/authentication.js)

import {
    AUTH_LOGIN,
    AUTH_LOGIN_SUCCESS,
    AUTH_LOGIN_FAILURE
} from './ActionTypes';


/*============================================================================
    authentication
==============================================================================*/

/* LOGIN */
export function loginRequest(username, password) {
    /* To be implemented */
}

export function login() {
    return {
        type: AUTH_LOGIN
    };
}

export function loginSuccess(username) {
    return {
        type: AUTH_LOGIN_SUCCESS,
        username
    };
}

export function loginFailure() {
    return {
        type: AUTH_LOGIN_FAILURE
    };
}

자, 이제 로그인 기능을 구현해보겠습니다 !

 

13. 로그인 기능 구현하기

HTTP Client, axios 살펴보기

axios 설명서 링크: https://github.com/mzabriskie/axios

axios는 저희가 AJAX 요청을 할 때 사용 할 HTTP Client 입니다

React 는 뷰만 담당하는 라이브러리이기 때문에, 서버와의 통신을 하려면 이렇게 써드 파티 라이브러리를 사용해야합니다.

물론, axios 외에도 다른 HTTP 클라이언트를 사용해도 됩니다.

이 라이브러리를 사용하려면 당연히, import를 해주어야합니다

axios 사용 예제

불러오기

import axios from 'axios';

저희는 npm 을 통해 설치하였기에 프로젝트에서 직접 불러와서 사용합니다.

상황에 따라 CDN 에서 불러와도 됩니다. (<script src="https://npmcdn.com/axios/dist/axios.min.js"></script>)

GET 요청

axios.get('/user?id=velopert')
    .then( response => { console.log(response); } ) // SUCCESS
    .catch( error => { console.log(error); } ); // ERROR

axios.get('/user', {
        params: { id: 'velopert' }
    })
    .then( response => { console.log(response) } );
    .catch( error => { console.log(error) } );

// catch 는 생략 될 수 있습니다.

POST 요청

axios.post('/msg', {
        user: 'velopert',
        message: 'hi'
    })
    .then( response => { console.log(response) } )
    .catch( response => { console.log(response) } );

put, delete 같은 메소드도 동일하게 사용합니다.

더 많은 사용 예제는 메뉴얼을 참고해주세요

 

authentication action 파일에 axios import 하기 (src/actions/authentication.js)

import axios from 'axios';

 

loginRequest 구현하기 (src/actions/authentication.js)

자, 이 loginRequest 는 다른 action creator 랑 다릅니다.

이 함수는 또 다른 함수를 리턴하거든요! (thunk)

근데 thunk 가 뭔가요?

thunk 는 특정 작업의 처리를 미루기위해서 함수로 wrapping 하는것을 의미해요

// 1 + 2 가 바로 처리됩니다.
// x === 3
let x = 1 + 2;

// 1 + 2 의 계산이 미뤄졌어요
// foo 는 나중에 계산을 해야 할 때 가서 실행할 수 있죠.
// foo 가 바로 thunk 에요!
let foo = () => 1 + 2;'

x = foo(); // 여기서 비로소 계산이 실행되죠.

loginRequest 는 dispatch 를 파라미터로 갖는 thunk 를 리턴합니다

export function loginRequest(username, password) {
    return (dispatch) => {
        /* do stuffs.. */
    }
}

이런식으로 말이죠, 그리고 나중에 컴포넌트에서 dispatch(loginRequest(username, pssword)) 를 하게 되면,

미들웨어를 통하여 loginRequest 가 반환한 thunk 를 처리하게 돼요.

계속해서, 로그인을 구현해보겠습니다.

export function loginRequest(username, password) {
    return (dispatch) => {
        // Inform Login API is starting
        dispatch(login());

        // API REQUEST
        return axios.post('/api/account/signin', { username, password })
        .then((response) => {
            // SUCCEED
            dispatch(loginSuccess(username));
        }).catch((error) => {
            // FAILED
            dispatch(loginFailure());
        });
    };
}

이런식으로 thunk 내부에서 다른 action 을 dispatch 할 수 있어요.

이제 reducer 를 완성시켜봅시다.

 

authentication 리듀서 – 로그인 기능 완성하기 (src/reducers/authentication.js)

import * as types from 'actions/ActionTypes';
import update from 'react-addons-update';

const initialState = {
    login: {
        status: 'INIT'
    },
    status: {
        isLoggedIn: false,
        currentUser: '',
    }
};

export default function authentication(state, action) {
    if(typeof state === "undefined")
        state = initialState;

    switch(action.type) {
        /* LOGIN */
        case types.AUTH_LOGIN:
            return update(state, {
                login: {
                    status: { $set: 'WAITING' }
                }
            });
        case types.AUTH_LOGIN_SUCCESS:
            return update(state, {
                login: {
                    status: { $set: 'SUCCESS' }
                },
                status: {
                    isLoggedIn: { $set: true },
                    currentUser: { $set: action.username }
                }
            });
        case types.AUTH_LOGIN_FAILURE:
            return update(state, {
                login: {
                    status: { $set: 'FAILURE' }
                }
            });
        default:
            return state;
    }
}

import * as types from ‘actions/ActionTypes’;  이 코드는 ActionTypes 에서 export 한 모든 상수를 types 객체에 넣어서 불러옵니다.

thunk 를 리턴하는 loginRequest는 리듀서에서 따로 case를 지정해주지 않아도 됩니다.

 

Login 컨테이너 컴포넌트를 Redux 에 연결하기 (src/container/Login.js)

import React from 'react';
import { Authentication } from 'components';
import { connect } from 'react-redux';
import { loginRequest } from 'actions/authentication';

class Login extends React.Component {
    render() {
        return (
            <div>
                <Authentication mode={true}/>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        status: state.authentication.login.status
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        loginRequest: (id, pw) => { 
            return dispatch(loginRequest(id,pw)); 
        }
    };
};

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

react-redux 를 통하여 컴포넌트를 Redux에 연결하고,

로그인요청을하는 loginRequest 와 로그인 요청 상태인 status 를 authentication 컴포넌트에 매핑 해줍니다.

 

Login 컨테이너 컴포넌트에 handleLogin 구현하기 (src/containers/Login.js)

import { browserHistory } from 'react-router';

class Login extends React.Component {
    constructor(props) {
        super(props);
        this.handleLogin = this.handleLogin.bind(this);
    }
    
    handleLogin(id, pw) {
        return this.props.loginRequest(id, pw).then(
            () => {
                if(this.props.status === "SUCCESS") {
                    // create session data
                    let loginData = {
                        isLoggedIn: true,
                        username: id
                    };

                    document.cookie = 'key=' + btoa(JSON.stringify(loginData));

                    Materialize.toast('Welcome, ' + id + '!', 2000);
                    browserHistory.push('/');
                    return true;
                } else {
                    let $toastContent = $('<span style="color: #FFB4BA">Incorrect username or password</span>');
                    Materialize.toast($toastContent, 2000);
                    return false;
                }
            }
        );
    }
    
    render() {
        return (
            <div>
                <Authentication mode={true}
                    onLogin={this.handleLogin}/>
            </div>
        );
    }
}

/* CODES */

handleLogin 메소드를 만들어주고, constructor에서 바인딩도 해줍니다.

그리고, 아까 매핑한 loginRequest 함수를 실행하게합니다.

 

뒤에 .then() 은, AJAX 요청이 끝난다음에 할 작업인데요, 이건 axios 가 Promise 를 사용하기 떄문에 사용 가능한거랍니다

Promise 는 JavaScript ES6 에 생긴 비동기 처리를 할 때 사용하는 기술입니다.

그리고, 맨 앞에 return이 들어갔죠? 이렇게 함으로서, handleLogin 메소드를 실행한 실행자에서, handleLogin.then() 방식으로 또 다음 할 작업을 설정 할 수 있게 해줍니다.

 

로그인이 성공하면, 세션 데이터를 쿠키에 저장합니다. btoa는 JavaScript의 base64 인코딩 함수입니다.

Material.toast 는 Materializecss 프레임워크의 알림 기능입니다.

browserHistory.push() 를 통하여 라우팅을 트리거 할 수 있습니다 (Link 를 누른것과 똑같은 효과, 이를 사용하기 위해 상단에 import)

 

성공하면 true, 실패하면 false 를 반환하죠?

이는 성공여부를 알리기 위함입니다 (로그인 실패시 비밀번호 인풋박스 초기화)

 

다 작성후, 이 메소드를 authentication 컴포넌트로 전달해주세요.

 

authentication 컴포넌트에서 위에서 받아온 props 사용하기

/* codes... */

    constructor(props) {
        super(props);
        this.state = {
            username: "",
            password: ""
        };
        this.handleChange = this.handleChange.bind(this);
        this.handleLogin = this.handleLogin.bind(this);
    }

/* codes... */

    handleLogin() {
        let id = this.state.username;
        let pw = this.state.password;
        
        this.props.onLogin(id, pw).then(
            (success) => {
                if(!success) {
                    this.setState({
                        password: ''
                    });
                }
            }
        );
    }

    render() {
       /* codes... */
        const loginView = (
            <div>
                <div className="card-content">
                    <div className="row">
                        {inputBoxes}
                        <a className="waves-effect waves-light btn"
                            onClick={this.handleLogin}>SUBMIT</a>
                    </div>
                </div>

        /* codes... */

 

여기에서도 handleLogin 메소드를 만들어주고 constructor 에서 바인딩을 해줍니다.

그리고 props로 전달받은 onLogin 을 실행하세요.

이번엔 (success) => { .. } 가 있죠? 여기서 success 는 아까전에 Login 컴포넌트의 handleLogin 에서 리턴한 true/false 값입니다.

 

마지막으로 로그인버튼을 클릭하면 이 메소드가 실행되게 설정하세요.

 

한번 저장을 하고 로그인을 시도해보세요. 잘 되나요?

14. 회원가입 구현하기

회원가입 구현하는건, 로그인 구현하는것과 매우 비슷합니다. 우선 ActionTypes 를 추가해주세요

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

export const AUTH_REGISTER = "AUTH_REGISTER";
export const AUTH_REGISTER_SUCCESS = "AUTH_REGISTER_SUCCESS";
export const AUTH_REGISTER_FAILURE = "AUTH_REGISTER_FAILURE";

대충 어떻게 해야 할 지 감이 오지 않나요?

 

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

import {
    AUTH_LOGIN,
    AUTH_LOGIN_SUCCESS,
    AUTH_LOGIN_FAILURE,
    AUTH_REGISTER,
    AUTH_REGISTER_SUCCESS,
    AUTH_REGISTER_FAILURE
} from './ActionTypes';
/* codes.. */
/* REGISTER */
export function registerRequest(username, password) {
    return (dispatch) => {
        // To be implemented..
    };
}

export function register() {
    return {
        type: AUTH_REGISTER
    };
}

export function registerSuccess() {
    return {
        type: AUTH_REGISTER_SUCCESS,
    };
}

export function registerFailure(error) {
    return {
        type: AUTH_REGISTER_FAILURE,
        error
    };
}

구조는 Login 구현할떄랑 정말 비슷합니다.

단, Login같은경우는 오류 종류가 하나밖에 없었던 방면,

Register는 오류 종류가 3개니까, resgisterFailure 에 error 값도 전해주도록 설정하였습니다.

 

registerRequest 구현하기 (src/actions/ActionTypes.js)

/* REGISTER */
export function registerRequest(username, password) {
    return (dispatch) => {
        // Inform Register API is starting
        dispatch(register());

        return axios.post('/api/account/signup', { username, password })
        .then((response) => {
            dispatch(registerSuccess());
        }).catch((error) => {
            dispatch(registerFailure(error.response.data.code));
        });
    };
}

 

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

const initialState = {
    login: {
        status: 'INIT'
    },
    register: {
        status: 'INIT',
        error: -1
    },
    status: {
        isLoggedIn: false,
        currentUser: '',
    }
};

export default function authentication(state, action) {
    /* codes... */
        case types.AUTH_REGISTER:
            return update(state, {
                register: {
                    status: { $set: 'WAITING' },
                    error: { $set: -1 }
                }
            });
        case types.AUTH_REGISTER_SUCCESS:
            return update(state, {
                register: {
                    status: { $set: 'SUCCESS' }
                }
            });
        case types.AUTH_REGISTER_FAILURE:
            return update(state, {
                register: {
                    status: { $set: 'FAILURE' },
                    error: { $set: action.error }
                }
            });
        default:
            return state;
    }
}

*initialState 에 register 가 추가되었습니다

리듀서가 AUTH_REGISTER 액션들을 처리 할 수 있도록 코드를 추가해주세요.

 

Register 컴포넌트 Redux 연결하기 (src/containers/Register.js)

import React from 'react';
import { Authentication } from 'components';
import { connect } from 'react-redux';
import { registerRequest } from 'actions/authentication';

class Register extends React.Component {
    render() {
        return (
            <div>
                <Authentication mode={false}/>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        status: state.authentication.register.status,
        errorCode: state.authentication.register.error
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        registerRequest: (id, pw) => {
            return dispatch(registerRequest(id, pw));
        }
    };
};

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

Login 을 Redux에 연결할때랑 비슷합니다. 다른점이 있다면 errorCode가 추가됐죠.

 

Register 컨테이너 컴포넌트에서 handleRegister 구현하기

import { browserHistory } from 'react-router';

class Register extends React.Component {
    
    
    constructor(props) {
        super(props);
        this.handleRegister = this.handleRegister.bind(this);    
    }
    
    handleRegister(id, pw) {
        return this.props.registerRequest(id, pw).then(
            () => {
                if(this.props.status === "SUCCESS") {
                    Materialize.toast('Success! Please log in.', 2000);
                    browserHistory.push('/login');
                    return true;
                } else {
                    /*
                        ERROR CODES:
                            1: BAD USERNAME
                            2: BAD PASSWORD
                            3: USERNAME EXISTS
                    */
                    let errorMessage = [
                        'Invalid Username',
                        'Password is too short',
                        'Username already exists'
                    ];

                    let $toastContent = $('<span style="color: #FFB4BA">' + errorMessage[this.props.errorCode - 1] + '</span>');
                    Materialize.toast($toastContent, 2000);
                    return false;
                }
            }
        );
    }

    render() {
        return (
            <div>
                <Authentication mode={false}
                    onRegister={this.handleRegister}/>
            </div>
        );
    }

/* CODES */

아까와 비슷합니다. 성공할 경우엔 login 페이지로 라우팅하고,

회원가입의 경우엔 오류의 종류가 3가지가 있습니다. 배열을 사용해 이를 처리하게 합니다.

 

 

컴포넌트 위에서 받아온 props 사용하기 (src/components/authentication.js)

/*..codes..*/
    constructor(props) {
        super(props);
        this.state = {
            username: "",
            password: ""
        };
        this.handleChange = this.handleChange.bind(this);
        this.handleLogin = this.handleLogin.bind(this);
        this.handleRegister = this.handleRegister.bind(this);
    }
 
    /* codes */

    handleRegister() {
        let id = this.state.username;
        let pw = this.state.password;
        
        this.props.onRegister(id, pw).then(
            (result) => {
                if(!result) {
                    this.setState({
                        username: '',
                        password: ''
                    });
                }
            }
        );
    }

/* codes */

        const registerView = (
            <div className="card-content">
                <div className="row">
                    {inputBoxes}
                    <a className="waves-effect waves-light btn"
                        onClick={this.handleRegister}>CREATE</a>
                </div>
            </div>
        );

handleRegister 메소드를 만들고, constructor 에서 this 바인딩 한 다음에

register 버튼을 클릭하면 실행하도록 하였습니다.

회원가입은 오류가 났을 경우 모든 인풋박스를 초기화 합니다.

 

한번 회원가입을 시도해보세요. 잘 되나요?

 

비밀번호 input에서 엔터를 눌렀을 때 로그인/회원가입 트리거 (src/components/authentication.js)

    constructor(props) {
        /* codes.. */
        this.handleKeyPress = this.handleKeyPress.bind(this);
    }
    /* codes.. */
    handleKeyPress(e) {
        if(e.charCode==13) {
            if(this.props.mode) {
                this.handleLogin();
            } else {
                this.handleRegister();
            }
        }
    }
    /* codes.. */

                <div className="input-field col s12">
                    <label>Password</label>
                    <input
                    name="password"
                    type="password"
                    className="validate"
                    onChange={this.handleChange}
                    value={this.state.password}
                    onKeyPress={this.handleKeyPress}/>
                </div>

자잘한 디테일을 추가해줬습니다. 이제 비밀번호 input 에서 엔터를 누르면 로그인/회원가입이 트리거됩니다.

 

 

Authentication 컴포넌트는 이제 정말  끝났습니다. 앞으로 손 댈 일 없어요.

 

15.  로그인 세션 확인 구현 / 로그아웃 구현

이제, 로그인 상태라면 로그아웃 버튼을 보여주게하고,

페이지가 새로고침 될 때, 현재 세션이 유효한지 체크하는 기능을 구현하겠습니다.

 

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

export const AUTH_GET_STATUS = "AUTH_GET_STATUS";
export const AUTH_GET_STATUS_SUCCESS = "AUTH_GET_STATUS_SUCCESS";
export const AUTH_GET_STATUS_FAILURE = "AUTH_GET_STATUS_FAILURE";

 

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

import {
   /* ... */
    AUTH_GET_STATUS,
    AUTH_GET_STATUS_SUCCESS,
    AUTH_GET_STATUS_FAILURE
} from './ActionTypes';

/* GET STATUS */
export function getStatusRequest() {
    return (dispatch) => {
        // to be implemented...
    };
}

export function getStatus() {
    return {
        type: AUTH_GET_STATUS
    };
}

export function getStatusSuccess(username) {
    return {
        type: AUTH_GET_STATUS_SUCCESS,
        username
    };
}

export function getStatusFailure() {
    return {
        type: AUTH_GET_STATUS_FAILURE
    };
}

이번에는 request가 아닌 post 가 아닌 get 메소드를 사용합니다.

한번 구현해볼까요?

 

getStatusRequest 구현하기 (src/actions/authentication.js)

/* GET STATUS */
export function getStatusRequest() {
    return (dispatch) => {
        // inform Get Status API is starting
        dispatch(getStatus());

        return axios.get('/api/account/getInfo')
        .then((response) => {
            dispatch(getStatusSuccess(response.data.info.username));
        }).catch((error) => {
            dispatch(getStatusFailure());
        });
    };
}

 

authentication 리듀서 수정하기

const initialState = {
    login: {
        status: 'INIT'
    },
    register: {
        status: 'INIT',
        error: -1
    },
    status: {
        valid: false,
        isLoggedIn: false,
        currentUser: '',
    }
};

export default function authentication(state, action) {
    /* codes .. */
        case types.AUTH_GET_STATUS:
            return update(state, {
                status: {
                    isLoggedIn: { $set: true }
                }
            });
        case types.AUTH_GET_STATUS_SUCCESS:
            return update(state, {
                status: {
                    valid: { $set: true },
                    currentUser: { $set: action.username }
                }
            });
        case types.AUTH_GET_STATUS_FAILURE:
            return update(state, {
                status: {
                    valid: { $set: false },
                    isLoggedIn: { $set: false }
                }
            });
        default:
            return state;
    }
}

initialState 의 status 부분에 valid 키가 추가되었습니다.

페이지가 새로고침 되었을 때, 세션이 유효한지 체크하고, 유효하다면 true, 만료되었거나 비정상적이면 false 로 설정합니다.

AUTH_GET_STATUS 는 쿠키에 세션이 저장 된 상태에서, 새로고침을 했을 때 만 실행이 됩니다.
액션이 처음 실행 될 때, isLoggedIn 을 true 로 하는데요, 이 이유는, 이렇게 하지 않으면 로그인 된 상태에서 새로고침 했을 때,
세션 확인 AJAX 요청이 끝날떄까지 (아주 짧은시간이지만) 컴포넌트가 현재 로그인상태가 아닌것으로 인식하기 때문에
미세한 시간이지만 살짝, 깜빡임이 있겠죠? (로그인 버튼에서 로그아웃 버튼으로 변하면서)

이를 방지하기위하여 요청을 시작 할때는 컴포넌트에서 로그인상태인것으로 인식하게 하고
세션이 유효하다면 그대로 두고, 그렇지 않다면 로그아웃상태로 만듭니다.

 

App 컨테이너 컴포넌트 Redux 연결하기 (src/containers/App.js)

import React from 'react';
import { Header } from 'components';
import { connect } from 'react-redux';
import { getStatusRequest } from 'actions/authentication';


class App extends React.Component {
   /*... codes */
}

const mapStateToProps = (state) => {
    return {
        status: state.authentication.status
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        getStatusRequest: () => {
            return dispatch(getStatusRequest());
        }
    };
};

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

 

App 컨테이너 컴포넌트 세션 확인 기능 구현 (src/containers/App.js)

class App extends React.Component {

    componentDidMount() {
        // get cookie by name
        function getCookie(name) {
            var value = "; " + document.cookie;
            var parts = value.split("; " + name + "=");
            if (parts.length == 2) return parts.pop().split(";").shift();
        }

        // get loginData from cookie
        let loginData = getCookie('key');

        // if loginData is undefined, do nothing
        if(typeof loginData === "undefined") return;

        // decode base64 & parse json
        loginData = JSON.parse(atob(loginData));

        // if not logged in, do nothing
        if(!loginData.isLoggedIn) return;

        // page refreshed & has a session in cookie,
        // check whether this cookie is valid or not
        this.props.getStatusRequest().then(
            () => {
                console.log(this.props.status);
                // if session is not valid
                if(!this.props.status.valid) {
                    // logout the session
                    loginData = {
                        isLoggedIn: false,
                        username: ''
                    };

                    document.cookie='key=' + btoa(JSON.stringify(loginData));

                    // and notify
                    let $toastContent = $('<span style="color: #FFB4BA">Your session is expired, please log in again</span>');
                    Materialize.toast($toastContent, 4000);

                }
            }
        );
    }

    render() {
        /* Check whether current route is login or register using regex */
        let re = /(login|register)/;
        let isAuth = re.test(this.props.location.pathname);

        return (
            <div>
                {isAuth ? undefined : <Header isLoggedIn={this.props.status.isLoggedIn}/>}
                { this.props.children }
            </div>
        );
    }
}

/*..more codes..*/

쿠키에 세션이 저장되어있지 않거나, 세션이 로그아웃된 상태라면 아무것도 안합니다.

세션이 로그인 된 상태라면, 유효한지 체크를 하고, 유효하지 않으면 세션을 로그아웃시키고 다시 로그인 하라고 알림을 띄웁니다.

 

렌더링 부분에, Header 컴포넌트에 isLoggedIn 값을 전달해주었습니다.

 

여기까지 하셨다면, 로그인 한 다음 새로고침을 해보세요.

로그인이 유지되어있는지 확인하세요.

 

이제 authentication 의 마지막 단계인 로그아웃을 구현하겠습니다!

 

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

export const AUTH_LOGOUT = "AUTH_LOGOUT";

로그아웃은 성공하고 안하고가 중요하지 않기 떄문에, 액션 하나로도 충분합니다.

 

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

import {
    /*...*/
    AUTH_LOGOUT
} from './ActionTypes';

/* codes... */

/* Logout */
export function logoutRequest() {
    return (dispatch) => {
        return axios.post('/api/account/logout')
        .then((response) => {
            dispatch(logout());
        });
    };
}

export function logout() {
    return {
        type: AUTH_LOGOUT
    };
}

로그아웃 요청을 하고, 성공하면 logout 액션을 dispatch 합니다.

 

authentication 리듀서 수정 (src/reducers/authentication.js)

export default function authentication(state, action) {
     /* codes.. */
        /* LOGOUT */
        case types.AUTH_LOGOUT:
            return update(state, {
                status: {
                    isLoggedIn: { $set: false },
                    currentUser: { $set: '' }
                }
            });
        default:
            return state;
    }
}

 

App 컨테이너 컴포넌트에서 mapDispatchToProps 수정 (src/containers/App.js)

import { getStatusRequest, logoutRequest } from 'actions/authentication';

/* codes... */
const mapDispatchToProps = (dispatch) => {
    return {
        getStatusRequest: () => {
            return dispatch(getStatusRequest());
        },
        logoutRequest: () => {
            return dispatch(logoutRequest());
        }
    };
};

mapDispatchToProps에 logoutRequest 를 추가하세요.

 

App 컨테이너 컴포넌트에서 handleLogout 구현 (src/containers/App.js)

import { getStatusRequest, logoutRequest } from 'actions/authentication';


class App extends React.Component {

    constructor(props) {
        super(props);
        this.handleLogout = this.handleLogout.bind(this);
    }
    
    /* CODES */

    handleLogout() {
        this.props.logoutRequest().then(
            () => {
                Materialize.toast('Good Bye!', 2000);

                // EMPTIES THE SESSION
                let loginData = {
                    isLoggedIn: false,
                    username: ''
                };

                document.cookie = 'key=' + btoa(JSON.stringify(loginData));
            }
        );
    }

    render() {
        /* CODES */

        return (
            <div>
                {isAuth ? undefined : <Header isLoggedIn={this.props.status.isLoggedIn}
                                                onLogout={this.handleLogout}/>}
                { this.props.children }
            </div>
        );
    }
}

로그아웃 요청을 하고, 로그인데이터를 초기화하여 쿠키에 적용합니다.

이 메소드를 Header 컴포넌트의 onLogout props 로 전달해줍니다.

 

 

Header 컴포넌트 수정 (src/components/Header.js)

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

 

logoutButton이 클릭되면 this.props.onLogout 함수를 실행하도록합니다.

 

계정 인증 기능 구현이 끝났습니다!

 

  • YoungJoo Han

    계정 인증 구현이 완료되었습니다~

  • GILIM HONG

    [Login 컨테이너 컴포넌트를 Redux 에 연결하기] 부분에서
    mapDispatchToProps 인자가 state가 아닌 dispatch 로 바뀌어야 할 것 같아용

    • 반영되었습니다 🙂 감사합니다.

  • pistis

    1.
    [Register 컴포넌트 Redux 연결하기 ] 부분에서
    Register.js는 components가 아니라 containers 로 변경되어야 할 거 같습니다.

    2. LOGOUT 구현시 [authentication 액션파일 수정 (src/actions/authentication.js)] 에서 status관련된 액션 타입을 import하는 코드가 빠져있습니다.

  • 토깽이

    Register 컨테이너 컴포넌트에서 handleRegister 구현하기
    에서..31 줄 tthis.props.error 아니라..this.props.errorCode 아닌가요?

    • 넵 errorCode네요! 이 자료가 좀 급하게 만들어진거라 실수가 꽤 있습니다.. ㅠㅠ 곧 반영하겠습니다.

  • hanwong

    registerRequest 구현하기 (src/actions/ActionTypes.js)
    이 부분 파일경로가 authentication.js 맞죠?

    • 아, 맞습니다! 실수를 했네요 :ㅇ
      조만간 수정하겠습니다.

  • Jatch

    /Abc 라는 routePath로 접근하면 componentDidMount에서 actions/Abc.js의 axios.get(‘/Abc’)를 하는데
    해당 rest API에 접근하지 않고

    app.get(‘*’, (req, res) => {
    res.sendFile(path.resolve(__dirname, ‘./public/index.html’));
    });

    에 걸려서 자꾸 index.html을 반환해버리는데 어떻게 처리하면 rest API에 접근을 할 수 있을까요?

    • Jatch

      아 해결이 되었네요.
      app.get(‘*’,…)을 app.use(‘/’, routes) 밑으로 옮기니 제대로 axio.get도 작동이 하네요.

  • qvil

    안녕하세요 궁금한 점이 있는데요. 13번 로그인 기능 구현의 src/containers/Login.js에서 Materialize.toast 기능과 $ jquery를 사용하면 not defined 되는데 materialize랑 jquery를 bundle.js전에 script src로 불러주었는데도 안되네요.. 제 프로젝트와 clone한 프로젝트 index.html을 비교해봐도, 검색해봐도 잘 모르겠어서 질문 드립니다!

    react를 잘 몰라서 그런가 import원리를 잘 모르겠네요.. 어떤 소스를 보면 jquery를 import $ from ‘jquery’; 이런식으로도 부르는데 velopert님 소스에서는 index.html에서 cdn으로 load하던데 차이가 있나요??

    • qvil

      일단 에러는 npm install jquery materialize-css -S
      import $ from ‘jquery’;
      import Materialize from ‘materialize-css’;
      로 해결했는데 이렇게 하는게 맞는지 모르겠네요 계속 강의 진행해봐야겠네요..

  • 두루콩이

    회원가입을 한후에 로그인시 계속해서 cannot set property ‘loginInfo’ of undefined오류가 나는데 서버쪽 라우터 부분이 오류일까요..? ㅠㅠ 코드에서 틀린곳이 있나 계속해서 확인중인데 안찾아 집니다 ㅠㅠ

    • 서버쪽 라우터가 문제일것이라고 판단됩니다.
      로그인이 완료 되었을때, 세션의 loginInfo 값을 설정하게 되는데요.

      var session = req.session;
      session.loginInfo = {
      _id: account._id,
      username: account.username
      };

      아마 이 코드에서 에러가 발생 하는 것 같은데
      undefined 의 loginInfo 값을 설정 못한다는 오류가 뜨니 현재 세션이 제대로 적용이 안된듯합니다. 한번 세션이 제대로 작동하고있나 테스트해보세요.

      답변이 늦었네요.. 죄송합니다 🙁

  • ggoban

    로그인 후 새로고침하면 바로 로그인 세션이 풀리는데 이건 왜그럴까요.. 로그인 로그아웃은 정상적으로 되는데.. 흐음

    • 세션확인 부분도 제대로 구현을 하였는데 이런 오류가 발생하는건가요?
      this.props.getStatusRequest() 가 실행되었을때 어떤 결과가 나타나는지 한번 살펴보세요.

      • ggoban

        해당 기능은 정상적으로 구현이 됬는데 express 쪽에서 확인해보면 req.session 안에 loginInfo 정보가 없더라구요. 버전 차인지 아니면 분리해놔서 생긴 문젠지..
        대신 req.sessionStore 안에 정보가 있어서 그부분 조금 수정해서 loginInfo 를 별도로 추출하고 리턴해주도록 하니 기능이 정상으로 동작하였습니다.
        OTL

  • ㅠㅠ

    export function loginRequest(username, password) {
    return (dispatch) => {
    // Inform Login API is starting
    dispatch(login());

    // API REQUEST
    return axios.post(‘/api/account/signin’, { username, password })
    .then((response) => {
    // SUCCEED
    dispatch(loginSuccess(username));
    }).catch((error) => {
    // FAILED
    dispatch(loginFailure());
    });
    };
    }

    이 코드중
    return (dispatch) => {
    // Inform Login API is starting
    dispatch(login());
    이부분이 잘 이해가 되지 않네요

    username, password만 매개변수로 받앗고 dispatch가 함수내부에 선언된것도 아닌데..
    함수 내부에서 내장 함수의 매개변수로 쓰이는 것이.. 어떻게 가능한거죠?ㅠㅠ

    • dispatch가 사용될 수 있는 이유는,
      여기서 리턴하는것이 dispatch를 파라미터로 받는 함수이기 때문입니다.
      function(dispatch) { …. } 이것을 리턴하는것이죠

      thunk 미들웨어에서, 액션생성자 함수가 JSON 객체가 아닌 함수형태의 객체를 반환한다면,

      loginRequest(username, password)(dispatch) 와 같은형식으로 실행을 하는것이죠

  • 김대웅

    안녕하세요 php개발자인데 react가 핫하다하여 이 블로그를 통하여 공부하고 있는데요.

    axios를 안쓰고 jquery를 쓰고싶어서

    return axios.post(‘/api/account/signup’, { username, password })
    .then((response) => {
    dispatch(registerSuccess());
    }).catch((error) => {
    dispatch(registerFailure(error.response.data.code));
    });

    이부분을

    return $.ajax({
    url : “/api/account/signup”,
    type : “post”,
    dataType : “json”,
    cache : false,
    data : {
    ‘username’ : username,
    ‘password’ : password
    },
    success : function(response) {
    // SUCCEED
    dispatch(registerSuccess());
    },
    error : function(error){
    // FAILED
    dispatch(registerFailure(error.response.data.code));
    }
    });

    이렇게 바꾸어 보냈는데 받는 라우트 쪽에서

    req.body가 안넘어오더라구요. 어떻게 받아야 하나요??

    • 김대웅

      해결되었습니다.
      axios는 라우트에서 body-parser를 app.use(bodyParser.json());
      이렇게 사용하는 반면에
      jquery는 app.use(bodyParser.urlencoded({ extended: true }));
      urlencoded를 사용하더라구요

      개발자툴에서 Request Headers에 Content-Type이
      application/x-www-form-urlencoded; 보고 바꿔봣더니 되네요..
      jquery ajax의 contentType을 바꿔도 안되던데…

      velopert님 보시게된다면 왜그런지 알려주시면 감사하겟습니다!

      • 이미 해결하셨네요! 축하드립니다 ㅎㅎㅎ
        $.post 가 기본적으로는 url-encoding 된 데이터를 전송해서, { number: ‘1’ } 을 보내게 된다면 자동으로 number=1 이란 값을 전달하게 됩니다 (한번 크롬개발자 도구의 네트워크 부분을 확인해보세요)

        그래서, 전송하실때에 url-encode를 true 로 하거나, 혹은 stringify를 해야 작동하는것같네요

    • 김대웅

      $.ajax({
      url : “/api/account/signup”,
      type : “post”,
      dataType : “json”,
      contentType:”application/json; charset=UTF-8″,
      cache : false,
      data : JSON.stringify({“username” : username,”password” : password}),

      이렇게 컨텐츠타입을 바꿧어야했네요 받는쪽에서도 문제생기길래 다시 해결하였습니다.!

  • Jong-Ho Jeong

    안녕하세요. 튜토리얼 감사히 잘 보고있습니다.
    그런데 제 경우엔 로그인 후 페이지를 refresh하면 세션이 만료된것처럼 보입니다.
    정확히는 세션과 쿠키엔 정보가 남아있는데 State가 초기화 됩니다.
    Session에 대해 not valid한 경우에만 처리를 하고 valid된 경우에 대한 처리가 없기 때문이라고 생각하는데요.
    위 설명 중 “App 컨테이너 컴포넌트 세션 확인 기능 구현” 파트에서 refresh했을 때도 State를 유지할 수 있는 이유가 무엇인가요?

    • 컴포넌트의 state 는 메모리에 저장 되므로, 새로고침 했을 때, 초기화 되는 부분은 정상적입니다.
      그래서 현재의 플로우를 간단하게 설명드리자면, 로그인을 한 다음엔, 자신이 로그인 했다는 정보를 쿠키에 저장합니다. (혹은 로컬스토리지에도 저장 할 수 있습니다)

      페이지가 새로고침 되었을땐 state 가 초기화됩니다. 그렇기에, componentDidMount 에서 저장매체에서 세션정보를 불러옵니다. (지금의 경우는 쿠키가 되겠죠) 쿠키의 정보에서 자신이 로그인되어있다고 기록되어있으면 현재 로그인 상태인걸로 간주합니다.

      하지만, 이 경우 세션이 만료되도 로그인 상태로 유지되는 문제가 발생합니다. 따라서, 그렇게 로그인 한 상태인것 처럼 임시로 설정을 하고, API 를 통해서 현재 세션을 재확인합니다. 그리고 그 세션 정보에 따라 로그인을 유지 할지, 아니면 현재 데이터를 파기시키고 로그아웃을 시킬지 정합니다.

      • Jong-Ho Jeong

        빠른 답변 감사합니다.
        말씀하신것 처럼 State에 영속성이 없기 때문에 쿠키에서 상태를 가져옵니다.

        그런데 그 상태를 가져와서 새로고침으로 초기화된 State에 어떻게 영향을 끼칠 수 있는건가요?
        새로고침시마다 State가 authentication 리듀서안의 initialState로 초기화되서
        로그인/아웃 버튼같은 것들이 로그인 되지않은 상태로 바뀝니다. 따로 State를 변경해주는 코드 없이 가능한건가요?

        • 정확히 코드를 지목해주자면 이부분입니다
          case types.AUTH_GET_STATUS:
          return update(state, {
          status: {
          isLoggedIn: { $set: true }
          }
          });

          쿠키에 정보가 만약에 저장되어있다면,
          세션 확인 요청을 보냄과 동시에, 로그인 상태를 true 로 설정합니다.

          • Jong-Ho Jeong

            제가 액션만 선언하고 state 변경에 대한 리듀서를 만들어놓지 않았었네요!
            state에 대한건 리듀서에게 위임한다는걸 간과했습니다.
            감사합니다.

  • Julio

    인프런 강의로 부터 시작해서 블로그 강의들도 너무 잘 보고 있습니다~
    한가지 문의를 드리려고 하는데요,
    위의 자료로 제가 만들고 있는 앱에 Redux를 적용해보고 있는중인데요
    reducers는 설치는 아래와 같이 하면 되나요?

    > npm install –save reducers

    모듈들을 모두 설치하고 실행하면 아래와 같이 오류가 발생합니다.
    무슨 문제일까요? ㅜㅜ

    https://uploads.disquscdn.com/images/2f33880290317516a0faf0018218d7b690cb9839be5b5475b15f070817babc16.png

    • 여기서 import reducers from ‘reducers’ 는 정확히 src/reducers를 의미한답니다 😉

      • lee jun hyung

        webpack 2 사용 중이신가요? 그러면 resolve 과정이 쪼~금 다르거든요.

        config파일 가셔서
        entry 오브젝트 아래에
        resolve 오브젝트 만드시고

        resolve: {

        alias: {

        com: path.resolve(__dirname, ‘src/components’),

        reducers: path.resolve(__dirname, ‘src/reducers’),

        },

        },

        이런 식으로 alias(별칭)를 만드시면 위와 같은 코드로 작업하실 수 있어요~.

  • 조용준

    https://uploads.disquscdn.com/images/22e3bf3c9026678a2b5ea0ec8c53b99c307843f0b44c1e358df0f26977e89637.png
    이번 강좌 제일 위에 Authentication.js 를 추가하다가 오류가 계속 발생하는데 이 오류는 어떻게 해결해야하는지 잘 모르겠습니다 ㅠㅠ 도와주세요
    render() {
    const asdf = (

    랜더 부분에서 const 하고 정의하는 부분이 사진과 같이 오류가 발생합니다.

    • preset 에 react 가 없는것 같아요.

  • Richa

    이 칼럼 주제와는 상관없는 질문 하나 드립니다.^^

    제가 request 패키지와 cheerio 패키지를 리엑트에서 사용하려고
    npm으로 설치하고 package.json에 추가를 했습니다.

    그런데, 아래와 같은 에러가 뜹니다.
    Uncaught Error: Cannot find module “../maps/decode.json”

    그래서 webpack 설정파일에 아래와 같이 옵션을 추가했습니다.
    ,
    externals: {
    jsdom: ‘window’,
    cheerio: ‘window’,
    htmlparser2: ‘window’,
    request: ‘window’,
    ‘react/lib/ExecutionEnvironment’: true,
    ‘react/lib/ReactContext’: ‘window’
    }

    그러고나서 위에 추가한 모듈을 import하여 사용하려고 하니, 제대로 작동을 하지 않네요..
    예를 들면 htmlparser2.Parse({…});부분을 실행하면 constructor가 아니라고 합니다.

    정확한 원인을 아직 발견하지 못해 혹시몰라 질문을 드립니다.^^

  • lee jun hyung

    axios post 요청하는 과정에서 두번째 파라미터에 오브젝트 값을 줘야하는데
    그냥 { username, password } 만 들어가있네요.

    별거 아니긴 한데 아직 수정이 안 된 것 같아 올려봅니다 ㅎㅎ

    axios.post(‘/api/account/signin’, { username, password })

    .then((response) => {

    • Coding Mentor

      es6부터 나온기능인지는 잘모르겠는데

      let 키 = “값”

      let 오브젝트 = {키}

      이런식으로 오브젝트 안에 변수 그대로 넣으면 {키: “값”}으로 변해요.

      let 키 = “값”

      let 오브젝트 = {키: 키}

      처럼 귀찮게 -> : 키

      이부분을 추가안해줘도 되요

  • 쿠키 저렇게 파싱 안하고
    if (typeof document.cookie !== ‘undefined’) {
    let cookies = document.cookie.replace(/;/g, ‘”;”‘).replace(/=/g, ‘”:”‘);
    cookies = `{“${cookies}”}`;
    cookies = JSON.parse(cookies);
    이렇게 JSON으로 바로 만들어도 됩니다.

  • 오타 수정하여 github에 PR 보내드렸습니다~

  • Harmed Nazim

    react-redux의 connect부분이 잘 리해가 안됩니다.

  • floorViper

    좋은 강좌 감사합니다! 튜토리얼 보고 잘 이해 안가던 것을
    코드랩을 하나하나 따라하다 보니 이제야 조금이나마 이해가 가네요.
    힘내서 마지막 챕터까지 끝내보겠습니다