Client side auth

email, password 기반의 authentication server를 다른 포스팅을 통해서 만들어 보았습니다. 이 서버를 이용해 로그인 기능만 갖춘 간단한 앱을 만들며 프론트엔드에서 로그인 처리를 하는 한 방법에 대해 확인해 보겠습니다.

Link

Contents

  • Dependency
  • File Structure
  • Make root file
  • Make App Component
  • Auth Reducers
  • Make Header Component
  • Make Signup Component using Redux Form
  • Where to store JWT
  • Auth HOC
  • Reference

Dependency

  • react
  • react-router-dom
  • redux
  • react-redux
  • redux-form
  • axios
  • redux-thunk

File Structure

public
|  |__index.html
|
src
   |__actions
   |    |__index.js
   |    |__types.js
   |
   |__components
   |    |__auth
   |    |  |__Signin.js
   |    |  |__Signout.js
   |    |  |__Signup.js
   |    |__App.js
   |    |__Feature.js
   |    |__Header.js
   |    |__HeaderStyle.css
   |    |__Welcome.js
   |    |__requireAuth.js
   |
   |__reducers
   |    |__auth.js
   |    |__index.js
   |__index.js

Make root file

Dependency에 나열한 라이브러리를 설치 후 index.tsx 파일에서 Redux store를 함께 설정합니다. 그리고, redux store의 초기값에 auth를 설정했습니다. 이후에 설명하겠지만, 유저가 로그인 후 페이지를 이동하더라도 로그인을 유지할 수 있도록 JWT Token을 localStorage에 저장했다 앱 초기에 저장된 token이 있으면 다시 불러옵니다. localStorage에 JWT token을 저장하는 것에 대해 여러 의견이 있는것 같은데, 이것에 대해서는 좀 더 찾아봐야 겠습니다.

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, appliMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';

import reducers from './reducers';
import App from './components/App';

const store = createStroe(reducers, {}, applyMiddleware(reduxThunk));

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.querySelector('#root')
);

Make App Component

App.js 파일의 구성은 아래와 같습니다. Header는 각 route로 이동하는 React-route-dom의 Link Tag가 담겨있어 항상 렌더가 되도록 위치시켰습니다. 그리고 각 Route 별 component를 설정했습니다. Feature 컴포넌트의 경우 로그인 상태에서만 확인을 할 수 있는 컴포넌트입니다. 컴포넌트에 대해서는 뒤쪽에서 살펴보겠습니다.

import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

import Header from './Header';

const App = () => (
  <div>
    <Header />
    <Route path="/" exact component={Welcome} />
    <Route path="/signup" component={Signup} />
    <Route path="/feature" component={Feature} />
    <Route path="/signout" component={Signout} />
    <Route path="/signin" component={Signin} />
  </div>
);

export default App;

Auth Reducers

애플리케이션의 state에는 auth state를 지정하기 위해 아래와 같이 reducer를 생성합니다. 유저가 signin 또는 signout하면 AUTH_USER action으로 state의 auth 값을 변경합니다.

import { AUTH_USER, AUTH_ERROR } from '../actions/types';

const INITIAL_STATE = {
  authenticated: '',
  errorMessage: ''
};

const authReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case AUTH_USER:
      return { ...state, authenticated: action.payload };
    case AUTH_ERROR:
      return { ...state, errorMessage: action.payload };
    default:
      return state;
  }
};

export default authReducer;

Make Header Component

Header Component는 앱 상단에 항상 위치해 각 Route로 이동할 Link Tag를 렌더합니다. 유저가 Signin 상태 일때 볼 수 있는 링크와, Signout 일때를 볼 수 있는 링크를 renderLinks 함수를 이용해 분간해서 렌더합니다.

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import './Header.css';

class Header extends Component {
  renderLinks() {
    if (this.props.authenticated) {
      return (
        <div>
          <Link to="/signout">Sign Out</Link>
          <Link to="/feature">Feature</Link>
        </div>
      );
    }

    return (
      <div>
        <Link to="/signup">Sign Up</Link>
        <Link to="/signin">Sign In</Link>
      </div>
    );
  }

  render() {
    return (
      <div>
        <Link to="/">Home</Link>
        {this.renderLinks()}
      </div>
    );
  }
}

export default Header;

Make Signup Component using Redux Form

React에서 form을 처리하는 여러 방법이 있지만, redux-form을 사용해 sign up 컴포넌트를 아래와 같이 생성합니다. 실제 애플리케이션에서는 Input value에 대한 validation이 필요합니다. Validation과 form 처리에 대한 여러 방법에 대해서도 좀 더 조사 후 정리해 보고자 합니다.

import React, { Component } from 'react';
import { reduxForm, Field } from 'redux-form';

class Signup extends Component {
  onSubmit = formProps => {
    this.props.onSignupCick(formProps, () => {
      this.props.history.push('/feature');
    });
  };

  render() {
    const { handleSubmit } = this.props;

    return (
      <form onSubmit={handleSubmit(this.onSubmit)}>
        <fieldset>
          <label>Email</label>
          <Field name="email" type="text" component="input" autoComplete="none" />
        </fieldset>
        <fieldset>
          <label>Password</label>
          <Field name="password" type="password" component="input" autoComplete="none" />
        </fieldset>
        <div>{this.props.errorMessage}</div>
        <button>Sign Up!</button>
      </form>
    );
  }
}

export default Signup;

Where to store JWT

Auth HOC

앞서 Feature 컴포넌트의 경우, 로그인한 유저만 접근이 가능하다고 언급했었습니다. 로그인을 하지 않은 User의 접근을 제한하기 위해서 아래와 같은 Higher Order Component를 이용할 수 있습니다.

// src/components/requireAuth.js
import React, { Component } from 'react';

export default ChildComponent => {
  class ComposedComponent extends Component {
    componentDidMount() {
      this.shouldNavigateAway();
    }

    componentDidUpdate() {
      this.shouldNavigateAway();
    }

    shouldNavigateAway() {
      if (!this.props.auth) {
        this.props.history.push('/');
      }
    }

    render() {
      return <ChildComponent {...this.props} />;
    }
  }
};

Limit access to Feature Component

위의 requireAuth Component를 이용해 Feature Component에 로그인 한 유저만 접근하도록 제한합니다.

// src/components/Feature.js
import React, { Component } from 'react';
import requireAuth from './requireAuth';

class Feature extends Component {
  render() {
    return <div>This is the feature!</div>;
  }
}

export default requireAuth(Feature);

Reference