관리 메뉴

거니의 velog

(33) 리덕스 툴킷 2 본문

SpringBoot_React 풀스택 프로젝트

(33) 리덕스 툴킷 2

Unlimited00 2024. 3. 6. 17:25

2. useSelector() / useDispatch()

* 애플리케이션 상태를 변경하기 위해서는 리듀서 함수인 login(), logout()을 호출하거나 변경된 상태를 전달(notify-통지) 받아야만 하는데 이를 위해서 useSelector(), useDispatch() 를 활용한다.

* useSelector() 는 컴포넌트 내에서 애플리케이션 상태를 받아서 자신이 원하는 상태 데이터를 선별(select)하는 용도로 사용한다. 예를 들어 로그인 상태가 변경되는 것을 알아야 하는 컴포넌트는 useSelector() 를 이용하게 된다.

* useDispatch() 는 리듀서를 통해서 만들어진 새로운 애플리케이션 상태를 반영하기 위해서 사용한다. 예를 들어 로그인 페이지에서 로그인이 처리되면 useDispatch()를 이용해서 새로운 애플리케이션 상태를 배포(dispatch)라는 경우에 사용하게 된다.


(1) 로그인 페이지와 로그인

* 현재 사용자의 로그인 상태에 따라 로그인 화면을 보거나 로그인 정보를 사용하는 예제를 작성해 보자. 먼저, 로그인 상황에 대한 처리부터 반영해 보자.

* 로그인 메뉴는 상단 메뉴에서 보이고 있고, BasicMenu.js 컴포넌트에서 처리되고 있다.

* 메뉴는 사용자의 로그인 상황에 따라 다르게 출력될 가능성이 있으므로 로그인 상황을 useSelector() 에서 감지하도록 설정한다.

import React from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";

const BasicMenu = () => {
  const loginState = useSelector((state) => state.loginSlice);

  return (
    (...)
  );
};

export default BasicMenu;

* useSelector()는 파라미터로 함수를 지정하는데, 이 함수를 사용해서 전달되는 애플리케이션 상태 중에 필요한 상태를 골라서 사용한다. 로그인 상태는 loginSlice라는 이름으로 store.js에 등록되어 있다.

* 화면의 메뉴 중에서 JWT를 이용해야 하는 Products와 Todo 메뉴는 email 값이 존재하는 로그인한 사용자에게만 노출하도록 제어한다.

import React from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";

const BasicMenu = () => {
  const loginState = useSelector((state) => state.loginSlice);

  return (
    <nav id="navbar" className=" flex  bg-blue-300">
      <div className="w-4/5 bg-gray-500">
        <ul className="flex p-4 text-white font-bold">
          <li className="pr-6 text-2xl">
            <Link to={"/"}>Main</Link>
          </li>
          <li className="pr-6 text-2xl">
            <Link to={"/about"}>About</Link>
          </li>
          {loginState.email ? ( //로그인한 사용자만 출력되는 메뉴
            <>
              <li className="pr-6 text-2xl">
                <Link to={"/todo/"}>Todo</Link>
              </li>
              <li className="pr-6 text-2xl">
                <Link to={"/products/"}>Products</Link>
              </li>
            </>
          ) : (
            <></>
          )}
        </ul>
      </div>

      <div className="w-1/5 flex justify-end bg-orange-300 p-4 font-medium">
        <div className="text-white text-sm m-1 rounded">Login</div>
      </div>
    </nav>
  );
};

export default BasicMenu;

* 브라우저에서 API 통신이 필요한 메뉴는 감춰진 것을 확인할 수 있다.


[로그인 페이지]

* 화면 상단의 오른쪽에는 Login 메뉴가 있으므로 이를 통해서 보여지는 LoginPage를 작성해 본다. pages 폴더 내에 member 폴더를 생성하고 LoginPage.js 파일을 추가한다.

import React from "react";
import BasicMenu from "../../components/menus/BasicMenu";

const LoginPage = () => {
  return (
    <div className="fixed top-0 left-0 z-[1055] flex flex-col h-full w-full">
      <BasicMenu />
      <div className="w-full flex flex-wrap h-full justify-center items-center border-2">
        <div className="text-2xl">Login Page</div>
      </div>
    </div>
  );
};

export default LoginPage;

* LoginPage에 대한 라우팅 설정을 위해 router 폴더에 memberRouter.js 파일을 추가한다.

import React, { Suspense, lazy } from "react";

const Loading = <div>Loading....</div>;
const Login = lazy(() => import("../pages/member/LoginPage"));

const memberRouter = () => {
  return [
    {
      path: "login",
      element: (
        <Suspense fallback={Loading}>
          <Login />
        </Suspense>
      ),
    },
  ];
};

export default memberRouter;

* 추가된 memberRouter에 대한 설정을 root.js를 이용해서 /member/ 경로로 시작할 때 memberRouter를 이용하도록 설정한다.

import React, { Suspense, lazy } from "react";
import { createBrowserRouter } from "react-router-dom";
import todoRouter from "./todoRouter";
import productsRouter from "./productsRouter";
import memberRouter from "./memberRouter";

(...)

const root = createBrowserRouter([
  (...)
  {
    path: "member",
    children: memberRouter(),
  },
]);

export default root;

* 브라우저에서 /member/login 경로를 호출하면 LoginPage의 결과를 확인할 수 있다.

* 상단의 BasicMenu 컴포넌트의 마지막 부분에는 로그인이 안된 상황(email 값이 없는 상황)에서는 Login 메뉴로 이동할 수 있도록 <Link>를 처리한다.

import React from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";

const BasicMenu = () => {
  const loginState = useSelector((state) => state.loginSlice);

  return (
    <nav id="navbar" className=" flex  bg-blue-300">
	  (...)

      <div className="w-1/5 flex justify-end bg-orange-300 p-4 font-medium">
        {!loginState.email ? (
          <div className="text-white text-sm m-1 rounded">
            <Link to={"/member/login"}>Login</Link>
          </div>
        ) : (
          <></>
        )}
      </div>
    </nav>
  );
};

export default BasicMenu;

* 브라우저에서 오른쪽 상단의 Login 메뉴를 클릭하면 /member/login 경로로 이동하고 LoginPage가 출력된다.


[로그인 컴포넌트]

* 로그인 페이지 안에 들어갈 로그인 컴포넌트는 components/member/LoginComponent.js 로 작성한다.

* LoginComponent는 email(아이디)과 pw(패스워드)를 입력받아서 로그인을 처리하는 함수이다. 서버와의 통신은 조금 미루는 대신에 리덕스 툴킷으로 애플리케이션 상태를 처리해 본다.

import React, { useState } from "react";

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

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

  const handleChange = (e) => {
    loginParam[e.target.name] = e.target.value;
    setLoginParam({ ...loginParam });
  };

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      <div className="flex justify-center">
        <div className="text-4xl m-4 p-4 font-extrabold text-blue-500">
          Login Component
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-full p-3 text-left font-bold">Email</div>
          <input
            className="w-full p-3 rounded-r border border-solid border-neutral-500 shadow-md"
            name="email"
            type={"text"}
            value={loginParam.email}
            onChange={handleChange}
          ></input>
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-full p-3 text-left font-bold">Password</div>
          <input
            className="w-full p-3 rounded-r border border-solid border-neutral-500 shadow-md"
            name="pw"
            type={"password"}
            value={loginParam.pw}
            onChange={handleChange}
          ></input>
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full justify-center">
          <div className="w-2/5 p-6 flex justify-center font-bold">
            <button className="rounded p-4 w-36 bg-blue-500 text-xl  text-white">
              LOGIN
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default LoginComponent;

* LoginPage 내부에 LoginComponent 를 추가하고 화면을 확인한다.

import React from "react";
import BasicMenu from "../../components/menus/BasicMenu";
import LoginComponent from "../../components/member/LoginComponent";

const LoginPage = () => {
  return (
    <div className="fixed top-0 left-0 z-[1055] flex flex-col h-full w-full">
      <BasicMenu />
      <div className="w-full flex flex-wrap  h-full justify-center  items-center border-2">
        <LoginComponent />
      </div>
    </div>
  );
};

export default LoginPage;

* 상단 메뉴에서 Login을 선택하면 LoginPage와 LoginComponent를 확인할 수 있다.


[로그인 상태 변경]

* LoginComponent에서는 LOGIN 버튼을 클릭했을 때 리듀서를 호출하고 이를 useDispatch()를 사용해서 애플리케이션의 상태를 변경해 본다.

* LoginComponent에 이벤트 처리를 위한 함수를 추가하고, 버튼과 연결한다.

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

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

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));
  };

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
    
      (...)
      
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full justify-center">
          <div className="w-2/5 p-6 flex justify-center font-bold">
            <button
              className="rounded p-4 w-36 bg-blue-500 text-xl  text-white"
              onClick={handleClickLogin}
            >
              LOGIN
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default LoginComponent;

* 화면 상에서 버튼을 클릭하면 loginSlice의 login()이 동작하는 것을 확인할 수 있다.

* loginSlice로 리듀서가 전달받은 데이터를 확인한다. 리듀서 함수의 두 번째 파라미터는 action으로 payload라는 속성을 이용해서 컴포넌트가 전달하는 데이터를 확인할 수 있다. 이를 이용해서 화면에서 전달된 email을 새로운 상태 값으로 처리한다.

import { createSlice } from "@reduxjs/toolkit";

const initState = {
  email: "",
};

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....");
    },
  },
});

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

export default loginSlice.reducer;

* 위 코드가 반영되면 화면에서 입력한 email 이 애플리케이션의 상태가 되고, useSelector()를 사용하는 메뉴에서는 email 값이 있기 때문에 로그인 전과 후의 메뉴 구성이 달라지면서 'Todo, Product' 메뉴가 노출되고 오른쪽 상단의 'Login' 링크는 사라지는 것을 확인할 수 있다.