๐ ๊ฐ์ ๋ฐ ๋ฌธ์ ์ํฉ
์ฌ์ฉ์๊ฐ ์ฒ์ ํ์๊ฐ์ ํ ๋ก๊ทธ์ธ์ ํ๋ฉด, ๋งค๋ฌ ์์ ์ ์ฒซ ๊ฐ๊ณ๋ถ์ ํ์ํ ์์ฐ์ ์ค์ ํฉ๋๋ค.
์ ๋ ์ด ์์ฐ์ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ก ํ์์ต๋๋ค. -> goalContext.jsx
๊ทธ๋ฌ๋, ๋ถ๋ช ๋ก๊ทธ์ธ ๋ก์ง์๋ goalAmount๊ฐ ์์ผ๋ฉด -> mainPage.jsx, ์์ผ๋ฉด -> goal-setting.jsx๋ก ๊ฐ๊ฒ๋ ์ค๊ณ๋ฅผ ํ๋๋ฐ
์ ์ฝ๋์ ์๋์๋ ๋ค๋ฅด๊ฒ ๊ณ์ ์ค๋ฅ๊ฐ ๋ฌ์ต๋๋ค.
์ฌ์ง์ด, ์ด๋ฏธ ๋ก๊ทธ์ธ์ ํ์ฌ ์์ฐ ์ค์ ์ ํ ์ฌ์ฉ์๋ ๋ค์ goal-setting์ผ๋ก ๊ฐ๋ ๋ฌธ์ ์ํฉ์ด ๋ฐ์ํ์ต๋๋ค.
-> ์๋ฒ(๋ฐฑ์๋ ๊ฐ๋ฐ์)๋ ํ ๋ฌ์ ๋ฑ ํ๋ฒ ์์ฐ์ ์ค์ ํ ์ ์๊ฒ ์ค๊ณ๋ฅผ ํ๋๋ฐ ์ด ๋์ ๋ ๋ฒ ์์ฐ ์ค์ ์ ํ๋ฉด ์ค๋ฅ๊ฐ ๋๋ ์ต์ ์ ์ํฉ์ด์์ต๋๋ค.
์ ๋ฆฌํ๋ฉด
1. ์๋์น์์ ๋ฆฌ๋ค์ด๋ ์ ๋ถ๊ฐ
2. 1๋ฒ์ผ๋ก ์ธํด ์๋ฒ์์ ์ฝ์์ ์งํค์ง ๋ชปํ๊ณ ์๋์ ๋ค๋ฅธ ์ค๋ฅ๋ฐ์!
๐ ํด๊ฒฐํด์ผ ํ ๊ธฐ๋ฅ
โ ๋ก๊ทธ์ธ ์ฑ๊ณต ํ, ์ฌ์ฉ์๊ฐ ๊ธฐ์กด์ ์ค์ ํ ์์ฐ์ด ์๋ค๋ฉด ๋ฐ๋ก ๋ฉ์ธ ํ์ด์ง(/main)๋ก ์ด๋
โ ์ฌ์ฉ์๊ฐ ์์ฐ์ ์ค์ ํ์ง ์์๋ค๋ฉด /goal-setting ํ์ด์ง๋ก ์ด๋ํ์ฌ ์์ฐ์ ์ค์ ํ๋๋ก ์ ๋
โ ์๋ก๊ณ ์นจํด๋ ์์ฐ ๋ฐ์ดํฐ(goalAmount)๊ฐ ์ ์ง๋๋๋ก localStorage๋ฅผ ํ์ฉ
โ ๋น๋๊ธฐ ์์ฒญ์ด ์๋ฃ๋๊ธฐ ์ ์ ํ์ด์ง๊ฐ ์ด๋ํ๋ ๋ฌธ์ ๋ฐฉ์ง
โ ๋ถํ์ํ API ์์ฒญ์ ์ต์ํํ์ฌ ์ฑ๋ฅ ์ต์ ํ
๐น 1. ํ์ํ ๊ฐ๋ ์ ๋ฆฌ
๐ 1๏ธโฃ ๋ก๊ทธ์ธ ํ ์์ฐ ๋ฐ์ดํฐ๋ฅผ ํ์ธํด์ผ ํ๋ ์ด์
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋ฉด,
1. ๊ธฐ์กด ์์ฐ์ด ์์ผ๋ฉด ๋ฐ๋ก ๋ฉ์ธ ํ์ด์ง(/main) ๋ก ์ด๋
2. ๊ธฐ์กด ์์ฐ์ด ์์ผ๋ฉด ์์ฐ ์ค์ ํ์ด์ง(/goal-setting) ๋ก ์ด๋ํ์ฌ ์ง์ ์ค์ ํ๋๋ก ์ ๋
์ด๋ฅผ ์ํด ๋ก๊ทธ์ธ → ์์ฐ ์กฐํ → ํ์ด์ง ์ด๋ ์์๋๋ก ์คํํด์ผ ํจ.
๐ 2๏ธโฃ ์ฐ๋ฆฌ๊ฐ ๊ฒช์๋ ๋ฌธ์ (์ฝ์งํ๋ ๋ถ๋ถ ๐)
โ ๋ก๊ทธ์ธ ํ ์์ฐ ๋ฐ์ดํฐ(goalAmount)๋ฅผ ํ์ธํ๊ธฐ ์ ์ ํ์ด์ง๊ฐ ๋จผ์ ์ด๋ → goalAmount === null๋ก ์ธ์๋์ด /goal-setting์ผ๋ก ์ด๋
โ ๋น๋๊ธฐ API ์์ฒญ(fetchBudget())์ด ์๋ฃ๋๊ธฐ ์ ์ goalAmount๋ฅผ ๊ฒ์ฌํ๋ฉด ์๋ชป๋ ๊ฐ์ด ์ฌ์ฉ๋ ๊ฐ๋ฅ์ฑ ์์
โ ์๋ก๊ณ ์นจํ๋ฉด goalAmount๊ฐ ์ด๊ธฐํ๋์ด /goal-setting์ผ๋ก ์ด๋ํ๋ ๋ฌธ์ ๋ฐ์
์ฒ์ goalAmount ์ ์ญ ์ํ์ ์ด๊ธฐ๊ฐ์ null๋ก ์ ์ํ๊ธฐ์ ๋ฐ์ํ ๋ฌธ์ . ( ๊ทธ๋ฌ๋ null๋ก ์ ์ง๋ ํด์ผํจ)
import { createContext, useState } from 'react';
import { getBudget } from '../api/budgetApi';
export const GoalContext = createContext();
export const GoalProvider = ({ children }) => {
const [goalAmount, setGoalAmount] = useState(null);
const [error, setError] = useState(null);
// โ
์์ฐ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๋ ํจ์
const fetchBudget = async () => {
try {
const data = await getBudget();
console.log('โ
์ ์ ์ธ๊ณฝ์ค์นด๋ค ์ ๋ณด:', data);
console.log('โ
์ ์ ์ค์นด๋ค ์ ๋ณด:', data.amount);
setGoalAmount(data.amount);
return data.amount;
//`${userBudget.toLocaleString()} ์` -> ์ฝค๋ง ์ฐํ๋๊ฑฐ์
} catch (error) {
console.error('๐จ ์์ฐ ์กฐํ ์คํจ:', error);
setError(error);
}
};
return (
<GoalContext.Provider
value={{ goalAmount, setGoalAmount, fetchBudget, error }}
>
{children}
</GoalContext.Provider>
);
};
export default GoalProvider;
๐น 2. ํด๊ฒฐ ๋ฐฉ๋ฒ (์ ์ฒด์ ์ธ ํ๋ฆ)
โ 1๏ธโฃ ๋ก๊ทธ์ธ ํ goalAmount ๊ฐ์ ๋น๋๊ธฐ ์์ฒญ์ผ๋ก ๊ฐ์ ธ์ด
• ๋ก๊ทธ์ธ ํ fetchBudget()์ ์คํํ์ฌ ์์ฐ์ ๊ฐ์ ธ์ด.
โ 2๏ธโฃ goalAmount๋ฅผ localStorage์ ์ ์ฅํ์ฌ ์๋ก๊ณ ์นจํด๋ ๊ฐ ์ ์ง -> ํต์ฌ ์์ด๋์ด!!
• goalAmount ๊ฐ์ localStorage์ ์ ์ฅํ๊ณ , useState์ ์ด๊ธฐ๊ฐ์ผ๋ก ํ์ฉ.
๋ํ ์ถ๊ฐ์ ์ผ๋ก, ๋ก๊ทธ์์ ์ ํ ํฐ๋ง ์ง์ ๋๋ฐ ์ด์ goalAmount๊ฐ๋ ์ง์์ผํจ
โ 3๏ธโฃ goalAmount ๊ฐ์ด null์ธ์ง ์๋์ง ํ์ธํ ํ ์ ์ ํ ํ์ด์ง๋ก ์ด๋
• ๊ฐ์ด ์์ผ๋ฉด /main์ผ๋ก ์ด๋, ์์ผ๋ฉด /goal-setting์ผ๋ก ์ด๋.
๐ 3. ์ค์ ์ฝ๋ ๊ตฌํ
๐น Step 1๏ธโฃ . GoalContext.jsx (์์ฐ ์ํ ๊ด๋ฆฌ)
• goalAmount ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ , API์์ ์์ฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ
• useEffect()๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์ธ ํ fetchBudget()์ ์คํ
• localStorage๋ฅผ ํ์ฉํ์ฌ ์๋ก๊ณ ์นจํด๋ ๋ฐ์ดํฐ ์ ์ง
import { createContext, useState, useEffect } from 'react';
import { getBudget } from '../api/budgetApi';
export const GoalContext = createContext();
export const GoalProvider = ({ children }) => {
const [goalAmount, setGoalAmount] = useState(() => {
// โ
localStorage์์ ๊ธฐ์กด ์์ฐ ๊ฐ์ ๋ถ๋ฌ์ค๊ธฐ (์๋ก๊ณ ์นจ ์ ๊ฐ ์ ์ง)
const savedGoal = localStorage.getItem('goalAmount');
return savedGoal ? JSON.parse(savedGoal) : null;
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// โ
๋ชฉํ ๊ธ์ก์ ๊ฐ์ ธ์ค๋ ํจ์
const fetchBudget = async () => {
setLoading(true);
try {
const data = await getBudget(); // API ํธ์ถ
setGoalAmount(data.amount);
localStorage.setItem('goalAmount', JSON.stringify(data.amount)); // โ
localStorage ์ ์ฅ
} catch (error) {
console.error('๐จ ์์ฐ ์กฐํ ์คํจ:', error.response?.data || error.message);
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
fetchBudget();
}
}, []);
return (
<GoalContext.Provider value={{ goalAmount, setGoalAmount, fetchBudget, loading, error }}>
{children}
</GoalContext.Provider>
);
};
export default GoalProvider;
๐น Step 2๏ธโฃ LoginPage.js (๋ก๊ทธ์ธ ํ ์์ฐ ์ฒดํฌ & ํ์ด์ง ์ด๋)
• ๋ก๊ทธ์ธ ์ฑ๊ณต ํ, fetchBudget()์ ํธ์ถํ์ฌ ์์ฐ ๋ฐ์ดํฐ ํ์ธ (์ฌ๊ธด ์๋ช ํจ)
• ๋น๋๊ธฐ ์์ฒญ์ด ๋๋ ๋๊น์ง goalAmount ๊ฐ์ ๊ธฐ๋ค๋ฆฐ ํ ํ์ด์ง ์ด๋ -> const budgetAmount = await fetchBudget()
-> ์ฌ๊ธฐ์๋ ์์ฃผ ์ข์ ๋ด์ฉ๋ค์ด ๋ง์. ( ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ ์ด๋ผ๋์ง ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ์ดํด)
• goalAmount๊ฐ ์์ผ๋ฉด /main์ผ๋ก, ์์ผ๋ฉด /goal-setting์ผ๋ก ์ด๋
import { useContext, useState, useEffect } from 'react';
import '../style/LoginPage.scss';
import SignupPage from './SignupPage';
import { useNavigate } from 'react-router-dom';
import { GoalContext } from '../context/GoalContext';
import { login } from '../api/authApi.jsx';
import { css, keyframes } from '@emotion/react';
// ๋ก๋ฉ ์ ํ์ํ
const spin = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;
const spinnerStyle = css`
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: ${spin} 1s linear infinite;
margin-left: 10px;
`;
const LoginPage = () => {
const [isSignupOpen, setIsSignupOpen] = useState(false); // ํ์๊ฐ์
๋ชจ๋ฌ ์ํ
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); // ๋ก๋ฉ ์ํ ์ถ๊ฐ
const [modalMessage, setModalMessage] = useState({ type: '', message: '' });
const [fetchingBudget, setFetchingBudget] = useState(false); //์์ฐ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ณ ์๋์ง?
const navigate = useNavigate();
const { goalAmount, fetchBudget } = useContext(GoalContext); //getํ๋ ํจ์๋ ๊ฐ์ ธ์ค์.
// ํ์๊ฐ์
๋ชจ๋ฌ ์ฌ๋ซ๊ธฐ
const openSignupModal = () => setIsSignupOpen(true);
const closeSignupModal = () => setIsSignupOpen(false);
// ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ํจ์
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true); // ๋ก๊ทธ์ธ ์์ ์ ๋ก๋ฉ ์ํ ํ์ฑํ
try {
const data = await login(email, password);
// const { token, refreshToken } = data; (ํ ํฐ key๋ช
๋ฐ๊ฟ)
const { accessToken, refreshToken } = data.data;
console.log('๐น ๋ก๊ทธ์ธ ์๋ต ๋ฐ์ดํฐ:', data); // ์๋ต ๊ตฌ์กฐ ํ์ธ ๋๋ฒ๊น
์ฉ์
console.log('๐ ์ ์ฅํ accessToken:', accessToken);
console.log('๐ ์ ์ฅํ refreshToken:', refreshToken);
//๋ก๊ทธ์ธ ์ฑ๊ณต์ ๋ก์ปฌ์คํ ๋ฆฌ์ง์์ ๋ฐ์์จ ํ ํฐ๋ค ์ ์ฅ.
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setModalMessage({
type: 'success',
message: '๋ก๊ทธ์ธ ์ฑ๊ณต! ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ณ ์์ต๋๋ค!',
});
setFetchingBudget(true);
const budgetAmount = await fetchBudget(); //๋ก๊ทธ์ธ ์ฑ๊ณตํ๋ฉด ๊ฐ๊ณ ์๋ ์์ฐ ์๋์ง ํ์ธ
setFetchingBudget(false);
console.log('๐ฆ ๋ก๊ทธ์ธ ํ ๋ฐ์ ์์ฐ ๊ธ์ก:', budgetAmount); // ๋๋ฒ๊น
์ฉ ์ฝ๋์
// goalAmount๊ฐ ์
๋ฐ์ดํธ๋๊ธฐ ์ ์ budgetAmount๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์ด์ง ์ด๋ (์ฑ๋ฅ ๊ฐ์ ๊ฐ๋ฅ์ฑ? )
setTimeout(() => {
if (budgetAmount !== null && budgetAmount > 0) {
navigate('/main');
} else {
navigate('/initial');
}
}, 500);
} catch (error) {
console.error('๋ก๊ทธ์ธ ์คํจ:', error);
setModalMessage({
type: 'error',
message: '๋ก๊ทธ์ธ ์คํจ! ๋ค์ ์๋ํด์ฃผ์ธ์.',
});
} finally {
setLoading(false); // ์๋ต ์๋ฃ ํ ๋ก๋ฉ ํด์
}
};
return (
<div className="login-page-container">
<h1>Login Page</h1>
<p>๋ก๊ทธ์ธ์ ์งํํด์ฃผ์ธ์.</p>
<form className="form" onSubmit={handleLogin}>
<input
type="email"
placeholder="์ด๋ฉ์ผ"
className="input-field"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
/>
<input
type="password"
placeholder="๋น๋ฐ๋ฒํธ"
className="input-field"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
/>
<button
type="submit"
className="login-button"
disabled={loading || fetchingBudget}
>
{loading ? '๋ก๋ฉ ์ค...' : '๋ก๊ทธ์ธ'}
{loading && <span css={spinnerStyle} />}
</button>
{fetchingBudget && (
<div className="loading-container">
<p>์์ฐ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...</p>
<span css={spinnerStyle} />
</div>
)}
{modalMessage.message && (
<div className={`modal-message ${modalMessage.type}`}>
{modalMessage.message}
</div>
)}
</form>
<p className="sign-up-prompt">
๊ณ์ ์ด ์์ผ์ ๊ฐ์?{' '}
<span className="sign-up-link" onClick={() => setIsSignupOpen(true)}>
ํ์๊ฐ์
</span>
</p>
{isSignupOpen && (
<SignupPage closeSignupModal={() => setIsSignupOpen(false)} />
)}
</div>
);
};
export default LoginPage;
๐น Step 3๏ธโฃ MainPage.js (๋ฉ์ธ ํ์ด์ง๋ก ์ด๋)
๐ ์ต์ข ๋์ ํ์ธ
โ๏ธ ๋ก๊ทธ์์ํ๋ฉด accessToken, refreshToken, goalAmount ๋ชจ๋ ์ญ์ ๋จ
โ๏ธ ๋ก๊ทธ์์ ํ /login ํ์ด์ง๋ก ์ ์ ์ด๋
โ๏ธ ์๋ก์ด ๋ก๊ทธ์ธ ์ ๊ธฐ์กด goalAmount ๊ฐ์ด ๋จ์์์ง ์์ ์ฌ๋ฐ๋ฅธ ๋ฐ์ดํฐ ํ์๋จ