관리 메뉴

거니의 velog

(35) 리덕스 툴킷 4 본문

SpringBoot_React 풀스택 프로젝트

(35) 리덕스 툴킷 4

Unlimited00 2024. 3. 6. 19:08

3. 비동기 호출과 createAsyncThunk()

* useSelector() 와 useDispatch() 로 애플리케이션 상태를 이용할 때 필요한 또 다른 기능은 비동기 처리이다. 위의 예제는 API 서버를 통해서 로그인/로그아웃을 처리해야 하는 작업로그인 시에 API 서버를 연동한 처리가 필요하다.

* 과거 리덕스의 경우 redux-thunk나 redux-saga라는 추가적인 라이브러리를 사용해서 비동기 처리를 했지만, 리덕스 툴킷은 createAsyncThunk() 라는 기능을 사용해서 비동기 통신 상태에 따른 처리가 가능하다.

* API 서버와의 통신을 위해 프로젝트 내에 api 폴더 내에 memberApi.js 파일을 추가한다.

import axios from "axios";
import { API_SERVER_HOST } from "./todoApi";

const host = `${API_SERVER_HOST}/api/member`;

export const loginPost = async (loginParam) => {
  const header = { headers: { "Content-Type": "x-www-form-urlencoded" } };

  const form = new FormData();
  form.append("username", loginParam.email);
  form.append("password", loginParam.pw);

  const res = await axios.post(`${host}/login`, form, header);

  return res.data;
};

* loginSlice에서는 createAsyncThunk() 를 사용해서 비동기 통신을 호출하는 함수를 작성하고 비동기 호출의 상태에 따라 동작하는 extraReducers 를 추가해 준다.

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { loginPost } from "../api/memberApi";

const initState = {
  email: "",
};

export const loginPostAsync = createAsyncThunk("loginPostAsync", (param) => {
  return loginPost(param);
});

const loginSlice = createSlice({
  name: "LoginSlice",
  initialState: initState,
  reducers: {
    login: (state, action) => {
      console.log("login.....");

      // {email, pw} 로 구성
      const data = action.payload;

      // 새로운 상태
      return { email: data.email };
    },
    logout: (state, action) => {
      console.log("logout....");

      // 값을 초기화
      return { ...initState };
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginPostAsync.fulfilled, (state, action) => {
        console.log("fulfilled");
      })
      .addCase(loginPostAsync.pending, (state, action) => {
        console.log("pending");
      })
      .addCase(loginPostAsync.rejected, (state, action) => {
        console.log("rejected");
      });
  },
});

export const { login, logout } = loginSlice.actions;

export default loginSlice.reducer;

* 변경된 loginSlice를 살펴보면 createAsyncThunk()를 사용해서 memberApi.js에 선언된 loginPost()를 호출하도록 구성하고, 아래쪽에 비동기 통신의 상태(fulfilled, 완료), pending(처리중), rejected(에러)에 따라 동작하는 함수를 작성하고 있다.

* API 서버를 실행한 상태에서 이전에 Postman에서 처리했던 /api/member/login 을 호출하도록 LoginComponent를 수정한다.

import React, { useState } from "react";
import { useDispatch } from "react-redux";
//import { login } from "../../slices/loginSlice";
import { loginPostAsync } from "../../slices/loginSlice";

(...)

const LoginComponent = () => {
  const [loginParam, setLoginParam] = useState({ ...initState });
  const dispatch = useDispatch();

  const handleChange = (e) => {
    loginParam[e.target.name] = e.target.value;
    setLoginParam({ ...loginParam });
  };
  const handleClickLogin = (e) => {
    // dispatch(login(loginParam)); // 동기화된 호출
    dispatch(loginPostAsync(loginParam)); // loginSlice의 비동기 호출
  };

  return (
    (...)
  );
};

export default LoginComponent;

* 브라우저에서 로그인을 수행하면 비동기 통신이 이루어지는 것을 확인할 수 있고 API 서버에서 발생한 로그인 결과를 확인할 수 있다(실행을 위해서는 이전 장에서 작성된 API 서버를 실행해 주어야 한다).

* 콘솔창을 확인하면 서버와 통신 중인 pending과 완료를 의미하는 fulfilled가 출력된다.


(1) 로그인 후처리

* loginSlice에서는 로그인 후에 애플리케이션 상태를 처리해 주어야 한다. API 서버에서 로그인 시에 전송되는 데이터들을 상태 데이터로 보관하도록 처리한다.

  extraReducers: (builder) => {
    builder
      .addCase(loginPostAsync.fulfilled, (state, action) => {
        console.log("fulfilled");

        const payload = action.payload;
        return payload;
      })
      .addCase(loginPostAsync.pending, (state, action) => {
        console.log("pending");
      })
      .addCase(loginPostAsync.rejected, (state, action) => {
        console.log("rejected");
      });
  },

* 브라우저에서 로그인을 진행하기 전과 로그인 후는 메뉴 구성이 달라지는 것을 확인할 수 있다.

* 잘못된 로그인의 경우 ["error" : "..."] 메시지가 전송된다.


[unwrap()을 이용한 후처리]

* LoginComponent에서 비동기 호출 이후에 처리된 결과를 받아보려면 unwrap()을 사용할 수 있다. 예를 들어 위와 같이 error 값이 전달되는 것을 확인해야 하는 경우나 로그인 결과를 받아야 하는 경우에 유용하다.

  const handleClickLogin = (e) => {
    // dispatch(login(loginParam)); // 동기화된 호출
    dispatch(loginPostAsync(loginParam)) // loginSlice의 비동기 호출
      .unwrap()
      .then((data) => {
        console.log("after unwrap...");
        console.log(data);
      });
  };

* 정상적으로 로그인이 처리되면 LoginComponent에서는 아래와 같은 로그를 출력한다.

* 로그인에 실패하는 경우에는 아래와 같은 메시지가 출력된다.

* 우선 로그인 결과에 맞게 경고창(alert)을 보이도록 수정한다.

  const handleClickLogin = (e) => {
    // dispatch(login(loginParam)); // 동기화된 호출
    dispatch(loginPostAsync(loginParam)) // loginSlice의 비동기 호출
      .unwrap()
      .then((data) => {
        console.log("after unwrap...");
        console.log(data);
        if (data.error) {
          alert("이메일과 패스워드를 다시 확인하세요.");
        } else {
          alert("로그인 성공");
        }
      });
  };


[로그인 후 이동 처리]

* 정상적으로 로그인이 되면 화면은 / 경로로 이동하도록 useNavigate()를 이용하는 코드를 추가한다.

import React, { useState } from "react";
import { useDispatch } from "react-redux";
//import { login } from "../../slices/loginSlice";
import { loginPostAsync } from "../../slices/loginSlice";
import { useNavigate } from "react-router-dom";

const initState = {
  email: "",
  pw: "",
};

const LoginComponent = () => {
  const [loginParam, setLoginParam] = useState({ ...initState });
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const handleChange = (e) => {
    loginParam[e.target.name] = e.target.value;
    setLoginParam({ ...loginParam });
  };
  const handleClickLogin = (e) => {
    // dispatch(login(loginParam)); // 동기화된 호출
    dispatch(loginPostAsync(loginParam)) // loginSlice의 비동기 호출
      .unwrap()
      .then((data) => {
        console.log("after unwrap...");
        if (data.error) {
          alert("이메일과 패스워드를 다시 확인하세요.");
        } else {
          alert("로그인 성공");
          navigate({ pathname: `/` }, { replace: true }); // 뒤로 가기 했을 때 로그인 화면을 볼 수 없게 처리한다.
        }
      });
  };

  return (
    (...)
  );
};

export default LoginComponent;

* 브라우저를 이용해서 결과를 확인하면 로그인 후 자동으로 이동하게 된다.

* 로그인/로그아웃 후에는 '/' 경로로 이동하는 처리가 필요하다. 로그인 관련 이동 로직은 공통적으로 이용하는 경우가 많기 때문에 커스텀 훅스로 작성해서 사용하는 것이 편리하다.


(2) 로그인 관련 기능 처리를 위한 커스텀 훅

* 로그인이나 로그인 상태의 체크 등은 많은 컴포넌트에서 공통적으로 사용할 수 있는 기능이므로 이를 커스텀 훅으로 작성해 두면 재사용이 가능해진다.

* hooks 폴더에 useCustomLogin.js 를 추가한다.

import { useDispatch, useSelector } from "react-redux";
import { Navigate, useNavigate } from "react-router-dom";
import { loginPostAsync, logout } from "../slices/loginSlice";

const useCustomLogin = () => {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const loginState = useSelector((state) => state.loginSlice); // 로그인 상태
  const isLogin = loginState.email ? true : false; // 로그인 여부

  // 로그인 함수
  const doLogin = async (loginParam) => {
    const action = await dispatch(loginPostAsync(loginParam));
    return action.payload;
  };
  // 로그아웃 함수
  const doLogout = () => {
    dispatch(logout());
  };
  // 페이지 이동
  const moveToPath = (path) => {
    navigate({ pathname: path }, { replace: true });
  };
  // 로그인 페이지로 이동
  const moveToLogin = () => {
    navigate({ pathname: "/member/login" }, { replace: true });
  };
  // 로그인 페이지로 이동 컴포넌트
  const moveToLoginReturn = () => {
    return <Navigate replace to="/member/login" />;
  };

  return {
    loginState,
    isLogin,
    doLogin,
    doLogout,
    moveToPath,
    moveToLogin,
    moveToLoginReturn,
  };
};

export default useCustomLogin;

* useCustomLogin을 이용하면 LoginComponent의 코드는 조금 더 단순해 진다.

import React, { useState } from "react";
import useCustomLogin from "../../hooks/useCustomLogin";

const initState = {
  email: "",
  pw: "",
};

const LoginComponent = () => {
  const [loginParam, setLoginParam] = useState({ ...initState });
  const { doLogin, moveToPath } = useCustomLogin();

  const handleChange = (e) => {
    loginParam[e.target.name] = e.target.value;
    setLoginParam({ ...loginParam });
  };
  const handleClickLogin = (e) => {
    doLogin(loginParam) // loginSlice의 비동기 호출
      .then((data) => {
        console.log(data);
        if (data.error) {
          alert("이메일과 패스워드를 다시 확인하세요");
        } else {
          alert("로그인 성공");
          moveToPath("/");
        }
      });
  };

  return (
    (...)
  );
};

export default LoginComponent;

(3) 로그인이 필요한 페이지

* useCustomLogin을 이용하면 로그인 체크가 필요한 페이지에서 몇 줄의 코드만으로 로그인 체크가 가능하다.

* 예를 들어 pages에 있는 AboutPage가 로그인한 사용자만이 볼 수 있는 페이지라면 아래와 같이 로그인 체크 및 이동을 처리할 수 있다.

import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import useCustomLogin from "../hooks/useCustomLogin";

const AboutPage = () => {
  const { isLogin, moveToLoginReturn } = useCustomLogin();

  if (!isLogin) {
    return moveToLoginReturn();
  }

  return (
    <BasicLayout>
      <div className="text-3xl">About Page</div>
    </BasicLayout>
  );
};

export default AboutPage;

* 브라우저에서 /about이라는 경로를 호출하면 /member/login 으로 이동하는 것을 볼 수 있다.


(4) 로그아웃 처리

* 로그아웃은 비동기 호출이 아니라 loginSlice의 logout()을 그대로 활용할 수 있다. 비동기 처리가 아니기 때문에 로그아웃 처리 후 / 경로로 이동하도록 구성한다.

import React from "react";
import useCustomLogin from "../../hooks/useCustomLogin";

const LogoutComponent = () => {
  const { doLogout, moveToPath } = useCustomLogin();

  const handleClickLogout = () => {
    doLogout();
    alert("로그아웃 되었습니다.");
    moveToPath("/");
  };

  return (
    (...)
  );
};

export default LogoutComponent;

* 브라우저에서 로그아웃 결과를 확인한다.


 

'SpringBoot_React 풀스택 프로젝트' 카테고리의 다른 글

(37) 리덕스 툴킷 6  (1) 2024.03.06
(36) 리덕스 툴킷 5  (0) 2024.03.06
(34) 리덕스 툴킷 3  (0) 2024.03.06
(33) 리덕스 툴킷 2  (0) 2024.03.06
(32) 리덕스 툴킷 1  (0) 2024.03.06