[React JS] 5. ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

 

1. ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ UI ์ƒ์„ฑํ•˜๊ธฐ

RegisterPage.js

import React from "react";

const RegisterPage = () => {
  return (
    <section className="flex flex-col justify-center mt-20 max-w-[400px] m-auto">
      <div className="p-6 bg-white rounded-md shadow">
        <h1 className="text-3xl front-semibold text-center">ํšŒ์›๊ฐ€์ž…</h1>
        <form className="mt-6">
          <div className="mb-2">
            <label
              htmlFor="email"
              className="text-sm font-semibold text-gray-800"
            >
              Email
            </label>
            <input
              type="email"
              id="email"
              className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
            />
          </div>
          <div className="mb-2">
            <label
              htmlFor="name"
              className="text-sm font-semibold text-gray-800"
            >
              Name
            </label>
            <input
              type="text"
              id="name"
              className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
            />
          </div>
          <div className="mb-2">
            <label
              htmlFor="password"
              className="text-sm font-semibold text-gray-800"
            >
              Password
            </label>
            <input
              type="password"
              id="password"
              className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
            />
          </div>
          <div className="mt-6">
            <button
              type="submit"
              className="w-full bg-black text-white px-4 py-2 rounded-md hover:bg-gray-700 duration-200"
            >
              ํšŒ์›๊ฐ€์ž…
            </button>
          </div>
          <p className="mt-8 text-xs font-light text-center text-gray-700">
            ์•„์ด๋””๊ฐ€ ์žˆ๋‹ค๋ฉด{" "}
            <a href="/login" className="font-medium hover:underline">
              ๋กœ๊ทธ์ธ
            </a>
          </p>
        </form>
      </div>
    </section>
  );
};

export default RegisterPage;

2. React-Hook-Form

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ

- ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๋งŒ๋“ค์–ด๋„ ๋˜์ง€๋งŒ react-hook-form ์‚ฌ์šฉํ•˜๋ฉด ํŽธ๋ฆฌ + re-๋žœ๋”๋ง ์ตœ์†Œํ™” ๊ฐ€๋Šฅ (์ ์€ ์ฝ”๋“œ, ๋†’์€ ์„ฑ๋Šฅ)

 

React-Hook-Form

npm i react-hook-form

- useForm() ์‚ฌ์šฉ

import React from "react";
import { useForm } from "react-hook-form";

const RegisterPage = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm( {mode: 'onChange'});

  const onSubmit = ({ email, password, name }) => {
    reset();
  };

  const userEmail = {
    required: "ํ•„์ˆ˜ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.",
  };

  const userName = {
    required: "ํ•„์ˆ˜ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.",
  };

  const userPassword = {
    required: "ํ•„์ˆ˜ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.",
    minLength: {
      value: 6,
      message: "์ตœ์†Œ 6์ž์ž…๋‹ˆ๋‹ค.",
    },
  };

  return (
    <section className="flex flex-col justify-center mt-20 max-w-[400px] m-auto">
      <div className="p-6 bg-white rounded-md shadow">
        <h1 className="text-3xl front-semibold text-center">ํšŒ์›๊ฐ€์ž…</h1>
        <form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
          <div className="mb-2">
            <label
              htmlFor="email"
              className="text-sm font-semibold text-gray-800"
            >
              Email
            </label>
            <input
              type="email"
              id="email"
              className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
              {...register("email", userEmail)}
            />
            {errors?.email && (
              <div>
                <span className="text-red-500">{errors.email.message}</span>
              </div>
            )}
          </div>
          

export default RegisterPage;

3. Axios Instance

Axios- ๋ธŒ๋ผ์šฐ์ €, Node.js๋ฅผ ์œ„ํ•œ Promise API๋ฅผ ํ™œ์šฉํ•˜๋Š” HTTP  ๋น„๋™๊ธฐ ํ†ต์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ- ๋ฐฑ์—”๋“œ - ํ”„๋ก ํŠธ์—”๋“œ ํ†ต์‹  ์‰ฝ๊ฒŒํ•˜๊ธฐ ์œ„ํ•ด Ajax์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ= fetch(๋‚ด์žฅ)์ด ์•„๋‹Œ Axios ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๋ง

npm i axios

 

Axios ์ธ์Šคํ„ด์Šคํ™”

- ์ธ์Šคํ„ด์Šคํ™” ํ•˜๋Š” ์ด์œ  : ์ค‘๋ณต๋œ ์š”์ฒญ ํŽธํ•˜๊ฒŒ ๊ฐ€๋Šฅ

- ex) localhost:4000 ์„ base URL๋กœ  ๋“ฑ๋“ฑ

 

* ํ† ํฐ์„ interceptor์— ๋„ฃ์–ด์ค˜์•ผ๋จ (์š”์ฒญ ๋ณด๋‚ผ๋•Œ๋งˆ๋‹ค ํ™•์ธํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ - ๋‚˜์ค‘์—)

 

utils > axios ํด๋”

import axios from "axios";

const axiosInstance = axios.create({
  baseURL: import.meta.env.PROD ? "" : "http://localhost:4000",
});

export default axiosInstance

(4000๋ฒˆ ์„œ๋ฒ„, ๊ฐœ๋ฐœํ™˜๊ฒฝ)


4. ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ ์ƒ์„ฑํ•˜๊ธฐ

Redux ์ด์šฉํ•ด์„œ ๋ฐฑ์—”๋“œ๋กœ ๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ- ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ redux store์— ๋„ฃ์–ด์ค„ ๊ฒƒ - action์„ dispatchํ•˜๋ฉด action์ด reducer ์ „๋‹ฌ๋˜๊ธฐ ์ „์— middleware์ด์šฉํ•ด์„œ ์š”์ฒญ ๋ณด๋‚ด๊ณ  ๋ฐฑ์—”๋“œ ์ฒ˜๋ฆฌ(์ •๋ณด์ €์žฅ)ํ•˜๊ณ  ์‘๋‹ต์ฃผ๋ฉด response data = action์˜ payload๋ฅผ ์ฒ˜๋ฆฌ

 

RegisterPage.js

  const dispatch = useDispatch();

  const onSubmit = ({ email, password, name }) => {
    const body = {
      email,
      password,
      name,
      image:
        "https://search.pstatic.net/sunny/?src=https%3A%2F%2Fi.pinimg.com%2F736x%2Fdf%2F77%2F7a%2Fdf777a668034cb80e6d0d0fa1a1d83cb.jpg&type=ofullfill340_600_png",
    };

    dispatch(registerUser(body));
    reset();
  };

userSlice.js

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
    .addCase(registerUser.pending, (state) => {
      state.isLoading = true;
    })
    .addCase(registerUser.fulfilled, (state) => {
      state.isLoading = false;
    })
    .addCase(registerUser.rejected, (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    })
  },
});

thunkFunctions.js (store)

import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axios";

export const registerUser = createAsyncThunk(
  "user/registerUser",
  async (body, thunkAPI) => {
    try {
      const response = await axiosInstance.post("/users/register", body);
      return response.data;
    } catch (error) {
      console.log(error);
      return thunkAPI.rejectWithValue(error.response.data || error.message);
    }
  }
);

5. react toast ์ด์šฉํ•˜๊ธฐ

์„ค์น˜

npm i react-toastify

- ์ผ์ • ์‹œ๊ฐ„๋™์•ˆ alert ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ 

App.jsx

<ToastContainer
        position="bottom-right"
        theme="light"
        pauseOnHover
        autoClose={1500}
      />

userSlice.js

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(registerUser.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(registerUser.fulfilled, (state) => {
        state.isLoading = false;
        toast.info("ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต");
      })
      .addCase(registerUser.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload;
        toast.error(action.payload);
      });
  },
});

6. register route ์ƒ์„ฑํ•˜๊ธฐ

index.js

app.use("/users", require("./routes/users"));

routes/users.js

const express = require("express");
const { User } = require("../models/User");
const router = express.Router();

router.post("/register", async (req, res, next) => {
  try {
    const user = new User(req.body);
    await user.save();
    return res.sendStatus(200);
  } catch (error) {
    next(error);
  }
});

router.post("/users/login", (req, res) => {});

router.post("/users/auth", (req, res) => {});

module.exports = router;

7. ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”ํ•˜๊ธฐ

bcryptjs ๋ชจ๋“ˆ ์‚ฌ์šฉ

- ์–‘๋ฐฉํ–ฅ (์•”ํ˜ธ ๋…ธ์ถœ๋˜๋ฉด X)

- ๋‹จ๋ฐฉํ–ฅ (hash, ๋ณตํ˜ธํ™” ๋ถˆ๊ฐ€)

 

๋ฌธ์ œ์ 

-> ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ์ฐพ์„ ์ˆ˜ ์žˆ์Œ (์ง€์ •๋ผ์žˆ๋Š” ๊ฒƒ)

 

ํ•ด๊ฒฐ

-> salt

-> ๋žœ๋ค๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ

-> salt + ๋น„๋ฐ€๋ฒˆํ˜ธ -> ๋ณตํ˜ธํ™” -> ํ•ด์‹œํ™”๋œ ์•”ํ˜ธ

 

User.js

userSchema.pre("save", async function (next) {
  let user = this;

  if (user.isModified("password")) {
    const salt = await bcrypt.genSalt(10);
    const hash = await bcrypt.hash(user.password, salt);
    user.password = hash;
  }
});

8. ๋กœ๊ทธ์ธํŽ˜์ด์ง€ ์ƒ์„ฑํ•˜๊ธฐ (Json Web Token)

ํ”„๋ก ํŠธ : ํŽ˜์ด์ง€ ์ถ”๊ฐ€ -> thunkFunctions.js ์ถ”๊ฐ€ -> userSlice.js ์ถ”๊ฐ€

๋ฐฑ์—”๋“œ : ๋ผ์šฐํ„ฐ ์ถ”๊ฐ€

 

* ์ธ์ฆ ์ ˆ์ฐจ๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

- HTTP๋Š” stateless (์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜์ง€ ์•Š์Œ)

- ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒƒ์„ ๋„ฃ์–ด ๋ณด๋‚ด๋ฉด ์„ฑ๋Šฅ์ด ์•ˆ์ข‹๊ธฐ ๋•Œ๋ฌธ์—

- ์ธ์ฆ์ ˆ์ฐจ ๊ธฐ๋ณธ ํ๋ฆ„

1) ์„œ๋ฒ„์— ์š”์ฒญ ๋ณด๋ƒ„

2) ์„œ๋ฒ„์—์„œ ์œ ์ € ์ •๋ณด ํฌํ•จํ•˜๋Š” ํ† ํฐ ์ƒ์„ฑ

3) response header์— ํ† ํฐ ์˜ฌ๋ ค์„œ ๋ณด๋ƒ„

4) ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฟ ํ‚ค/๋กœ์ปฌ/๋ฉ”๋ชจ๋ฆฌ์— ํ† ํฐ ์ €์žฅ

5) ์ดํ›„ ์š”์ฒญ ๋ณด๋‚ผ ๋•Œ ํ† ํฐ์„ ๊ฐ™์ด ๋ณด๋‚ด์คŒ

-> ์„œ๋ฒ„์—์„œ๋Š” ํ† ํฐ ๋ณตํ˜ธํ™”ํ•ด์„œ ์œ ์ € ์ •๋ณด ์•Œ ์ˆ˜ ์žˆ์Œ

 

* JWT๋ž€? (JSON Web Token) ๋ชจ๋“ˆ

- ๋‹น์‚ฌ์ž๊ฐ„ ์ •๋ณด๋ฅผ JSON ๊ฐ์ฒด๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ ์ปดํŒฉํŠธํ•˜๊ณ  ๋…๋ฆฝ์ ์ธ ๋ฐฉ์‹์„ ์ •์˜ํ•˜๋Š” ๊ฐœ๋ฐฉํ˜• ํ‘œ์ค€ (RFC 7519)

- ๋””์ง€ํ„ธ ์„œ๋ช…์ด ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ™•์ธํ•˜๊ณ  ์‹ ๋ขฐ๊ฐ€๋Šฅ

- ๊ตฌ์กฐ (header / payload / verify signature)

1) ํ† ํฐ ์œ ํšจ ํŒ๋‹จ

2) ์œ ์ € ์ •๋ณด ํŒ๋‹จ

 

๋น„๊ต : headers(ํด๋ผ์ด์–ธํŠธ) + payload(ํด๋ผ์ด์–ธํŠธ) + secrete text(์„œ๋ฒ„)

 

* login router

users.js

router.post("/login", async (req, res, next) => {
  // req.body / password / emali
  try {
    // ์กด์žฌํ•˜๋Š” ์œ ์ €์ธ์ง€ ์ฒดํฌ
    const user = await User.findOne({ email: req.body.email });
    if (!user) {
      return res.status(400).send("Auth failed, email not found");
    }
    
    // ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒƒ์ธ์ง€ ์ฒดํฌ
    const isMatch = await user.comparePassword(req.body.password);
    if (!isMatch) {
      return res.status(400).send("Wrong Password");
    }

    const payload = {
      userId: user._id.toHexString(),
    };
    // token์„ ์ƒ์„ฑ
    const accessToken = jwt.sign(payload, process.env.JWT_SECRET, {
      expiresIn: "1h",
    });

    return res.json({ user, accessToken });
  } catch (error) {
    next(error);
  }
});

User.js

userSchema.methods.comparePassword = async function (plainPassword) {
  let user = this;
  const match = await bcrypt.compare(plainPassword, user.password);
  return match;
};

9. ํŽ˜์ด์ง€ ์ด๋™ํ•  ๋•Œ๋งˆ๋‹ค ์ธ์ฆ์ด ๋˜์–ด์žˆ๋Š”์ง€ ์ฒดํฌํ•˜๊ธฐ

ํ”„๋ก ํŠธ

App.jsx

function App() {
  const dispatch = useDispatch();
  const isAuth = useSelector((state) => state.user?.isAuth);
  const { pathname } = useLocation();

  useEffect(() => {
    if (isAuth) {
      dispatch(authUser());
    }
  }, [isAuth, pathname, dispatch]);

  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<LandingPage />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />
      </Route>
    </Routes>
  );
}

thunkfunciton.js

export const authUser = createAsyncThunk(
  "user/authUser",
  async (_, thunkAPI) => {
    try {
      const response = await axiosInstance.get("/users/auth");
      return response.data;
    } catch (error) {
      console.log(error);
      return thunkAPI.rejectWithValue(error.response.data || error.message);
    }
  }
);

axios.js

axiosInstance.interceptors.request.use(
  function (config) {
    config.headers.Authorization =
      "Bearer " + localStorage.getItem("accessToken");
    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);

- ์š”์ฒญ ๋ณด๋‚ด์ง€๊ธฐ ์ „์— ๋ญ˜ ํ•˜๊ณ ์‹ถ์„๋•Œ ์—ฌ๊ธฐ์„œ!

 

๋ฐฑ์—”๋“œ

users.js

router.get("/auth", auth, async (req, res, next) => {
  return res.json({
    id: req.user._id,
    email: req.user.email,
    name: req.user.name,
    role: req.user.role,
    image: req.user.image,
  });
});

๋ฏธ๋“ค์›จ์–ด auth.js

const jwt = require("jsonwebtoken");
const { User } = require("../models/User");

let auth = async (req, res, next) => {
  // ํ† ํฐ์„ request headers์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
  const authHeader = req.headers["authorization"];

  // Bearer asdfja;sdfjasdlfjsdaf.asdfasf.adsfdasdfasf
  const token = authHeader && authHeader.split(" ")[1];
  if (token === null) return res.sendStatus(401);

  try {
    // ํ† ํฐ์ด ์œ ํšจํ•œ ํ† ํฐ์ธ์ง€ ํ™•์ธ
    const decode = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findOne({ _id: decode.userId });

    if (!user) {
      return res.status(400).send("์—†๋Š” ์œ ์ €์ž…๋‹ˆ๋‹ค.");
    }

    req.user = user;
    next();
  } catch (error) {
    next(error);
  }
};

module.exports = { auth };

ํ”„๋ก ํŠธ (์‘๋‹ต)

userslice.js

      .addCase(authUser.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(authUser.fulfilled, (state, action) => {
        state.isLoading = false;
        state.userData = action.payload;
        state.isAuth = true;
      })
      .addCase(authUser.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload;
        state.isAuth = false;
        localStorage.removeItem("accessToken");
      });

10. NotAuthRoutes, ProtectedRoutes

auth์— ๋”ฐ๋ผ ์ ‘๊ทผ ๋ง‰์•„์ฃผ๋Š” ๋ฐฉ๋ฒ• 

NotAuthRotues : ๋กœ๊ทธ์ธ ์•ˆ๋œ ์‚ฌ๋žŒ์ด ๋กœ๊ทธ์ธ ํ•œ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ€๋ ค๋Š” ๊ฑธ ๋ง‰์•„์ฃผ๋Š” ๊ฒƒ

ProtectedRoutes : ๋กœ๊ทธ์ธ ๋œ ์‚ฌ๋žŒ์ด ๋กœ๊ทธ์ธ ์•ˆ ํ•œ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ€๋ ค๋Š” ๊ฑธ ๋ง‰์•„์ฃผ๋Š” ๊ฒƒ

 

ProtectedRoutes.js

import React from "react";
import { Navigate, Outlet } from "react-router-dom";

const ProtectedRoutes = ({ isAuth }) => {
  return isAuth ? <Outlet /> : <Navigate to={"/lgoin"} />;
};

export default ProtectedRoutes;

App.jsx

<Route element={<ProtectedRoutes isAuth={isAuth} />}>
          <Route path="/protected" element={<ProtectedPage />} />
</Route>

- Outlet ์ด ProtectedPage๊ฐ€ ๋˜๋Š” ๊ฒƒ


11. Navbar & NavItem (logout)

NavBar

import React, { useState } from "react";
import { Link } from "react-router-dom";
import NavItem from "./Sections/NavItem";

const Navbar = () => {
  const [menu, setMenu] = useState(false);

  const handleMenu = () => {
    setMenu(!menu);
  };

  return (
    <section className="relative z-10 text-white bg-gray-900">
      {/* z index๋Š” static ์•ˆ๋˜๊ณ  relative๋งŒ ๋ผ์„œ */}
      <div className="w-full">
        <div className="flex items-center justify-between mx-5 sm:mx-10 lg:mx-20">
          {/* logo */}
          <div className="flex items-center text-2xl h-14">
            <Link to="/">logo</Link>
          </div>

          {/* menu button */}
          <div className="text-2xl sm:hidden">
            <button onClick={handleMenu}>{menu ? "-" : "+"}</button>
          </div>

          {/* big screen nav-items */}
          <div className="hidden sm:block">
            {/* ์‚ฌ์ด์ฆˆ๊ฐ€ sm๋ณด๋‹ค ํด๋•Œ block */}
            <NavItem />
          </div>
        </div>

        {/* mobile nav-items */}
        <div className="block sm:hidden">{menu && <NavItem />}</div>
      </div>
    </section>
  );
};

export default Navbar;

NavItem

import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { logoutUser } from "../../../store/thunkFunctions";

const routes = [
  { to: "/login", name: "๋กœ๊ทธ์ธ", auth: false },
  { to: "/register", name: "ํšŒ์›๊ฐ€์ž…", auth: false },
  { to: "", name: "๋กœ๊ทธ์•„์›ƒ", auth: true },
];

const NavItem = ({ mobile }) => {
  const isAuth = useSelector((state) => state.user?.isAuth);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const handleLogout = () => {
    dispatch(logoutUser()).then(() => {
      navigate("/login");
    });
  };
  return (
    <ul
      className={`text-md justify-center w-full flex gap-4 ${
        mobile && "flex-col bg-gray-900 h-full"
      } items-center`}
    >
      {routes.map(({ to, name, auth }) => {
        if (isAuth !== auth) return null;

        if (name === "๋กœ๊ทธ์•„์›ƒ") {
          return (
            <li
              key={name}
              className="py-2 text-center border-b-4 cursor-pointer"
            >
              <Link onClick={handleLogout}>{name}</Link>
            </li>
          );
        } else {
          return (
            <li
              key={name}
              className="py-2 text-center border-b-4 cursor-pointer"
            >
              <Link to={to}>{name}</Link>
            </li>
          );
        }
      })}
    </ul>
  );
};

export default NavItem;

 

thunkfunction.js

export const logoutUser = createAsyncThunk(
  "user/logoutUser",
  async (_, thunkAPI) => {
    try {
      const response = await axiosInstance.post("/users/logout");
      return response.data;
    } catch (error) {
      console.log(error);
      return thunkAPI.rejectWithValue(error.response.data || error.message);
    }
  }
);

 

user.js

router.post("/logout", auth, async (req, res, next) => {
  try {
    return res.sendStatus(200);
  } catch (error) {
    next(error);
  }
});

 

userslice.js

.addCase(logoutUser.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(logoutUser.fulfilled, (state, action) => {
        state.isLoading = false;
        state.userData = initialState.userData;
        state.isAuth = false;
        localStorage.removeItem("accessToken");
      })
      .addCase(logoutUser.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload;
        toast.error(action.payload);
      });

12. token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ

axios.js

axiosInstance.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error) {
    if (error.response.data === "jwt expired") {
      window.location.reload();
    }
    return Promise.reject(error);
  }
);

๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ์•ฝ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ axios ์š”์ฒญ์—์„œ ํ™•์ธํ•ด์„œ ํ† ํฐ ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ reloadํ•˜์—ฌ 

ํ”„๋ก ํŠธ App.jsx์˜ if(isAuth) dispatch ๋ถ€๋ถ„์—์„œ -> userslice.js์˜ authUser rejected์—์„œ accessToken ์ง€์šธ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์คŒ

 

  • ๋„ค์ด๋ฒ„ ๋ธ”๋Ÿฌ๊ทธ ๊ณต์œ ํ•˜๊ธฐ
  • ๋„ค์ด๋ฒ„ ๋ฐด๋“œ์— ๊ณต์œ ํ•˜๊ธฐ
  • ํŽ˜์ด์Šค๋ถ ๊ณต์œ ํ•˜๊ธฐ
  • ์นด์นด์˜ค์Šคํ† ๋ฆฌ ๊ณต์œ ํ•˜๊ธฐ