7 분 소요


먼저 [쿠키, 세션, 토큰, JWT↗️]에 대해 바삭하게 이해하고 오자!


1. 인증 기반 라우팅 구현 개요

1.1 인증(Authentication)

개념

  • 사용자가 누구인지 확인하는 과정 (누구인지“인” 증하는 것)
  • ex) 사용자가 이름과 비밀번호를 입력하여 자신을 식별


1.2 인가(Authorization)

개념

  • 인증된 사용자가 특정 자원이나 기능에 접근할 수 있는 권한을 부여하는 과정 (권한을 허 “가” 하는 것)
  • ex) 특정 문서에 대한 읽기 권한 부여


1.3 폴더 구조

폴더 구조는 다음과 같다.

my-app:.
│
├── 📄 App.jsx
├── 📄 App.test.js
├── 📄 index.jsx
├── 📄 reportWebVitals.js
├── 📄 setupTests.js
│
├── 📁 api
│ ├── 📄 auth.js
│ ├── 📄 comments.js
│ └── 📄 posts.js
│
├── 📁 assets
│ └── 📁 images
│ ├── 🖼️ logo.png
│ └── 🖼️ user.png
│
├── 📁 components
│ ├── 📄 TextEditor.jsx
│ │
│ ├── 📁 comments
│ │ ├── 📄 CommentForm.jsx
│ │ ├── 📄 CommentReply.jsx
│ │ ├── 📄 CommentsList.jsx
│ │ ├── 📄 CommentsReplyForm.jsx
│ │ └── 📄 CommentsReplyList.jsx
│ │
│ └── 📁 layouts
│ ├── 📄 Footer.jsx
│ ├── 📄 Layout.jsx
│ └── 📄 Navbar.jsx
│
├── 📁 context
│ └── 📄 AuthContext.jsx
│
├── 📁 pages
│ ├── 📁 default-set
│ │ └── 📄 NotFound.jsx
│ │
│ ├── 📁 protected
│ │ ├── 📄 HomePage.jsx
│ │ └── 📄 MyPage.jsx
│ │
│ └── 📁 public
│ ├── 📄 PostDetailPage.jsx
│ ├── 📄 PostFormPage.jsx
│ ├── 📄 PostListPage.jsx
│ ├── 📄 PublicHomePage.jsx
│ ├── 📄 SigninPage.jsx
│ ├── 📄 SignupPage.jsx
│ └── 📄 UserProfilePage.jsx
│
├── 📁 shared
│ ├── 📄 ProtectedRoute.jsx
│ └── 📄 Router.jsx
│
├── 📁 styles
│ ├── 📄 CommonContainer.jsx
│ ├── 📄 CustomToolbar.jsx
│ ├── 📄 GlobalStyle.jsx
│ └── 📄 theme.jsx
│
└── 📁 utils
├── 📄 calculator.js
└── 📄 data.js



2. Axios 설정

인증이 필요한 API 요청을 위해 Axios 인스턴스를 생성한 후, 회원가입, 로그인, 로그아웃 등 인증과 관련된 API 함수를 정의하자.

// src/api/auth.js
import axios from "axios";

const authAxios = axios.create({
  baseURL: process.env.REACT_APP_SERVER_URL, // 환경 변수에서 기본 URL을 설정
  withCredentials: true, // 쿠키를 자동으로 포함시키기 위한 설정
});

// Axios 인스턴스 생성 - 요청 전 에러 처리 위함
authAxios.interceptors.request.use(
  (config) => {
    return config;
  },
  (error) => {
    console.error("에러가 발생하였습니다. 문의해주세요.", error);
    return Promise.reject({ state: "ERROR", message: error.message });
  }
);

// 회원 가입
export const register = async (data) => {
  return await authAxios.post(`/members/new`, data);
};

// 로그인
export const login = async (data) => {
  return await authAxios.post(`/login`, data);
};

// 로그아웃
export const logout = async () => {
  return await authAxios.get(`/logout`);
};

// 프로필 조회
export const getProfile = async () => {
  return await authAxios.get(`/profile`);
};

// 프로필 닉네임 변경
export const updateNickname = async (newNickname) => {
  return await authAxios.post(`/profile/updateNickname`, { newNickname });
};

// 타 사용자 프로필 조회
export const getMembersProfile = async (memberId) => {
  return await authAxios.get(`/members/${memberId}/profile`);
};



3. 인증 제공자 (AuthContext.jsx)

인증 상태를 관리하기 위해 AuthContext를 생성하자.

Axios를 사용하여 API 요청을 처리하며, 인증 상태를 관리한다.

// src/context/AuthContext.jsx
import { getProfile } from "api/auth";
import { createContext, useContext, useEffect, useState } from "react";

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const isSignIn = !!user; // 로그인 상태 확인

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await getProfile();
        setUser(response.data.member);
      } catch (error) {
        setUser(null); // 인증 실패 시 사용자 정보 초기화
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <AuthContext.Provider value={{ user, setUser, isSignIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext); // AuthContext를 쉽게 사용할 수 있도록 하는 커스텀 훅
// export default AuthContext;
// const { user, login, logout, isSignIn } = useAuth(); 와 같이 사용 가능



4. 보호된 라우트 (ProtectedRouter.jsx)

보호된 라우트는 인증된 사용자만 접근할 수 있도록 제한한다.

인증되지 않은 사용자는 로그인 페이지로 리다이렉트된다.

// src/shared/ProtectedRoute.jsx
import { useAuth } from "context/AuthContext";
import { Navigate, Outlet, useLocation } from "react-router-dom";

const ProtectedRoute = () => {
  const { isSignIn } = useAuth();
  const location = useLocation(); // 현재 페이지의 경로 정보를 가져옴

  if (!isSignIn) {
    return <Navigate to="/sign-in" state={{ from: location }} replace />;
  }

  return <Outlet />;
};

export default ProtectedRoute;



5. 라우터 설정 (Router.jsx)

라우터 설정에서는 인증 여부에 따라 접근 가능한 페이지를 다르게 설정한다.

ProtectedRouter 컴포넌트를 사용하여 인증된 사용자만 접근할 수 있는 라우트를 설정한다.

// src/shared/Router.jsx
import Layout from "components/layouts/Layout";
import MyPage from "pages/protected/MyPage";
import PostFormPage from "pages/public/PostFormPage";
import SigninPage from "pages/public/SigninPage";
import SignupPage from "pages/public/SignupPage";
import {
  createBrowserRouter,
  Navigate,
  RouterProvider,
} from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import PostListPage from "pages/public/PostListPage";
import PostDetailPage from "pages/public/PostDetailPage";
import UserProfilePage from "pages/public/UserProfilePage";
import PublicHomePage from "pages/public/PublicHomePage";

const Router = () => {
  // 공통 라우트 설정
  const commonRoutes = [
    { path: "/", element: <PublicHomePage /> }, // 모든 사용자에게 기본 페이지로 제공
    { path: "/post-list", element: <PostListPage /> },
    { path: "/posts/:id", element: <PostDetailPage /> },
    { path: "/user-profile/:id", element: <UserProfilePage /> },
  ];

  // 비인증 사용자 전용 라우터 설정
  const notAuthenticatedRoutes = [
    { path: "/sign-in", element: <SigninPage /> },
    { path: "/sign-up", element: <SignupPage /> },
  ];

  // 인증 사용자 전용 라우트 설정
  const authenticatedRoutes = [
    {
      element: <ProtectedRoute />, // 보호된 라우트 적용
      children: [
        { path: "/my-page", element: <MyPage /> },
        { path: "/post-form", element: <PostFormPage /> },
      ],
    },
  ];

  // 404 페이지 라우트 설정
  const notFound = {
    path: "*",
    element: <Navigate to="/" />,
  };

  const router = createBrowserRouter([
    {
      element: <Layout />,
      children: [
        ...commonRoutes,
        ...notAuthenticatedRoutes,
        ...authenticatedRoutes,
      ],
    },
    notFound,
  ]);

  return <RouterProvider router={router} />;
};

export default Router;



6. App.jsx

App 컴포넌트를 아래와 같이 설정해 주도록 하자.

// src/App.jsx
import { AuthProvider } from "context/AuthContext";
import Router from "shared/Router";

const App = () => {
  return (
    <AuthProvider>
      <Router />
    </AuthProvider>
  );
};

export default App;



7. Navbar 설정

사용자의 인증 상태에 따라 다르게 표시되게 아래와 같이 설정해주자.

import { logout } from "api/auth";
import { useAuth } from "context/AuthContext";
import { NavLink } from "react-router-dom";
import styled from "styled-components";

const Navbar = () => {
  const { setUser, isSignIn } = useAuth();

  const hanldeLogout = async () => {
    try {
      const logoutConfirm = window.confirm("정말 로그아웃 하시겠습니까?");
      if (logoutConfirm) {
        await logout();
        alert("로그아웃 되었습니다.");
        setUser(null);
      }
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <StNavbarContainer>
      <StNavLink to="/">로고</StNavLink>
      <StNav>
        {isSignIn ? (
          <>
            <StNavLink to="/my-page">마이페이지</StNavLink>
            <StNavLink to="/post-form">글 작성</StNavLink>
            <StNavLink to="/post-list">글 목록</StNavLink>
            <StLogout onClick={hanldeLogout}>로그아웃</StLogout>
          </>
        ) : (
          <>
            <StNavLink to="/post-list">글 목록</StNavLink>
            <StNavLink to="/my-page">마이페이지</StNavLink>
            <StNavLink to="/sign-in">로그인</StNavLink>
            <StNavLink to="/sign-up">회원가입</StNavLink>
          </>
        )}
      </StNav>
    </StNavbarContainer>
  );
};

export default Navbar;

const StNavbarContainer = styled.nav`
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: space-between;
`;

const StNav = styled.div`
  display: flex;
  justify-content: end;
`;
const StNavLink = styled(NavLink)`
  margin: 0 6px;
  &.active {
    color: red;
    font-weight: bold;
  }
`;

const StLogout = styled.div`
  cursor: pointer;
`;



8. 로그인 (Signin.jsx)

import { login } from "api/auth";
import { useAuth } from "context/AuthContext";
import { useState } from "react";
import { useNavigate } from "react-router-dom";

const SigninPage = () => {
  const [email, setEmail] = useState("");
  const [password1, setPassword1] = useState("");
  const [error, setError] = useState(null);

  const navigate = useNavigate();
  const { setUser } = useAuth();

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const response = await login({ email, password1 });
      alert("로그인이 완료되었습니다.");
      setUser(response.data); // 로그인 성공 후 사용자 정보 업데이트
      navigate("/");
    } catch (error) {
      const massage = error.response.data;
      console.log(massage); // 개발자 확인
      setError(massage); // 사용자 확인
    }
  };

  return (
    <div>
      <h2>로그인</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">이메일</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password1">비밀번호</label>
          <input
            type="password1"
            id="password1"
            value={password1}
            onChange={(e) => setPassword1(e.target.value)}
          />
        </div>
        {error && <p style={{ color: "red" }}>{error}</p>}
        <button type="submit">로그인</button>
      </form>
    </div>
  );
};

export default SigninPage;



9. 회원가입 (Signup.jsx)

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { register } from "../../api/auth";

const SignupPage = () => {
  const [name, setName] = useState("");
  const [nickname, setNickname] = useState("");
  const [email, setEmail] = useState("");
  const [password1, setPassword1] = useState("");
  const [password2, setPassword2] = useState("");
  const [error, setError] = useState("");
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (password1 !== password2) {
      setError("비밀번호가 일치하지 않습니다.");
      return;
    }
    try {
      await register({ name, nickname, email, password1, password2 });
      alert("회원 가입이 완료되었습니다.");
      navigate("/sign-in"); // 회원 가입 성공 후 로그인 페이지로 리다이렉트
    } catch (error) {
      const message = error.response.data;
      setError(message || "회원가입 중 문제가 발생했습니다."); // 사용자 확인
      console.log(message); // 개발자 확인
    }
  };

  return (
    <div>
      <h2>회원 가입</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="name">이름</label>
          <input
            type="text"
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
          />
        </div>
        <div>
          <label htmlFor="nickname">닉네임</label>
          <input
            type="text"
            id="nickname"
            value={nickname}
            onChange={(e) => setNickname(e.target.value)}
            required
          />
        </div>
        <div>
          <label htmlFor="email">이메일</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label htmlFor="password1">비밀번호</label>
          <input
            type="password"
            id="password1"
            value={password1}
            onChange={(e) => setPassword1(e.target.value)}
            required
          />
        </div>
        <div>
          <label htmlFor="password2">비밀번호 확인</label>
          <input
            type="password"
            id="password2"
            value={password2}
            onChange={(e) => setPassword2(e.target.value)}
            required
          />
        </div>
        {error && <p style={{ color: "red" }}>{error}</p>}
        <button type="submit">회원 가입</button>
      </form>
    </div>
  );
};

export default SignupPage;


카테고리:

업데이트:

댓글남기기