FrontEnd Develop/Project : Wallet Guardians

๋ณธ๊ฒฉ ์—ฐ๋™ #2. ๋ณด์•ˆ ๊ฐ•ํ™”๋œ ์ธ์ฆ ๋ฐ ์ ‘๊ทผ ์ œ์–ด ์„ค๊ณ„ ๊ณผ์ •

Frisbeen 2025. 2. 8. 22:12

๐Ÿ”น ์„œ๋ก 

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋กœ๊ทธ์ธ ๋ฐ ์ธ์ฆ ๋ณด์•ˆ ๊ฐ•ํ™”๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ์ €๋Š” ํ”„๋กœ์ ํŠธ <๊ฑฐ์ง€๋‚˜๊ธฐ : wallet Guardians>์˜ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋กœ๊ทธ์ธ, ํ† ํฐ ์ธ์ฆ, ์‚ฌ์šฉ์ž ์ •๋ณด ๋ณดํ˜ธ, ์ ‘๊ทผ ์ œ์–ด ๋“ฑ์˜ ๋ณด์•ˆ ๊ณผ์ •์„ ์„ค๊ณ„ํ•˜๋ฉด์„œ ๋ณด๋‹ค ์•ˆ์ „ํ•œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ ์šฉํ•œ ๋ณด์•ˆ ์„ค๊ณ„ ๊ณผ์ •์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

1๏ธโƒฃ JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

โœ… ๋กœ๊ทธ์ธ ์‹œ ํ† ํฐ ๋ฐœ๊ธ‰ ๋ฐ ์ €์žฅ

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด JWT(JSON Web Token) ๋ฅผ ๋ฐœ๊ธ‰ํ•˜์—ฌ ์ธ์ฆ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • accessToken๊ณผ refreshToken์„ ์„œ๋ฒ„์—์„œ ์‘๋‹ต๋ฐ›์•„ ํด๋ผ์ด์–ธํŠธ(LocalStorage)์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.๋‹น์—ฐํžˆ ์ด ๊ณผ์ •์€ ๋ฐฑ์—”๋“œ์™€ ์†Œํ†ต์œผ๋กœ์„œ ์ •ํ–ˆ๋˜ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.๋กœ๊ทธ์ธ ํ•จ์ˆ˜ ๋กœ์ง์ž…๋‹ˆ๋‹ค.
const handleLogin = async (e) => {
  e.preventDefault();
  try {
    const data = await login(email, password);
    const { accessToken, refreshToken } = data.data;

    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
  } catch (error) {
    console.error('๋กœ๊ทธ์ธ ์‹คํŒจ:', error);
  }
};

 

โœ… ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ์—์„œ ์ตœ์‹  ํ† ํฐ ๋ฐ˜์˜  -> ์žฌ์‚ฌ์šฉ์„ฑ ์ฆ๊ฐ€

api ํ†ต์‹ ์„ ์ˆ˜๋งŽ์€ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ง„ํ–‰ํ•˜๊ธฐ์— ํ•˜๋‚˜์˜ axios ํ†ต์‹  ๊ฐ์ฒด์ธ apiClient.jsx๋ฅผ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

apiClient ํ†ต์‹  ๊ฐ์ฒด์˜ ์ƒ์„ฑ ์‹œ์ , 2๊ฐœ์˜ ์ธํ„ฐ์…‰ํ„ฐ 

  • Axios ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ API ์š”์ฒญ ์‹œ ํ•ญ์ƒ ์ตœ์‹  ํ† ํฐ์„ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋„๋ก ์„ค์ •
  • ์ดˆ๊ธฐ API ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์‹œ headers์— ํ† ํฐ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ธํ„ฐ์…‰ํ„ฐ์—์„œ ์š”์ฒญ ์‹œ์ ์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉ (ํ† ํฐ ๊ฐฑ์‹  ์‹œ ์ฆ‰์‹œ ๋ฐ˜์˜ ๊ฐ€๋Šฅ)
  • ์ดˆ๊ธฐ ์‹œ์ ์— ์ถ”๊ฐ€ํ•ด๋ฒ„๋ฆฌ๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋‚  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Œ. ๋™์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด์„  ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ธํ„ฐ์…‰ํŠธํ•˜์—ฌ ๊ด€๋ฆฌํ•ด์•ผํ•จ
apiClient.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken');
    if (accessToken) {
      config.headers['ACCESS-AUTH-KEY'] = `BEARER ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

 

 

2๏ธโƒฃ ๋กœ๊ทธ์•„์›ƒ ๋ฐ ํ† ํฐ ๊ด€๋ฆฌ

โœ… ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ ์ œ๊ฑฐ

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์˜ ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ผํ•œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘๊ทผํ•  ์œ„ํ—˜์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
export const logout = async () => {
  try {
    await apiClient.delete('/auth/logout');
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    window.location.href = '/login';
  } catch (e) {
    console.log('๋กœ๊ทธ์•„์›ƒ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ:', e);
  }
};

 

 

3๏ธโƒฃ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ณดํ˜ธ ๋ฐ ์ ‘๊ทผ ์ œ์–ด

โœ… ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ API ์ ์šฉ

  • ๋กœ๊ทธ์ธ ํ›„, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜์—ฌ ์œ ์ € ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ์ €์˜ ์—ญํ• (Role) ๋ฐ ID๋ฅผ ํ™œ์šฉํ•ด ํŽ˜์ด์ง€ ์ ‘๊ทผ์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
export const getUserInfo = async () => {
  try {
    const response = await apiClient.get('/auth/info');
    return response.data.data;
  } catch (error) {
    console.error('์œ ์ € ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:', error);
    throw error;
  }
};

โœ… ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ์— ์œ ์ € ์ •๋ณด ํ‘œ์‹œ

  • Navbar์— ์œ ์ € ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜์—ฌ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋ฅผ ์ง๊ด€์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
import { useEffect, useState } from 'react';
import { getUserInfo } from '../api/authApi';

const UserInfoComponent = () => {
  const [userInfo, setUserInfo] = useState(null);
  useEffect(() => {
    const fetchUserData = async () => {
      try {
        const data = await getUserInfo();
        setUserInfo(data);
      } catch (error) {
        console.error('์œ ์ € ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:', error);
      }
    };
    fetchUserData();
  }, []);
  return userInfo ? <p>๐Ÿ‘ค {userInfo.username}</p> : <p>์œ ์ € ์ •๋ณด ์—†์Œ</p>;
};

4๏ธโƒฃ ๋ณด์•ˆ ๊ฐ•ํ™”๋œ ์ ‘๊ทผ ์ œํ•œ ๋กœ์ง

โœ… AuthenticatedComponent๋ฅผ ํ™œ์šฉํ•œ ์ ‘๊ทผ ์ œํ•œ

  • ๋กœ๊ทธ์ธ ๋ฐ ๋ชฉํ‘œ ๊ธˆ์•ก ์„ค์ •์ด ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ๋งŒ ๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ.
  • ๋กœ๊ทธ์ธ์ด ์•ˆ ๋œ ๊ฒฝ์šฐ → /login์œผ๋กœ ์ด๋™
  • ๋ชฉํ‘œ ๊ธˆ์•ก์ด ์—†๋Š” ๊ฒฝ์šฐ → /goal-setting์œผ๋กœ
import { useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { GoalContext } from '../context/GoalContext';

const AuthenticatedComponent = ({ children }) => {
  const { goalAmount, loading } = useContext(GoalContext);
  const navigate = useNavigate();

  useEffect(() => {
    if (loading) return;
    const accessToken = localStorage.getItem('accessToken');
    if (!accessToken) {
      navigate('/login');
      return;
    }
    if (goalAmount === null) {
      navigate('/goal-setting');
    }
  }, [goalAmount, loading, navigate]);

  return children;
};

export default AuthenticatedComponent;

 

โœ… App.jsx์— ๋ณด์•ˆ ์ ์šฉ

  • ๋กœ๊ทธ์ธ ๋ฐ ๋ชฉํ‘œ ๊ธˆ์•ก ์„ค์ •์ด ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ์—๋งŒ ๋ฉ”์ธ ํ™”๋ฉด ๋ฐ ์ฃผ์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ ์šฉ
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import StartPage from './pages/StartPage';
import LoginPage from './pages/LoginPage';
import ProfilePage from './pages/ProfilePage';
import NotFoundPage from './pages/NotFoundPage';
import IncomePage from './pages/IncomePage';
import MainPage from './pages/MainPage';
import ExpensePage from './pages/ExpensePage';
import InitialPage from './pages/InitialPage';
import GoalSettingPage from './pages/GoalSettingPage';
import GraphPage from './pages/GraphPage';
import InputEntryPage from './pages/InputEntryPage';
import ReceiptPicPage from './pages/ReceiptPicPage';

import { GoalProvider } from './context/GoalContext'; // โœ… GoalContext import
import { SidebarProvider } from './context/SidebarContext';
import { FriendProvider } from './context/FriendContext';
import FriendModal from './components/FriendModal';
import AuthenticatedComponent from './components/AuthenticatedComponent'; // โœ… ์ธ์ฆ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
import Layout from './components/Layout'; // โœ… ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ import

import './style/MainPage.scss';

const App = () => {
  return (
    <SidebarProvider>
      <GoalProvider>
        <FriendProvider>
          <Routes>
            {/* โœ… ์ธ์ฆ์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€๋ฅผ AuthenticatedComponent๋กœ ๊ฐ์‹ธ๊ธฐ */}
            <Route
              element={
                <AuthenticatedComponent>
                  <Layout />
                </AuthenticatedComponent>
              }
            >
              <Route path="/main" element={<MainPage />} />
              <Route path="/profile" element={<ProfilePage />} />
              <Route path="/income" element={<IncomePage />} />
              <Route path="/expenses" element={<ExpensePage />} />
              <Route path="/graph" element={<GraphPage />} />
              <Route path="/input-entry/:date" element={<InputEntryPage />} />
              <Route path="/receipt-picture" element={<ReceiptPicPage />} />
            </Route>

            {/* ๐Ÿ”น ๊ฐœ๋ณ„ ํŽ˜์ด์ง€ (์ธ์ฆ ํ•„์š” ์—†์Œ) */}
            <Route path="/initial" element={<InitialPage />} />
            <Route path="/goal-setting" element={<GoalSettingPage />} />
            <Route path="/" element={<StartPage />} />
            <Route path="/login" element={<LoginPage />} />
            <Route path="*" element={<NotFoundPage />} /> {/* 404 ํŽ˜์ด์ง€ */}
          </Routes>

          <FriendModal />
        </FriendProvider>
      </GoalProvider>
    </SidebarProvider>
  );
};

export default App;

 

โœ… ๊ฒฐ๋ก 

๐Ÿ”น ํ˜„ ์‹œ์ . ์„ค๊ณ„ํ•œ ๋ณด์•ˆ ์‹œ์Šคํ…œ (ํ”„๋กœํ† ํƒ€์ž…)

  1. JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ ์ ์šฉ (ํ† ํฐ ๋ฐœ๊ธ‰ ๋ฐ ์š”์ฒญ ์‹œ ์ตœ์‹  ํ† ํฐ ๋ฐ˜์˜)
  2. ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ ์ œ๊ฑฐ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”
  3. ์œ ์ € ์ •๋ณด ๋ณดํ˜ธ ๋ฐ ์—ญํ• (Role) ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด
  4. ๋ชฉํ‘œ ๊ธˆ์•ก ์„ค์ • ์—ฌ๋ถ€๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ ‘๊ทผ ์ œํ•œ (AuthenticatedComponent ํ™œ์šฉ)

๐Ÿ”น ๋ณด์•ˆ ํšจ๊ณผ

  • ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•˜์—ฌ ๋น„์ธ๊ฐ€ ์‚ฌ์šฉ์ž ์ฐจ๋‹จ
  • ๋ชฉํ‘œ ๊ธˆ์•ก์„ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ฃผ์š” ๊ธฐ๋Šฅ ์ ‘๊ทผ ์ œํ•œ
  • ์ตœ์‹  ํ† ํฐ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ธ์ฆ ์˜ค๋ฅ˜ ๋ฐฉ์ง€

 

'FrontEnd Develop > Project : Wallet Guardians' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

๋ณธ๊ฒฉ ์—ฐ๋™ #6. ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ UI ๊นœ๋นก์ž„ ๋ฐฉ์ง€ (await & setTimeout ํ™œ์šฉ)์œผ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ฆ๋Œ€  (0) 2025.02.11
๋ณธ๊ฒฉ ์—ฐ๋™ #5. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ์ดํ•ด๋กœ ๋กœ๊ทธ์ธ ํ›„ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ์ฒ˜๋ฆฌ  (0) 2025.02.10
๋ณธ๊ฒฉ ์—ฐ๋™ #4 ๋กœ๊ทธ์ธ ํ›„ ๊ธฐ์กด ์ปจํ…์ŠคํŠธ(๋ฐ์ดํ„ฐ)๊ฐ€ ์žˆ์œผ๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋ฐฉ๋ฒ• (React + Context API + Axios)  (0) 2025.02.10
๋ณธ๊ฒฉ ์—ฐ๋™ #3. ํ”„๋ก ํŠธ์—์„œ์˜ ๋ผ์šฐํŒ…์„ ํ™œ์šฉํ•œ ๋ณด์•ˆ, ์ธ์ฆ ์ฒ˜๋ฆฌ  (0) 2025.02.09
๋ณธ๊ฒฉ ์—ฐ๋™ #1. ํ† ํฐ ํ—ท๊ฐˆ๋ฆฌ์ง€ ์•Š๊ฒŒ ์„ค์ •ํ•˜๊ธฐ: ๋กœ๊ทธ์ธ & API ์ธ์ฆ ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ณผ์ • -> axios Instance์™€ ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ ๊ทธ๋ฆฌ๊ณ  ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ  (1) 2025.02.07