2-5. 관리자 로그인 인증 구현하기

포스트 작성, 수정, 삭제 작업을 하기 위하여, 간단한 비밀번호 인증을 거치는 작업을 구현해봅시다.

이 기능은, 서버 쪽에서 세션과 인증된(signed) 쿠키를 사용하여 구현됩니다. 인증된 쿠키란, 쿠키를 설정 할 때에 쿠키의 내용과 사전 설정해둔 비밀 키를 가지고 HMAC(해시 메시지 인증코드) 를 생성하여 같이 보관하는것을 의미합니다. 이를 통하여, 사용자가 쿠키를 변조하지 않았음을 검증 할 수 있습니다.

서버에 세션 적용하기

우리의 백엔드 서버에 세션을 사용하기 위해선 koa-session 라이브러리를 설치해야합니다.

$ yarn add koa-session

그리고, 이 라이브러리를 백엔드 서버에 적용하기 앞서, .env 파일에 두가지 환경변수를 설정하겠습니다.

첫째로 설정 할 것은 관리자 비밀번호 ADMIN_PASS 입니다. 나중에 로그인을 할 때 이 값을 사용하게 됩니다. 두번째로 설정 할 것은 COOKIE_SIGN_KEY 입니다. 이 값은, 인증된 쿠키를 만들 때 인증키로 사용됩니다. 비밀번호는 react123 으로 하고, 인증키에는 숫자/문자/특수문자를 포함하여 아무거나 적어보세요.

백엔드 프로젝트의 .env 파일을 수정하세요

.env

PORT=4000
MONGO_URI=mongodb://localhost/blog
ADMIN_PASS=react123
COOKIE_SIGN_KEY=C00KiE$1GNK3Y

그 다음엔 index.js 에서 koa-session 을 불러오고, COOKIE_SIGN_KEY 환경변수를 signKey 라는 레퍼런스로 설정해두고, 세션을 적용하겠습니다.

src/index.js

(...)
const session = require('koa-session');

const {
  PORT: port = 4000, // 값이 존재하지 않는다면 4000 을 기본값으로 사용
  MONGO_URI: mongoURI,
  COOKIE_SIGN_KEY: signKey
} = process.env;

(...)

const app = new Koa();

(...)

// 라우터 적용전에, bodyParser 적용
app.use(bodyParser());

// 세션 / 키 적용
const sessionConfig = {
  maxAge: 86400000, // 하루
  // signed: true (기본으로 설정되어있습니다.)
};

app.use(session(sessionConfig, app));
app.keys = [signKey];

(...)

세션의 유효기간은 하루로 설정해놓았습니다. 그리고 별도로 설정을 하지 않아도, signed 옵션이 기본값으로 활성화되며, 그 외에도 자바스크립트로는 접근 할 수 없고 브라우저의 네트워크 단에서만 조회 가능한 http 옵션도 기본값으로 활성화됩니다. koa-session 의 기본 설정값을 자세히 확인하고 싶다면 https://github.com/koajs/session 를 참조하세요.

비밀번호 인증 API 만들기

세션에 관한 준비는 다 마쳤으니, 비밀번호 인증 API 를 만들어보겠습니다. 우리가 앞으로 만들게될 API 들은 총 3가지 입니다

  1. POST /api/auth/login: 비밀번호로 로그인
  2. GET /api/auth/check: 현재 로그인 상태 확인
  3. POST /api/auth/logout: 로그아웃

우선 auth 라우트의 인덱스 부터 만들어봅시다. api 디렉토리에 auth 디렉토리를 생성 한 후, 다음 파일을 작성하세요.

`src/api/auth/index.js

const Router = require('koa-router');

const auth = new Router();
const authCtrl = require('./auth.ctrl');

auth.post('/login', authCtrl.login);
auth.get('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);

module.exports = auth;

그 다음엔, 각 라우트에 연결된 함수들을 구현해봅시다.

src/api/auth/auth.ctrl.js

const { ADMIN_PASS: adminPass } = process.env;

exports.login = (ctx) => {
  const { password } = ctx.request.body;
  if (adminPass === password) {
    ctx.body = {
      success: true
    };
    // 로그인에 성공하면 logged 값을 true 로 설정합니다.
    ctx.session.logged = true;
  } else {
    ctx.body = {
      success: false
    };
    ctx.status = 401; // Unauthorized
  }
};

exports.check = (ctx) => {
  ctx.body = {
    // ! 문자를 두번 입력함으로서,
    // 값이 존재하지 않을때도 false 를 반환하도록 설정합니다
    logged: !!ctx.session.logged
  };
};

exports.logout = (ctx) => {
  ctx.session = null;
  ctx.status = 204; // No Content
};

세션에 값을 설정 할 때에는 ctx.session.이름 = 값 형식으로 설정을 하면 되며, 조회 할 때는, ctx.session.이름 을 조회하면 됩니다. 그리고, 세션을 파기 할 때에는 ctx.session 값을 null 로 설정하세요.

api 에 auth 라우트 적용

우리가 방금 만든 라우트를 api 에 추가해주겠습니다.

src/api/index.js

const Router = require('koa-router');
const posts = require('./posts');
const auth = require('./auth');

const api = new Router();

api.use('/posts', posts.routes());
api.use('/auth', auth.routes());

// 라우터를 내보냅니다.
module.exports = api;

이제, 로그인 관련 API 개발이 거의 끝났습니다! Postman 을 사용하여 다음 요청들을 처리해보세요.

# 로그인
POST http://localhost:4000/auth/login
{
“password”: “react123”
}

## 응답
{
    "success": true
}

# 인증상태 확인
GET http://localhost:4000/auth/check

## 응답
{
    "logged": true
}

# 로그아웃
POST http://localhost:4000/auth/logout

## 응답: 공백

인증이 필요한 API 보호하기

이번에는, 포스트 읽기를 제외한 작업을 로그인 했을때만 수행 할 수 있도록 코드를 수정하겠습니다. 일반적인 방법으론, API 의 컨트롤러 함수에서 그냥 ctx.session.logged 값이 true 가 아니라면 작업을 중지하도록 구현하면 됩니다. 하지만, 이러한 코드를 write, update, remove 함수에 모두 작성하면, 중복되는 코드가 발생하겠죠?

따라서, 우리가 post, delete, patch API 라우트에서 checkObjectId 함수를 사전 수행 하게 했던 것 처럼, checkLogin 함수를 만들어서 API 처리를 하게 될 때 인증 상태를 확인하고나서 작업을 계속 진행하도록 코드를 작성해봅시다.

우선 posts.ctrl.js 파일에 checkLogin 함수를 작성하세요.

src/api/posts/posts.ctrl.js - checkLogin

exports.checkLogin = (ctx, next) => {
  if (!ctx.session.logged) {
    ctx.status = 401; // Unauthorized
    return null;
  }
  return next();
};

그리고나서, get 을 제외한 API 라우트들에 checkLogin 을 넣어보세요.

src/api/posts/index.js

const Router = require('koa-router');
const postsCtrl = require('./posts.ctrl');

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.get('/:id', postsCtrl.checkObjectId, postsCtrl.read);

posts.post('/', postsCtrl.checkLogin, postsCtrl.write);
posts.delete('/:id', postsCtrl.checkLogin, postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkLogin, postsCtrl.checkObjectId, postsCtrl.update);

module.exports = posts;

간단하지요? 이제 checkLogin 이 적용된 API 들은 로그인상태가 아니라면 401 에러를 응답하게 됩니다.

로그인 모달 만들기

다시 프론트엔드 프로젝트로 돌아와서, 로그인 모달을 만들어봅시다. 이 과정에선, 우리가 아까 만들었던 ModalWrapper 컴포넌트를 재사용하게되어, 금방 만들 수 있게 됩니다.

components/modal 디렉토리에 LoginModal 컴포넌트를 생성하여 다음 코드를 작성하세요:

src/components/modal/LoginModal/LoginModal.js

import React from 'react';
import styles from './LoginModal.scss';
import classNames from 'classnames/bind';
import ModalWrapper from 'components/modal/ModalWrapper';

const cx = classNames.bind(styles);

const LoginModal = ({
  visible, password, error, onCancel,  onLogin,  onChange, onKeyPress
}) => (
  <ModalWrapper visible={visible}>
    <div className={cx('form')}>
      <div onClick={onCancel} className={cx('close')}>&times;</div>
      <div className={cx('title')}>로그인</div>
      <div className={cx('description')}>관리자 비밀번호를 입력하세요</div>
      <input autoFocus type="password" placeholder="비밀번호 입력" value={password} onChange={onChange} onKeyPress={onKeyPress}/>
      { error && <div className={cx('error')}>로그인 실패</div> }
      <div className={cx('login')} onClick={onLogin}>로그인</div>
    </div>
  </ModalWrapper>
);

export default LoginModal;

이번 컴포넌트는 전달받은 props 가 꽤 많죠? password 의 경우엔 로그인 창에 있는 input 값의 value 로 설정되는 값으로서, 우리가 나중에 base 모듈에서 상태 관리를 하게 될 것입니다. error 값의 경우엔 사용자가 잘못된 비밀번호를 입력 했을 경우 에러를 보여주기위한 값입니다.

onCancel 은 닫기버튼 (× 문자는 × 입니다)을 클릭했을때 실행되는 함수이며, onLogin 은 로그인 버튼을 눌렀을때 실행되는 함수입니다. onChange 와 onKeyPress 는 비밀번호가 입력 될 때 호출되는 함수인데, onChange 는 값을 변경시켜주고, onKeyPress 의 경우엔 나중에 우리가 버튼 클릭 뿐만 아니라, 인풋입력 후 엔터를 눌렀을 때에도 로그인 작업을 수행하기 위하여 설정해주었습니다.

자, 그러면 이 컴포넌트를 스타일링도 해봅시다.

`src/components/modal/LoginModal/LoginModal.scss

@import 'utils';

.form {
  background: white;
  padding: 2rem;
  position: relative;
  padding-top: 2.5rem;
  width: 20rem;
  .close {
    line-height: 2rem;
    font-size: 2rem;
    position: absolute;
    right: 1rem;
    top: 0.5rem;
    cursor: pointer;
    &:hover {
      color: $oc-gray-6;
    }
  }
  .title {
    font-size: 1.25rem;
    font-weight: 500;
  }
  .description {
    margin-top: 0.25rem;
  }
  .error {
    margin-top: 0.5rem;
    margin-bottom: 0.5rem;
    color: $oc-red-6;
    text-align: center;
    font-size: 0.85rem;
  }

  input {
    width: 100%;
    font-size: 1.25rem;
    margin-top: 0.5rem;
    border: none;
    border-bottom: 1px solid $oc-gray-3;
    padding: 0.25rem;
    outline: none;
    border-radius: 4px;
  }

  .login {
    background: $oc-blue-6;
    text-align: center;
    color: white;
    font-weight: 500;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
    cursor: pointer;
    margin-top: 1rem;
    font-size: 1.25rem;
    &:hover {
      background: $oc-blue-5;
    }
    &:active {
      background: $oc-blue-6;
    }
  }
}

그 다음에는, 로그인 모달을 위한 컨테이너 컴포넌트인 LoginModalContainer 를 containers/modal 디렉토리에 만드세요.

src/containers/modal/LoginModalContainer.js

import React, { Component } from 'react';
import LoginModal from 'components/modal/LoginModal';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as baseActions from 'store/modules/base';

class LoginModalContainer extends Component {
  handleLogin = () => {

  }
  handleCancel = () => {
    const { BaseActions } = this.props;
    BaseActions.hideModal('login');
  }
  handleChange = (e) => {

  }
  handleKeyPress = (e) => {

  }
  render() {
    const { 
      handleLogin, handleCancel, handleChange, handleKeyPress
    } = this;
    const { visible } = this.props;

    return (
      <LoginModal
        onLogin={handleLogin} onCancel={handleCancel}
        onChange={handleChange} onKeyPress={handleKeyPress}
        visible={visible}
      />
    );
  }
}

export default connect(
  (state) => ({
    visible: state.base.getIn(['modal', 'login'])
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(LoginModalContainer);

지금은 우선 바로 구현 할 수 있는 handleCancel 메소드만 준비하고, 나머지는 추후 구현하겠습니다.

로그인 모달은 리스트 페이지던 포스트페이지던, 전역적으로 사용되는 모달이기때문에, 우리는 App 에서 렌더링 해주어야 합니다.

지금의 경우엔, 그냥 LoginModalContainer 컴포넌트를 App 에서 바로 렌더링해주어도 무방합니다. 하지만, 만약에 이렇게 전역적으로 필요해지는 컴포넌트들이 많아진다면, App 컴포넌트에 렌더링하게 되는 컴포넌트가 늘어나게 되면서 App 컴포넌트의 render 함수가 복잡해질 수 있습니다.

따라서, 우리는 Base 라는 컨테이너 컴포넌트를 만들어서 그 안에 LoginModalContainer 를 렌더링해주겠습니다. Base 를 컨테이너로 만드는 이유는, 우리가 페이지를 새로고침 할 때마다 현재 사용자가 로그인중인지 검증을 하게 는데, 이 작업을 Base 컴포넌트에서 처리 할 것이기 때문입니다.

src/containers/common/Base.js

import React, { Component } from 'react';
import LoginModalContainer from 'containers/modal/LoginModalContainer';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as baseActions from 'store/modules/base';

class Base extends Component {
  initialize = async () => {
    // 로그인 상태 확인 (추후 작성)
  }
  componentDidMount() {
    this.initialize();
  }
  render() {
    return (
      <div>
        <LoginModalContainer/>
        { /* 전역적으로 사용되는 컴포넌트들이 있다면
        여기서 렌더링 합니다. */}
      </div>
    )
  }
}

export default connect(
  null,
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(Base);

이 컴포넌트는, App 에서 Switch 하단에 렌더링 됩니다.

src/components/App.js

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ListPage, PostPage, EditorPage, NotFoundPage } from 'pages';
import Base from 'containers/common/Base';

const App = () => {
  return (
    <div>
      <Switch>
        (...)
      </Switch>
      <Base/>
    </div>
  );
};

export default App;

이제 Footer 에서 보여지는 관리자 로그인을 클릭하면 로그인 모달을 띄워주겠습니다. 이 작업을 하기 위해선, Footer 에서 리덕스 액션을 호출해야하므로 FooterContainer 컴포넌트를 만들어 주어야합니다.

src/containers/common/FooterContainer.js

import React, { Component } from 'react';
import Footer from 'components/common/Footer';
import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import * as baseActions from 'store/modules/base';

class FooterContainer extends Component {
  handleLoginClick = async () => {
    const { BaseActions } = this.props;
    BaseActions.showModal('login');
  }
  render() {
    const { handleLoginClick } = this;
    return (
      <Footer onLoginClick={handleLoginClick}/>
    );
  }
}

export default connect(
  (state) => ({
    // 추후 입력
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(FooterContainer);

handleLoginClick 이라는 메소드를 만들어서, 이를 Footer 에 onLoginClick props 로 전달을 해주었습니다. 그럼, Footer 컴포넌트에서 이 함수를 받아와서 로그인 버튼에 onClick 으로 설정해주어야겠죠?

const Footer = ({onLoginClick}) => (
  <footer className={cx('footer')}>
    <Link to="/" className={cx('brand')}>reactblog</Link>
    <div onClick={onLoginClick} className={cx('admin-login')}>관리자 로그인</div>
  </footer>
);

그리고 나서, PageTemplate 컴포넌트에서 기존 Footer 컴포넌트를 대체시키세요.

src/components/common/PageTemplate/PageTemplate.js

import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
import HeaderContainer from 'containers/common/HeaderContainer';
import FooterContainer from 'containers/common/FooterContainer';

const cx = classNames.bind(styles);

const PageTemplate = ({children}) => (
  <div className={cx('page-template')}>
    <HeaderContainer/>
    <main>
      {children}
    </main>
    <FooterContainer />
  </div>
);

export default PageTemplate;

이제 페이지 하단의 관리자 로그인을 누르면, 로그인 모달이 나타날 것입니다.

로그인 기능 구현하기

로그인 모달을 띄워줬으니, 로그인 API 를 연동해주겠습니다. 먼저 다음 API 함수들을 추가작성 하세요.

src/lib/api.js

(...)
export const login = (password) => axios.post('/api/auth/login', { password });
export const checkLogin = () => axios.get('/api/auth/check');
export const logout = () => axios.post('/api/auth/logout');

아까 만들었던 3가지 API를 호출하는 함수를 만들어 주었습니다. 이제 base 리덕스 모듈을 열어서 위 함수들을 사용하는 액션들을 만들겠습니다.

그리고, 로그인 모달에 있는 인풋 값을 설정하는 액션과, 로그인 모달 상태를 초기화하는 액션도 만들어보세요.

src/store/modules/base.js

(...)
import * as api from 'lib/api';

// action types
(...)
const LOGIN = 'base/LOGIN';
const LOGOUT = 'base/LOGOUT';
const CHECK_LOGIN ='base/CHECK_LOGIN';
const CHANGE_PASSWORD_INPUT = 'base/CHANGE_PASSWORD_INPUT';
const INITIALIZE_LOGIN_MODAL = 'base/INITIALIZE_LOGIN_MODAL';

// action creators
(...)

export const login = createAction(LOGIN, api.login);
export const logout = createAction(LOGOUT, api.logout);
export const checkLogin = createAction(CHECK_LOGIN, api.checkLogin);
export const changePasswordInput = createAction(CHANGE_PASSWORD_INPUT);
export const initializeLoginModal = createAction(INITIALIZE_LOGIN_MODAL);

// initial state
const initialState = Map({
  (...)
  // 로그인 모달 상태
  loginModal: Map({
    password: '',
    error: false
  }),
  logged: false // 현재 로그인 상태
});

// reducer
export default handleActions({
  (...)
  ...pender({
    type: LOGIN,
    onSuccess: (state, action) => {  // 로그인 성공 시
      return state.set('logged', true);
    },
    onError: (state, action) => {  // 에러 발생 시
      return state.setIn(['loginModal', 'error'], true)
                  .setIn(['loginModal', 'password'], '');
    }
  }),
  ...pender({
    type: CHECK_LOGIN,
    onSuccess: (state, action) => {
      const { logged } = action.payload.data;
      return state.set('logged', logged);
    }
}),
  [CHANGE_PASSWORD_INPUT]: (state, action) => {
    const { payload: value } = action;
    return state.setIn(['loginModal', 'password'], value);
  },
  [INITIALIZE_LOGIN_MODAL]: (state, action) => {
    // 로그인 모달의 상태를 초기상태로 설정합니다 (텍스트/에러 초기화)
    return state.set('loginModal', initialState.get('loginModal'));
  },

}, initialState)

이제 로그인 기능을 구현 할 준비가 끝났으니, 컴포넌트를 수정해봅시다. 먼저 LoginModalContainer 쪽에 아직 구현하지 않았던 메소드들을 완성해보세요. 추가적으로, 하단의 connect 부분에서 error와 password 값도 받아와서 props 에 넣어주세요.

src/containers/modal/LoginModalContainer.js - 내부 메소드

(...)

class LoginModalContainer extends Component {
  handleLogin = async () => {
    const { BaseActions, password } = this.props;
    try {
      // 로그인 시도, 성공 시 모달 닫기
      await BaseActions.login(password);
      BaseActions.hideModal('login');
    } catch (e) {
      console.log(e);
    }
  }
  handleCancel = () => {
    const { BaseActions } = this.props;
    BaseActions.hideModal('login');
  }
  handleChange = (e) => {
    const { value } = e.target;
    const { BaseActions } = this.props;
    BaseActions.changePasswordInput(value);
  }
  handleKeyPress = (e) => {
    // 엔터키가 눌리면 로그인 호출
    if(e.key === 'Enter') { 
      this.handleLogin();
    }
  }
  render() {
    const { 
      handleLogin, handleCancel, handleChange, handleKeyPress
    } = this;
    const { visible, error, password } = this.props;

    return (
      <LoginModal
        onLogin={handleLogin} onCancel={handleCancel}
        onChange={handleChange} onKeyPress={handleKeyPress}
        visible={visible} error={error} password={password}
      />
    );
  }
}

export default connect(
  (state) => ({
    visible: state.base.getIn(['modal', 'login']),
    password: state.base.getIn(['loginModal', 'password']),
    error: state.base.getIn(['loginModal', 'error'])
  }),
 (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(LoginModalContainer);

자, 이제 페이지 하단의 관리자 로그인을 눌러서 로그인 모달을 띄우고, 비밀번호 react123 을 입력 후, 로그인을 시도해보세요. 모달이 자동으로 닫힌다면 정상 작동 하는 것 입니다. 그 다음엔, 다시 로그인 모달을 띄워서 잘못 된 비밀번호를 입력해보세요. 에러가 나타나나요?

FooterContainer 완성하기

이제 FooterContainer 에서 로그인 상태일때에는 로그아웃을 할 수 있도록 현재 로그인 상태를 Footer 로 전달하세요. 그리고, handleLoginClick 메소드에서는, 로그인 상태일때에는 로그아웃 API 를 호출하고 새로고침을 하도록 설정해보세요.

src/containers/common/FooterContainer.js

(...)

class FooterContainer extends Component {
  handleLoginClick = async () => {
    const { BaseActions, logged } = this.props;
    if(logged) {
      try {
        await BaseActions.logout();
        window.location.reload(); // 페이지 새로고침
      } catch (e) {
        console.log(e);
      }
      return;
    }
    BaseActions.showModal('login');
    BaseActions.initializeLoginModal();
  }
  render() {
    const { handleLoginClick } = this;
    const { logged } = this.props;

    return (
      <Footer onLoginClick={handleLoginClick} logged={logged}/>
    );
  }
}

export default connect(
  (state) => ({
    logged: state.base.get('logged')
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(FooterContainer);

그리고, Footer 컴포넌트에선 logged 값이 true 면 관리자 로그인이 아닌 로그아웃을 띄우도록 설정하세요.

const Footer = ({onLoginClick, logged}) => (
  <footer className={cx('footer')}>
    <Link to="/" className={cx('brand')}>reactblog</Link>
    <div onClick={onLoginClick} className={cx('admin-login')}>
      {logged ? '로그아웃' : '관리자 로그인'}
    </div>
  </footer>
);

이제 페이지를 띄워서 로그인을 했을 때 버튼이 “로그아웃” 으로 바뀌는지 확인하세요. 그 다음엔, 그 버튼을 눌러서 로그아웃이 정상적으로 이뤄지는지도 확인해보세요.

페이지 로딩 시 로그인 상태 확인하기

현재, 로그인 상태에서 페이지를 새로고침을 하게 된다면, 상태가 초기화 되버립니다. 그 이유는, 리덕스 스토어 안에 있는 상태는 페이지를 새로 불러오게 된다면 보존되지 않기 때문입니다.

현재 서버 세션상으로는 로그인상태가 유지되어있기 때문에, 우리가 이전에 만든 checkLogin API 를 호출하여 현재 로그인 상태를 확인 후 리덕스 스토어에 반영해봅시다.

이 작업은 Base 컴포넌트에서 이뤄집니다. initialize 함수를 다음과 같이 작성해보세요

src/containers/common/Base.js - initialize

  initialize = async () => {
    const { BaseActions } = this.props;
    BaseActions.checkLogin();
  }

이렇게 해주면, 새로고침을 해도 클라이언트쪽에서 로그인 상태가 유지됩니다. 페이지를 불러오게 되면, checkLogin 이 호출 되면서 서버에 로그인 상태를 요청하고 이에 따라 상태에 반영시켜줍니다.

하지만 이것만으로는 충분하지 않습니다. 그 이유는, 새로고침을 했을 때, checkLogin API 가 응답 할 때 까지는 클라이언트에선 로그아웃 상태로 간주하기 때문이죠.

따라서, 사용자가 로그인을 했다면, 새로고침을 해도, checkLogin 이 응답 할 때 까지 임시적으로 로그인 상태를 유지하고 있어야합니다. 이를 구현하기 위해선, HTML5 의 localStorage 를 이용하면 됩니다. localStorage 에 값을 넣으면, 페이지가 새로고침 되거나 브라우저를 껐다 켜도 값이 유지됩니다. 하지만 주의 할 점은, 값이 문자열 형태로 들어가게 되므로, 만약에 객체, 혹은 숫자, Boolean 등의 값을 넣게 된다면, JSON.stringify / JSON.parse 를 사용하거나, 문자열로 취급을 해 주어야 합니다.

자, 임시로 로그인 상태를 설정하기 위하여 TEMP_LOGIN 이라는 액션을 준비해줍시다.

src/store/modules/base.js

(...)

// action types
(...)
const TEMP_LOGIN = 'base/TEMP_LOGIN';

// action creators
(...)
export const tempLogin = createAction(TEMP_LOGIN);

// initial state
(...)

// reducer
export default handleActions({
  (...)
  [TEMP_LOGIN]: (state, action) => {
    return state.set('logged', true);
  }
}, initialState)

이제 우리는 상황에 따라 localStorage 에 값을 넣거나 조회를 해주겠습니다. 우선, 로그인에 성공 했을때 localStorage 의 logged 값을 “true” 로 설정하세요.

src/containers/modal/LoginModalContainer.js - handleLogin

  handleLogin = async () => {
    const { BaseActions, password } = this.props;
    try {
      // 로그인 시도, 성공 시 모달 닫기
      await BaseActions.login(password);
      BaseActions.hideModal('login');
      localStorage.logged = "true";
    } catch (e) {
      console.log(e);
    }
  }

그리고, 페이지를 로딩 할 때 localStorage 의 logged 값을 불러온 뒤, 이 값에 따라 TEMP_LOGIN 액션을 호출하세요.

src/containers/common/Base.js - initialize

  initialize = () => {
    const { BaseActions } = this.props;
    if(localStorage.logged === "true") {
      BaseActions.tempLogin();
    }
    BaseActions.checkLogin();
  }

이렇게 하고나면, 로그인을 했을 때 localStorage 에 로그인 상태를 저장하게 되어 이 상태가 존재한다면 checkLogin API 가 응답하기 전부터 로그인중인 것으로 간주합니다.

그리고, 이는 임시적으로 로그인 상태로 만든 것 뿐이므로, 만약에 서버 세션상에서는 로그인 상태가 아니라면 다시 로그인 상태가 비활성화 됩니다.

포스트 작성 / 수정 / 삭제버튼 로그인시에만 보여주기

로그인 기능을 구현 했으니, 이에 따라 비로그인 상태일 때 기능을 제한시켜야겠지요? 비로그인 상태에선 포스트 작성 / 수정 / 삭제를 할 수 없습니다.

기능을 제한시키기 위해서, 먼저 HeaderContainer 에서 스토어의 logged 값을 연동시키고, 이를 Header 컴포넌트로 전달하세요.

src/containers/common/HeaderContainer.js

(...)

  render() {
    const { handleRemove } = this;
    const { match, logged } = this.props;

    const { id } = match.params;

    return (
      <Header
        postId={id}
        logged={logged}
        onRemove={handleRemove}
      />
    );
  }
}

export default connect(
  (state) => ({
    logged: state.base.get('logged')
  }),
  (dispatch) => ({
    BaseActions: bindActionCreators(baseActions, dispatch)
  })
)(withRouter(HeaderContainer));

Header 컴포넌트에서는 logged 값이 true 일 때만, right 클래스를 가지고있는 div 엘리먼트가 보여지도록 설정하세요

src/components/base/Header/Header.js

(...)

const Header = ({postId, logged, onRemove}) => (
  <header className={cx('header')}>
    <div className={cx('header-content')}>
      <div className={cx('brand')}>
        <Link to="/">reactblog</Link>
      </div>
      { logged &&  <div className={cx('right')}>
        {
          // flex 를 유지하기 위하여 배열 형태로 렌더링 합니다.
          postId && [
            <Button key="edit" theme="outline" to={`/editor?id=${postId}`}>수정</Button>,
            <Button key="remove" theme="outline" onClick={onRemove}>삭제</Button>
          ]
        }
        <Button theme="outline" to="/editor">새 포스트</Button>
      </div> }
    </div>
  </header>
);

export default Header;

이제 로그인을 하지 않았다면, 헤더에서 우측에 보여지는 버튼들이 사라지고, 로그인을 하면 다시 나타나게 됩니다.

results matching ""

    No results matching ""