Login & Register with the MERN Stack (part5)

PART5. Frontend 구현 (redux setting)



In this part

  • create component for Login, Register
  • set up redux

Create Component

i. auth component

auth를 위한 Login.jsRegister.js파일을 auth 폴더 내에 생성한다.

mkdir auth && cd auth && touch Login.js Register.js

1. Register.js

email, name, password, confirm password를 입력받고 제출하는 폼을 만든다. 간단한 스타일도 미리 적용했다.

import React, { useState } from "react";
import { Link } from "react-router-dom";

import styled from "styled-components";

const StyledOuterContainer = styled.div`
  box-sizing: border-box;
  display: flex;
  width: 100%;
  height: 500px;
`;
const StyledInnerContainer = styled.div`
  box-sizing: border-box;
  display: block;
  width: 50%;
  height: 100%;
`;

const StyledForm = styled.form`
  padding: 50px 0px 0px 50px;
`;

const StyledImg = styled.img`
  max-width: 100%;
  height: 100%;
  overflow: hidden;
  box-shadow: 1px 2px 10px black;
  clip-path: polygon(-20% 0%, 100% 0%, 100% 100%, -20% 100%);
`;

const Styledh3 = styled.h3`
  font: bold 40px/1 sans-serif;
  text-shadow: 1px 1px 1px #999da0;
  margin: 20px 0px;
  padding: 50px 0px 0px 50px;
`;

const Styledp = styled.p`
  font: 15px sans-serif;
  color: #999da0;
  padding: 0px 0px 0px 50px;
`;

const StyledLink = styled(Link)`
  font: 15px/1 sans-serif;
  color: #63c5da;
  text-decoration-line: none;
`;

const StyledDiv = styled.div`
  height: 50px;
`;

const StyledInput = styled.input`
  border: none;
  height: 30px;
  width: 400px;
  border-bottom: 1px #ebecf0 solid;
  font: 13px sans-serif;

  &:focus {
    outline: none;
    box-shadow: 0px 1px 2px #999da0;
  }
`;

const StyledButton = styled.button`
  all: unset;
  box-sizing: border-box;
  display: block;
  width: 150px;
  margin: 10px auto;
  text-align: center;
  padding: 10px;
  color: white;
  background-color: #3e4248;
  text-decoration-line: none;
  border-radius: 3px;
  box-shadow: 1px 1px 2px black;
  cursor: pointer;
`;

const Register = () => {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [password, setPassword] = useState("");
  const [password2, setPassword2] = useState("");

  const onChangeEmail = (e) => {
    setEmail(e.target.value);
  };

  const onChangeName = (e) => {
    setName(e.target.value);
  };
  const onChangePassword = (e) => {
    setPassword(e.target.value);
  };

  const onChangePassword2 = (e) => {
    setPassword2(e.target.value);
  };

  const onSubmit = (e) => {
    e.preventDefault();

    const newUser = {
      name: name,
      email: email,
      password: password,
      password2: password2,
    };

    console.log(newUser);
  };

  return (
    <StyledOuterContainer>
      <StyledInnerContainer>
        <Styledh3>Register</Styledh3>
        <Styledp>
          Already have an account? <StyledLink to="/login">Log in</StyledLink>
        </Styledp>
        <StyledForm onSubmit={onSubmit}>
          <StyledDiv>
            <StyledInput
              type="email"
              placeholder="Email"
              value={email}
              onChange={onChangeEmail}
            />
          </StyledDiv>
          <StyledDiv>
            <StyledInput
              type="text"
              placeholder="Name"
              value={name}
              onChange={onChangeName}
            />
          </StyledDiv>
          <StyledDiv>
            <StyledInput
              type="password"
              placeholder="Password"
              value={password}
              onChange={onChangePassword}
            />
          </StyledDiv>
          <StyledDiv>
            <StyledInput
              type="password"
              placeholder="Confirm Password"
              value={password2}
              onChange={onChangePassword2}
            />
          </StyledDiv>
          <StyledButton type="submit">Sign up</StyledButton>
        </StyledForm>
      </StyledInnerContainer>
      <StyledInnerContainer>
        <StyledImg src="https://cdn.pixabay.com/photo/2016/03/27/07/32/clouds-1282314_960_720.jpg"></StyledImg>
      </StyledInnerContainer>
    </StyledOuterContainer>
  );
};

export default Register;

register화면은 다음과 같다.

입력한 뒤 SIGN UP 버튼을 누르면 콘솔창에 잘 나오는 것을 확인할 수 있다.


2. Login.js

Login.jsRegister.js와 비슷하다. email, password를 입력받는 폼을 만든다.

import React, { useState } from "react";
import { Link } from "react-router-dom";

import styled from "styled-components";

const StyledOuterContainer = styled.div`
  box-sizing: border-box;
  display: flex;
  width: 100%;
  height: 500px;
`;
const StyledInnerContainer = styled.div`
  box-sizing: border-box;
  display: block;
  width: 50%;
  height: 100%;
`;

const StyledForm = styled.form`
  padding: 50px 0px 0px 50px;
`;

const StyledImg = styled.img`
  max-width: 100%;
  height: 100%;
  overflow: hidden;
  box-shadow: 1px 2px 10px black;
  clip-path: polygon(-20% 0%, 100% 0%, 100% 100%, -20% 100%);
`;

const Styledh3 = styled.h3`
  font: bold 40px/1 sans-serif;
  text-shadow: 1px 1px 1px #999da0;
  margin: 20px 0px;
  padding: 50px 0px 0px 50px;
`;

const Styledp = styled.p`
  font: 15px sans-serif;
  color: #999da0;
  padding: 0px 0px 0px 50px;
`;

const StyledLink = styled(Link)`
  font: 15px/1 sans-serif;
  color: #63c5da;
  text-decoration-line: none;
`;

const StyledDiv = styled.div`
  height: 50px;
`;

const StyledInput = styled.input`
  border: none;
  height: 30px;
  width: 400px;
  border-bottom: 1px #ebecf0 solid;
  font: 13px sans-serif;

  &:focus {
    outline: none;
    box-shadow: 0px 1px 2px #999da0;
  }
`;

const StyledButton = styled.button`
  all: unset;
  box-sizing: border-box;
  display: block;
  width: 150px;
  margin: 10px auto;
  text-align: center;
  padding: 10px;
  color: white;
  background-color: #3e4248;
  text-decoration-line: none;
  border-radius: 3px;
  box-shadow: 1px 1px 2px black;
  cursor: pointer;
`;

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const onChangeEmail = (e) => {
    setEmail(e.target.value);
  };

  const onChangePassword = (e) => {
    setPassword(e.target.value);
  };

  const onSubmit = (e) => {
    e.preventDefault();

    const userData = {
      email: email,
      password: password,
    };

    console.log(userData);
  };

  return (
    <StyledOuterContainer>
      <StyledInnerContainer>
        <Styledh3>Login</Styledh3>
        <Styledp>
          Dont't have an account?{" "}
          <StyledLink to="/register">Register</StyledLink>
        </Styledp>
        <StyledForm onSubmit={onSubmit}>
          <StyledDiv>
            <StyledInput
              type="email"
              placeholder="Email"
              value={email}
              onChange={onChangeEmail}
            />
          </StyledDiv>
          <StyledDiv>
            <StyledInput
              type="password"
              placeholder="Password"
              value={password}
              onChange={onChangePassword}
            />
          </StyledDiv>
          <StyledButton type="submit">LOGIN</StyledButton>
        </StyledForm>
      </StyledInnerContainer>
      <StyledInnerContainer>
        <StyledImg src="https://cdn.pixabay.com/photo/2016/03/27/07/32/clouds-1282314_960_720.jpg"></StyledImg>
      </StyledInnerContainer>
    </StyledOuterContainer>
  );
};

export default Login;

login화면은 다음과 같다.

입력한 뒤 LOGIN 버튼을 누르면 콘솔창에 잘 나오는 것을 확인할 수 있다.


3. App.js

먼저 Routes를 이용하기 위해 Route,와 Routes를 가져온다.

import { Route, Routes } from "react-router-dom";

그리고 앞에서 만든 Register, Login 컴포넌트를 가져온다.

import Register from "./components/auth/Register";
import Login from "./components/auth/Login";

그다음 화면에서 Navbar 아래에서 각 url에 맞는 화면이 나타나야 한다. 따라서 Route를 이용하여 각 url에 component를 설정해준다. 완성된 코드는 아래와 같다.

import React, { Component } from "react";
import { Route, Routes } from "react-router-dom";

import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";

class App extends Component {
  render() {
    return (
      <div className="App">
        <Navbar />
        <Routes>
          <Route path="/" element={<Landing />} />
          <Route path="/register" element={<Register />} />
          <Route path="/login" element={<Login />} />
        </Routes>
      </div>
    );
  }
}
export default App;

각 버튼을 누르거나 url을 입력했을때 화면이 잘 나타나는 것을 확인 할 수 있다.


Setting up Redux

i. edit App.js

App.js에 redux를 위해 Provider와 store를 가져온다.

import React, { Component } from "react";
import { Route, Routes } from "react-router-dom";

import { Provider } from "react-redux";
import store from "./store";

import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <div className="App">
          <Navbar />
          <Routes>
            <Route path="/" element={<Landing />} />
            <Route path="/register" element={<Register />} />
            <Route path="/login" element={<Login />} />
          </Routes>
        </div>
      </Provider>
    );
  }
}
export default App;

ii. setting up redux file structure

먼저 store.js 파일을 만든다.

mkdir store.js

다음 actions 폴더에 authActions.jstypes.js파일을 만든다.

mkdir actions && cd actions && touch authActions.js && touch types.js

reducers 폴더에 index.js, authReducer.js, errorReducer.js 파일을 만든다.

mkdir reducers && cd reducers && touch authReducer.js && touch errorReducer.js && touch index.js

iii. setting up store

store는 redux에서 가장 핵심적인 인스턴스이다. 현재 상태를 내장하고있고, subscribe 중인 함수들이 상태가 업데이트 될 때 마다 다시 실행되게 한다.

store.js 에서 sotre를 create 한다.

import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";

const initialState = {};

const middleware = [thunk];

const store = createStore(
  () => [],
  initialState,
  compose(applyMiddleware(...middleware))
);
export default store;

iiii. define actions

action은 store라는 저장소에 정보를 전달하기 위한 데이터의 묶음이다.
따라서 types.js에 다음을 export한다.

export const GET_ERRORS = "GET_ERRORS";
export const USER_LOADING = "USER_LOADING";
export const SET_CURRENT_USER = "SET_CURRENT_USER";

v. create reducers

Reducer들은 상태변화들을 어떻게 할지 관리하는 정보가 담겨있다. 따라서 Store와 정보를 주고받는 역할을 한다.


1. authReducer.js

먼저 types.js에서 actions를 import 한다. 다음 initialState를 설정한다. 마지막으로 action type에 따라 state의 변화를 정의한다.

import { SET_CURRENT_USER, USER_LOADING } from "../actions/types";

const isEmpty = require("is-empty");
const initialState = {
  isAuthenticated: false,
  user: {},
  loading: false,
};

export default function (state = initialState, action) {
  switch (action.type) {
    case SET_CURRENT_USER:
      return {
        ...state,
        isAuthenticated: !isEmpty(action.payload),
        user: action.payload,
      };
    case USER_LOADING:
      return {
        ...state,
        loading: true,
      };
    default:
      return state;
  }
}

2. errorReducer.js

actions가 GET_ERRORS일 때 처리를 해준다.

import { GET_ERRORS } from "../actions/types";

const initialState = {};

export default function (state = initialState, action) {
  switch (action.type) {
    case GET_ERRORS:
      return action.payload;
    default:
      return state;
  }
}

3. index.js

combineReducers를 사용하여 authReducer와 errorReducer를 하나의 Reducer로 관리할 수 있게 만든다.

import { combineReducers } from "redux";
import authReducer from "./authReducer";
import errorReducer from "./errorReducer";

export default combineReducers({
  auth: authReducer,
  errors: errorReducer,
});

vi. set auth token

Authorization을 관리하기 위한 auth token을 setAuthToken.js에 정의한다.
먼저 util 폴더를 만들고 setAuthToken.js 파일을 생성한다.

mkdir utils && cd utils && touch setAuthToken.js

다음 코드를 작성한다.

import axios from "axios";

const setAuthToken = (token) => {
  if (token) {
    // 만약 로그인되어 있다면 authorization token을 모든 request에 apply한다.
    axios.defaults.headers.common["Authorization"] = token;
  } else {
    // 로그인 안되어있으면 삭제
    delete axios.defaults.headers.common["Authorization"];
  }
};
export default setAuthToken;

vii. create actions

먼저 types.js에서 actions와 dependencies를 import한다. 다음 axios를 이용하여 각 action에 HTTPRequest를 만든다. 마지막으로 dispatch를 이용하여 actions를 reducers에 전달한다.

import axios from "axios";
import setAuthToken from "../utils/setAuthToken";
import jwt_decode from "jwt-decode";

import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from "./types";

// Register User
export const registerUser = (userData, history) => (dispatch) => {
  axios
    .post("/api/users/register", userData)
    .then((res) => history.push("/login")) // register가 성공하면 login창으로 redirect
    .catch((err) =>
      dispatch({
        type: GET_ERRORS,
        payload: err.response.data,
      })
    );
};

// Login User
export const loginUser = (userData) => (dispatch) => {
  axios
    .post("/api/users/login", userData)
    .then((res) => {
      //localStorage에 token 저장
      const { token } = res.data;
      localStorage.setItem("jwtToken", token);

      // set authToken
      setAuthToken(token);

      // decode token
      const decoded = jwt_decode(token);

      // Set current user
      dispatch(setCurrentUser(decoded));
    })
    .catch((err) =>
      dispatch({
        type: GET_ERRORS,
        payload: err.response.data,
      })
    );
};

// set current user
export const setCurrentUser = (decoded) => {
  return {
    type: SET_CURRENT_USER,
    payload: decoded,
  };
};

// set user loading
export const setUserLoading = () => {
  return {
    type: USER_LOADING,
  };
};

// logout
export const logoutUser = () => (dispatch) => {
  // local storage에서 token 제거
  localStorage.removeItem("jwtToken");

  // auth token 제거
  setAuthToken(false);

  // current user 비우기
  dispatch(setCurrentUser({}));
};

viii. set rootReducer

store.js에 rootReducer를 추가한다.

import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};

const middleware = [thunk];

const store = createStore(
  rootReducer,
  initialState,
  compose(applyMiddleware(...middleware))
);
export default store;

redux 세팅 끝~~

좋은 웹페이지 즐겨찾기