June 24th 2019
email, password 기반의 authentication server를 다른 포스팅을 통해서 만들어 보았습니다. 이 서버를 이용해 로그인 기능만 갖춘 간단한 앱을 만들며 프론트엔드에서 로그인 처리를 하는 한 방법에 대해 확인해 보겠습니다.
Contents
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
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')
);
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;
애플리케이션의 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;
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;
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;
앞서 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} />;
}
}
};
위의 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);