React 유저 기능 구현

LeeKyungwon·2024년 6월 18일
0

프론트엔드 

목록 보기
49/56
post-custom-banner

회원가입

function RegisterPage() {
  const [values, setValues] = useState({
    name: "",
    email: "",
    password: "",
    passwordRepeat: "",
  });
  const navigate = useNavigate();
  const toast = useToaster();

  function handleChange(e) {
    const { name, value } = e.target;

    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  }

  async function handleSubmit(e) {
    e.preventDefault();

    if (values.password !== values.passwordRepeat) {
      toast("warn", "비밀번호가 일치하지 않습니다.");
      return;
    }
    const { name, email, password } = values;

    await axios.post("/users", {
      name,
      email,
      password,
    });
    await axios.post(
      "/auth/login",
      {
        email,
        password,
      },
      {
        withCredentials: true,
      }
    );
    navigate("/me");
  }

이런식으로 axiod를 사용해서 로그인 포스트 요청을 보낸다.

로그인

function LoginPage() {
  const [values, setValues] = useState({
    email: "",
    password: "",
  });
  const navigate = useNavigate();

  function handleChange(e) {
    const { name, value } = e.target;

    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    const { email, password } = values;
    await axios.post(
      "/auth/login",
      {
        email,
        password,
      },
      {
        withCredentials: true,
      }
    );
    navigate("/me");
  }
withCredentials: true,

이 옵션은 서로 다른 도메인에서 쿠키를 주고 받을 때 반드시 필요한 옵션이다.
리퀘스트를 보내는 쪽의 도메인이랑 받는 쪽의 도메인이 다를 때 반드시 true로 설정해줘야 한다. (지금은 localhost:3000에서 api 주소로 보내는 것이기 때문에 도메인이 다르므로 설정해줘야 함)

로그인할때 주로 쿠키를 받기 때문에 여기서 설정 추가

브라우저의 애플리케이션 탭으로 가면 쿠키값을 확인할 수 있음

리퀘스트에서 쿠키 사용하기

Origin이란?

Origin이랑 리퀘스트를 보내는 사이트의 도메인이다.
http://localhost:3000 여기에서 :3000은 포트 번호인데 이 포트 번호도 Origin에 포함된다.

http://localhost:3000에서 https://learn.codeit.kr로 리퀘스트를 보내는 경우, 서로 다른 Origin이라는 의미에서 Cross Origin이라고 표현하는데 여러 가지 보안 문제가 발생할 수 있기 때문에 주의해야 한다.
-> CORS 문제

Credential이란?

웹 개발에서 Credential은 유저를 증명할 수 있는 정보를 말한다.
예를 들면 아이디와 비밀번호라던지 서버에서 발급받은 토큰 같은 것들. 리퀘스트를 보내는 상황에서는 주로 쿠키를 말한다.

Axios에서 Credential 사용하기

Axios에서는 withCredentials라는 옵션을 불린형으로 지정할 수 있다.
이 값을 true 로 설정해야만 Cross Origin에 쿠키를 보내거나 받을 수 있다.
참고로 이건 fetch() 함수에서 credentials: 'include'를 설정하는 것과 같다.

axios.post(
  '/auth/login',
  { email: 'sunny@sundaymorning.kr', password: 't3st!' },
  { withCredentials: true },
);

fetch() 함수에서 Credential 사용하기

fetch()함수에서 리퀘스트를 보낼 때 쿠키를 사용하려면 적절한 credentials 옵션을 설정해줘야 한다.

  • 'omit': 쿠키를 사용하지 않는다. 리퀘스트를 보낼 때도 쿠키를 사용하지 않고, 리스폰스로 Set-Cookie 헤더를 받았을 때에도 쿠키를 저장하지 않는다.
  • 'same-origin': 아무 옵션을 지정하지 않았을 때 기본 값이다. 같은 Origin인 경우에만 쿠키를 사용하겠다는 옵션이다. 프론트엔드 사이트 주소와 리퀘스트를 보낼 백엔드 서버의 주소가 다르다면 Cross Origin이라고 이해하면 된다.
  • 'include': 이 옵션을 사용하면 Cross Origin인 경우에도 쿠키를 사용한다.

실습 서버와 프론트엔드 사이트가 다르다면 Cross Origin 리퀘스트를 보내게 되기 때문에 include옵션을 설정해야 한다.
예를 들어서 로그인을 한다면 아래와 같이 credentials: 'include' 옵션을 설정해야 쿠키를 사용할 수 있다.

fetch('https://learn.codeit.kr/api/link-service/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'sunny@sundaymorning.kr', password: 't3st!' }),
  credentials: 'include'
});

쿠키가 제대로 저장되지 않을 때

개발을 할 때 쿠키가 저장이 안 됐다면 크게 두 가지 이유를 생각해볼 수 있다. 1) 프론트 잘못, 2) 백엔드 잘못

프론트의 경우 REST Client로 테스트했을 때 잘 동작하는지 확인해보고, Cross Origin인 경우 프론트에서 리퀘스트를 보내는 코드에 withCredentials 옵션을 적절하게 설정해 줬는지 확인해 봤다면, 쿠키를 저장하는데 문제가 없다고 생각해도 좋다.

만약 그래도 쿠키 저장이 안 됐다면 쿠키 옵션의 문제일 가능성이 높다.
이럴 땐 의심되는 부분을 찾아서 백엔드 개발자에게 확인을 부탁하면 된다.
쿠키가 제대로 저장되지 않을 때 프론트엔드에서 확인해 볼 수 있는 것들에 대해 알아보도록 하겠다.

개발자 도구의 Network 탭에서 리스폰스 헤더를 확인해 보면 된다.
Headers라는 탭에서 Response Headers 안에 있는 Set-Cookie 값을 확인해 보면 된다.

크롬 개발자 도구에서는 경고 표시 아이콘으로 SameSite=Strict 옵션이지만 도메인이 다른(크로스 사이트) 리스폰스이기 때문에 쿠키를 저장하지 않았다고 알려주기도 한다.

This attempt to set a cookie via a Set-Cookie header was blocked because it had the "SameSite=Strict" attribute but came from a cross-site response which was not the response to a top-level navigation.

SameSite 옵션

SameSite는 리퀘스트를 보내는 쪽의 도메인과 리퀘스트를 받는 쪽의 도메인이 일치하는지 확인하고 쿠키의 사용을 허용하는 옵션인데, 이런 옵션은 백엔드 쪽에서 설정할 수 있다.

그 밖에도 Set-Cookie 헤더의 값을 보았을 때 쿠키가 저장되지 않는 다양한 이유가 있을 수 있는데, 이 값들은 리스폰스로 오는 것이기 때문에 프론트엔드 영역에서는 수정할 수 없다. 이럴 땐 개발자 도구에서 리퀘스트 헤더와 리스폰스 헤더를 복사해서 백엔드 개발자에게 확인을 요청해야 한다.

항상 Access Token 사용하기

lib/axios.js

import axios from "axios";

const instance = axios.create({
  baseURL: "URL",
  withCredentials: true,
});

export default instance;

이렇게 하면 일일히 withCredentials를 설정해줄 필요 없이 한 번에 설정할 수 있다.

컨텍스트로 유저 데이터 관리하기

유저 데이터 같은 것은 거의 모든 페이지에서 쓰기 때문에 매번 리퀘스트를 보내는 것은 비효율적이다.
사이트 전체에서 전역적으로 데이터를 써야할 때 컨텍스트를 사용한다.
AuthProvider.js

import { createContext, useContext, useState } from "react";

const AuthContext = createContext({
  user: null,
  login: () => {},
  logout: () => {},
  updateMe: () => {},
});

async function getMe() {
  const res = await axios.get("users/me");
  const nextUser = res.data;
  setUser(nextUser);
}
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  async function login({ email, password }) {
    await axios.post("/auth/login", {
      email,
      password,
    });
    await getMe();
  }
  async function logout() {}

  async function updateMe(formData) {
    const res = await axios.patch("users/me", formData);
    const nextUser = res.data;
    setUser(nextUser);
  }
  return (
    <AuthContext.Provider value={{ user, login, logout, updateMe }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("반드시 AuthProvider 안에서 사용해야 합니다.");
  }

  return context;
}

이런식으로 컨텍스트를 만들고 App.js에

import { AuthProvider } from "../contexts/AuthProvider";
import ToasterProvider from "../contexts/ToasterProvider";

function Providers({ children }) {
  return (
    <ToasterProvider>
      <AuthProvider>{children}</AuthProvider>
    </ToasterProvider>
  );
}

function App({ children }) {
  return <Providers>{children}</Providers>;
}

export default App;

적용을 해주었다.


하지만 새로고침을 하면 로그인이 풀리는 현상이 발생하는데 이를 해결하기 위해 AuthProvider.js에 useEffect 훅을 활용해서 유저데이터를 가져온다.

useEffect(() => {
  getMe();
}, []);

로그인 상태에 따라 리다이렉트하기

로그인 상태에서는 마이페이지로, 로그아웃 상태에서 마이페이지 들어가면 메인페이지로 이동하기

import { createContext, useContext, useEffect, useState } from "react";
import axios from "../lib/axios";
import { useNavigate } from "react-router-dom";

const AuthContext = createContext({
  user: null,
  isPending: true,
  login: () => {},
  logout: () => {},
  updateMe: () => {},
});

export function AuthProvider({ children }) {
  const [values, setValues] = useState({
    user: null,
    isPending: true,
  });

  async function getMe() {
    setValues((prevValues) => ({
      ...prevValues,
      isPending: true,
    }));
    let nextUser;
    try {
      const res = await axios.get("/users/me");
      nextUser = res.data;
    } catch {
    } finally {
      setValues((prevValues) => ({
        ...prevValues,
        user: nextUser,
        isPending: false,
      }));
    }
  }

  async function login({ email, password }) {
    await axios.post("/auth/login", {
      email,
      password,
    });
    await getMe();
  }

  async function logout() {
    /** @TODO 로그아웃 구현하기 */
  }

  async function updateMe(formData) {
    const res = await axios.patch("/users/me", formData);
    const nextUser = res.data;
    setValues((prevValues) => ({
      ...prevValues,
      user: nextUser,
    }));
  }

  useEffect(() => {
    getMe();
  }, []);

  return (
    <AuthContext.Provider
      value={{
        user: values.user,
        isPending: values.isPending,
        login,
        logout,
        updateMe,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}
export function useAuth(required) {
  const context = useContext(AuthContext);
  const navigate = useNavigate();
  if (!context) {
    throw new Error("반드시 AuthProvider 안에서 사용해야 합니다.");
  }

  useEffect(() => {
    if (required && !context.user && !context.isPending) {
      navigate("/login");
    }
  }, [context.user, context.isPending, navigate, required]);
  return context;
}

Refresh Token 활용하기

interceptor로 리스폰스를 가로챈 다음에 토큰 만료로 추정이 되면 토큰을 재발급하고 재시도 한다는 의미
axios.js에 아래 코드 추가

instance.interceptors.response.use(res => res, async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      await instance.post('/auth/token/refresh', undefined, { _retry: true });
      originalRequest._retry = true;
      return instance(originalRequest);
    }
    return Promise.reject(error);
  });

주의할 점은, 토큰 갱신을 실패했을 때도 401 상태 코드 리스폰스가 올 수 있기 때문에 _retry 를 미리 설정해서 재요청을 방지해야 한다는 것이다.

로그아웃

보통 토큰 기반 인증에서는 서버에 따로 로그아웃을 알려줄 필요없이 쿠키만 지우면 되고, 세션 기반에서는 서버에 리퀘스트에 로그아웃이라고 알려줘야 한다.

async function logout() {
  await axios.delete('/auth/logout');
  setValues((prevValues) => ({
    ...prevValues,
    user: null,
    avatar: null,
  }));
}

워크 플로

회원가입

로그인

유저 데이터 가져오기

토큰 갱신하기

로그아웃

구글 로그인


사진출처 : 코드잇

post-custom-banner

0개의 댓글