Login with Redux Pattern

seoyeon·2023년 5월 21일
2

리덕스 패턴이란

  • MVC 패턴을 대체하기 위해서 페이스북이 사용한 Flux 패턴을 살짝 바꾼 것
☝ 순서

View → Action → Dispatcher → Store (Middleware → Reducer) → View

  • 한방향으로 흐르기 때문에 데이터의 흐름을 예측하기 쉬움 → 관리가 편함
  • 데이터가 불변하여 예측하기 쉽고, 이전 상태로 되돌리기 쉬움

로그인 예시

  • 로그인 버튼 클릭
  • View : 로그인 form이 있는 화면
  • Action : 폼에 로그인 Action 연결
    • Dispatcher을 통해서 데이터가 Action에서 Store로 넘어감
  • Store
    • Middleware : Reducer에 가기 전 Action을 조작하고 처리된 데이터를 View로 넘겨줌
    • Reducer : 데이터를 처리

Login page ui 만들기

<div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        width: "100%",
        height: "100vh",
      }}
    >
      <form
        style={{
          display: "flex",
          flexDirection: "column",
        }}
        **onSubmit**={onSubmitHandler}
      >
        <label>Email</label>
        <input type="email" value={Email} **onChange**={**onEmailHandler**} />
        <label>Password</label>
        <input type="password" value={Password} onChange={**onPasswordHandler**} />

        <br />
        <button>Login</button>
      </form>
    </div>
  • 이메일과 페스워드 인풋을 폼 태그로 감싸주기

이메일 패스워드 변경하면 state값 세팅

function LoginPage() {

  const [Email, setEmail] = useState("");
  const [Password, setPassword] = useState("");

  const onEmailHandler = (event) => {
    setEmail(event.currentTarget.value);
  };

  const onPasswordHandler = (event) => {
    setPassword(event.currentTarget.value);
  };

  const onSubmitHandler = (event) => {
    event.preventDefault();

  };
  • 사용자가 이메일, 패스워드 변경하면 onEmailHandelr onPasswordHandler 가 실행되어서
    이메일과 패스워드 변경된 값으로 세팅
  • event.preventDefault() : 이벤트의 기본동작을 막아줌 (페이지 리프레시)

서버에 보낼 값 세팅

let body = {
      email: Email,
      password: Password,
    };

리덕스 패턴을 사용하는 컴포넌트 만들기

☝ react-redux 다운로드
npm install redux react-redux

LoginPage.js

function LoginPage() {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const onSubmitHandler = (event) => {
    event.preventDefault();

    let body = {
      email: Email,
      password: Password,
    };

    dispatch(loginUser(body)).then((response) => {
      if (response.payload.loginSuccess) {
        // props.history.push("/");
        navigate("/");
      } else {
        alert(response.payload.message);
      }
    });
  };

login Action 만들기

user_action.js

import axios from "axios";
import { LOGIN_USER } from "./types";
export function loginUser(dataToSubmit) {
  const request = axios
    // 백엔드 서버 url에 dataToSubmit 데이터 보내주기
    .post("/api/users/login", dataToSubmit)
    .then((response) => response.data);

  return {
    // redux로 옮겨주기
    type: LOGIN_USER,
    payload: request,
  };
}
  • then 메소드의 역할
    • then() 메서드는 Promise 를 리턴하고 두 개의 콜백 함수를 인수로 받음

login Action 순서

  • LoginPage로부터 데이터(email, password)를 받아옴(dataToSubmit)
  • axios를 사용하여서 백엔드 서버 url(/api/users/login)에 데이터(dataToSubmit)보내주기
    • 백엔드에서 로그인 과정 거쳐서 response를 받는 과정

server/models/User.js

        userSchema.methods.comparePassword = function (plainPassword, cb) {
          //plainPassword 1234567    암호회된 비밀번호 $2b$10$l492vQ0M4s9YUBfwYkkaZOgWHExahjWC
          bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
            // 사용자가 입력한 비번(plainPassword)와 db에 저장된 비번과 비교
            // 틀리고, 에러가 나면 콜백함수에 err를 반환하고,
            // 같다면 콜백함수에 null값과 true값이 있는 isMatch를 반환
            if (err) return cb(err);
            cb(null, isMatch);
          });
        };
        
        userSchema.methods.generateToken = function (cb) {
          var user = this;
          // console.log('user._id', user._id)
        
          // jsonwebtoken을 이용해서 token을 생성하기
          var token = jwt.sign(user._id.toHexString(), "secretToken");
          // user._id + 'secretToken' = token
          // ->
          // 'secretToken' -> user._id
        
          user.token = token;
          user.save(function (err, user) {
            if (err) return cb(err);
            cb(null, user);
          });
        };

server/index.js

        app.post("/api/users/login", (req, res) => {
          // 요청된 이메일틀 데이터베이스에서 있는지 찾아보기
          // findOne() : 요소를 찾는 몽고DB 메소드
          User.findOne({ email: req.body.email }, (err, user) => {
            // 이메일에 해당하는 유저가 없으면
            if (!user) {
              return res.json({
                loginSuccess: false,
                message: "제공된 이메일에 해당하는 유저가 없습니다.",
              });
            }
            // 요청된 이메일이 데이터베이스에 있다면 비밀번호가 맞는 비밀번호인지 확인
            user.comparePassword(req.body.password, (err, isMatch) => {
              // isMatch가 없으면 비번이 틀림
              if (!isMatch)
                return res.json({
                  loginSuccess: false,
                  message: "비밀번호가 틀렸습니다.",
                });
              // 비밀번호까지 맞다면 토큰 생성
              user.generateToken((err, user) => {
                // status(400)->에러
                if (err) return res.status(400).send(err);
        
                // 토큰을 저장(쿠키, 로컬스토리지)
                // status(200)->성공
                res
                  .cookie("x_auth", user.token)
                  .status(200)
                  .json({ loginSuccess: true, userId: user._id });
              });
            });
          });
        });
  • 이메일에 해당하는 유저가 없으면 loginSuccess가 false인 res(.json)를 리턴
  • 비밀번호가 틀리면 loginSuccess가 false인 res(.json)를 리턴
  • 비밀번호가 맞으면 토큰을 생성하고, loginSuccess가 true이고, userID가 있는
    res(.json)를 리턴
  • 백엔드에서 받은 response.data를 request에 저장
  • types 파일에서 받은 type과 백엔드 서버에서 받은 request를 payload로 리턴
  • _actions/types.js
        export const LOGIN_USER = "login_user";
  • 폼을 submit하면 dispatch 메소드(Dispatcher) 실행
    • LoginPage 컴포넌트의 onSubmitHandler 안에 있는 dispatch가 실행되는것!
    • dispatch(loginUser(body))
      • Dispatcher을 통해서 데이터가 login Action에서 Store로 넘어감

login reducer 만들기

_reducers/user_reducer.js

import { LOGIN_USER } from "../_actions/types";

export default function f(state = {}, action) {
  switch (action.type) {
    case LOGIN_USER:
      return { ...state, loginSuccess: action.payload };

    default:
      return state;
  }
}
  • Reducer은 Action별로 state를 어떻게 바꿀지 결정함!
  • (중요) 반드시 새로운 객체를 반환 해야됨!!!
  • {…state} ⇒ 기존 state(비어있음)를 복사하는것
    ⇒ 새로운 객체를 만들어야 현재, 이전 상태가 구분되기에 이전 상태로 쉽게 되돌릴 수 있음
  • state만 바꾸면 알아서 View도 바뀜
  • LOGIN_USER Action이 발생하면 loginSuccess에는 서버로부터 받은
    유저 정보(action.payload)를 넣어줌

_reducers/index.js

import { combineReducers } from "redux";
import user from "./user_reducer";

// reducer가 나눠져 있는데 combineReducer을 이용하여
// root reducer에서 하나로 합침

const rootReducer = combineReducers({
  user,
});

export default rootReducer;
  • Reducer는 여러 개를 사용할 수 있기 때문에
    redux 패키지로부터 combineReducers 함수를 불러워서 합쳐주기
  • 위에서 만든 reducer을 user로 가져오기

Store

☝ store에서 제공하는 dispatch 메소드로 Action을 Store로 넘김 Store에서 View로는 props를 통해 데이터가 넘어감

다시 View

dispatch(loginUser(body)).then((response) => {
      if (response.payload.loginSuccess) {
        // props.history.push("/");
        navigate("/");
      } else {
        alert(response.payload.message);
      }
    });
  • dispatch가 끝났으면 받아온 response값을 활용하여서 로그인에 성공하면 홈페이지(/)로 이동
  • 로그인에 성공하지 못하면 json의 에러 메세지를 alert 해주기
⚠️ LoginPage 함수가 props를 못받아오는 이슈 발생 해결방법

props.history.push("/");

react-router-dom v6부터 useHistory 에서 useNavigate로 바뀜

  1. useNavigate import
import { useNavigate } from "react-router-dom";
  1. navigete로 이동
const navigate = useNavigate();

dispatch(loginUser(body)).then((response) => {
      if (response.payload.loginSuccess) {
        // props.history.push("/");
        navigate("/");
      } else {
        alert(response.payload.message);
      }
    });

참고

https://www.zerocho.com/category/React/post/57b60e7fcfbef617003bf456

profile
항상 질문하는 개발자가 되고 싶습니다✋

0개의 댓글