로그인 화면 구현

이메일과 패스워드를 입력받아서 로그인을 하는 화면을 만든다.

회원가입과 마찬가지로 formik과 yup 라이브러리를 사용해서 validation을 처리한다.

import React from 'react';
import {Formik} from "formik";
import * as Yup from "yup";
import {Button, Col, Form, Row} from "react-bootstrap";
import axios from "axios";
import {toast} from "react-toastify";

const Login = (props: any) => {
  const submit = async (values: any) => {
    const {email, password} = values;
    try {
      const {data} = await axios.post('/api/auth/signin', {email, password});
      console.log(data);

      toast.success('로그인하였습니다.', {
        position: "top-center",
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
      props.history.push('/');
    } catch(e) {
      console.log(e.toString());
      toast.error('로그인에 실패하였습니다. 다시 시도하세요', {
        position: "top-center",
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
    }
  }

  return (
    <>
      <div className="d-flex justify-content-center my-3">
        <h2>로그인</h2>
      </div>

      <Row className="justify-content-center">
        <Col xs={12} sm={10} md={8} lg={6} xl={4}>
          <Formik
            initialValues={{email: '', password: ''}}
            onSubmit={submit}
            validationSchema={Yup.object().shape({
              email: Yup.string()
                .email("이메일형식으로 입력하세요")
                .required("필수필드 입니다."),
              password: Yup.string()
                .required("필수필드 입니다.")
            })}>
            {
              ({
                 values,
                 errors,
                 touched,
                 handleChange,
                 handleBlur,
                 handleSubmit,
                 isSubmitting
               }) => (<Form onSubmit={handleSubmit}>
                <Form.Group controlId="email">
                  <Form.Label>Email address</Form.Label>
                  <Form.Control name="email" placeholder="Enter email"
                                value={values.email}
                                onChange={handleChange} onBlur={handleBlur}
                                isValid={touched.email && !errors.email}
                                isInvalid={touched.email && errors.email ? true : false}/>
                  {touched.email && !errors.email &&
                    <Form.Control.Feedback type="valid">Looks good!</Form.Control.Feedback>}
                  {touched.email && errors.email &&
                    <Form.Control.Feedback type="invalid">{errors.email}</Form.Control.Feedback>}
                </Form.Group>
                <Form.Group controlId="formGroupPassword">
                  <Form.Label>Password</Form.Label>
                  <Form.Control type="password" name="password" placeholder="enter Password"
                                value={values.password}
                                onChange={handleChange} onBlur={handleBlur}
                                isValid={touched.password && !errors.password}
                                isInvalid={touched.password && errors.password ? true : false}/>
                  {touched.password && !errors.password &&
                    <Form.Control.Feedback type="valid">Looks good!</Form.Control.Feedback>}
                  {touched.password && errors.password &&
                    <Form.Control.Feedback type="invalid">{errors.password}</Form.Control.Feedback>}
                </Form.Group>
                <Button variant="primary" type="submit" disabled={isSubmitting}>
                  Submit
                </Button>
              </Form>)
            }
          </Formik>
        </Col>
      </Row>
    </>
  );
}

export default Login;

jwt 토큰 정보 저장

로그인을 성공하면 서버에서 jwt 토큰을 받는다. 그것을 store에 저장하고 redux-persist 모듈을 사용해서 storage에도 저장한다.

redux에 저장하는것은 영구적인 저장소가 아니다. 만일 새로고침하게 되면 store에 저장된 정보는 메모리에 저장된것이기 때문에 모두 없어지게 된다. 따라서 영구적으로 저장하기 위해서는 storage에 저장해야 하는데 redux-persistence 모듈이 그것을 도와준다.

먼저 필요한 모듈을 설치한다. redux-persist가 local storage에 저장하는 모듈이다. jwt-decode는 jwt를 파싱하는데 도움을 주는 라이브러리이다. querystring 은 query parameter를 쉽게 파싱할 수 있도록 도와주는 모듈이다.

yarn add redux react-redux redux-persist jwt-decode querystring

redux 폴더에 AuthReducer를 먼저 생성한다.

src/redux/reducers/AuthReducer.js

const SET_TOKEN = 'set_token';

const AuthInitialState = {
  token: null
}

export const setToken = (token) => ({
  type: SET_TOKEN,
  token
})

export const AuthReducer = (state = AuthInitialState, action) => {
  switch(action.type) {
    case SET_TOKEN:
      return {
        ...state,
        token: action.token
      }
    default:
      return state;
  }
}

모든 reducer를 combine하는 allReducers를 생성한다.

src/redux/reducers/index.js

import {persistReducer} from "redux-persist";
import storage from "redux-persist/lib/storage";
import {AuthReducer} from "./AuthReducer";
import {combineReducers} from "redux";

const persistConfig = {
  key: "root",
  // localStorage에 저장합니다.
  storage,
  // auth, board, studio 3개의 reducer 중에 auth reducer만 localstorage에 저장합니다.
  // whitelist: ["auth"]
  // blacklist -> 그것만 제외합니다
};

const allReducers = combineReducers({
  Auth: AuthReducer
});

export default persistReducer(persistConfig, allReducers);

store를 생성한다.

src/redux/store.js

import {createStore} from "redux";
import allReducers from "./reducers";

export const store = createStore(allReducers,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

이제 Login에서 로그인이 성공하면 서버에서리턴된 token을 store에 저장한다. setToken이라는 액션 creator를 사용해서 액션을 생성한 다음 액션을 dispatch한다. 그러면 store에 token이 저장되며 redux-persist에 의해서 local storage에도 저장되게 된다.

const Login = (props: any) => {
  console.log(props);
  const dispatch = useDispatch();
  const submit = async (values: any) => {
    const {email, password} = values;
    try {
      const {data} = await axios.post('/api/auth/signin', {email, password});
      console.log(data);

      dispatch(setToken(data.token))
      const {redirectUrl} = queryString.parse(props.location.search);
      if (redirectUrl) {
        props.history.push(redirectUrl);
      } else {
        props.history.push('/');
      }

      toast.success('로그인하였습니다.', {
        position: "top-center",
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
      props.history.push('/');
    } catch(e) {
      console.log(e.toString());
      toast.error('로그인에 실패하였습니다. 다시 시도하세요', {
        position: "top-center",
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
    }
  }
  ...
}