2편에서는, 클라이언트 사이드의 초기 구조를 대충 잡았죠, 이번 편에서는 Redux 구조를 설정하도록 하겠습니다.
계속해서 컴포넌트를 뚝딱뚝딱 만들어봅시다.
11. Authentication 컴포넌트 만들기
Login 과 Register 라우트는 상당히 비슷하기 때문에, Authentication 이라는 컴포넌트를 만들어서 같이 사용하도록 하겠습니다.
CodePen Link:
- 로그인 페이지: http://codepen.io/velopert/pen/bZaVjb
- 회원가입 페이지: http://codepen.io/velopert/pen/pbpxGY
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 ... */
이 부분이 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 함수를 실행하도록합니다.
계정 인증 기능 구현이 끝났습니다!