하루 하나씩 작성하는 TIL #41
필수 구현 사항 및 권장 진행 사항에 맞게 각 단계에 대해 설명해보도록 하겠다.
const register = async (credentials) => {
try {
await axios.post(`${API_URL}/register`, credentials);
} catch (error) {
throw new Error('회원가입 실패: ' + error.response.data.message);
}
};
회원가입 요청을 JWT 인증 서버로 보내기.
xios.post 메소드를 사용하여 API_URL/register로 회원가입 요청을 보내준다.
const login = async (credentials) => {
try {
const response = await axios.post(`${API_URL}/login`, credentials);
localStorage.setItem('token', response.data.accessToken);
setUser(response.data);
navigate('/');
} catch (error) {
throw new Error('로그인 실패: ' + error.response.data.message);
}
};
로그인 요청을 JWT 인증 서버로 보내고, 서버로부터 받은 JWT 토큰을 localStorage에 저장
이 부분에서 axios.post 메소드를 사용하여 API_URL/login로 로그인 요청을 보내고, 응답으로 받은 accessToken을 localStorage에 저장한다.
const mutationAdd = useMutation({
mutationFn: addExpense,
onSuccess: () => {
queryClient.invalidateQueries(['expenses']);
}
});
useMutation를 사용하여 지출 데이터를 추가
const { data: expenses, error, isLoading } = useQuery({
queryKey: ['expenses', month],
queryFn: () => getExpenses(month)
});
useQuery를 사용하여 지출 데이터를 가져오기
const mutationUpdate = useMutation({
mutationFn: updateExpense,
onSuccess: () => {
queryClient.invalidateQueries(['expenses']);
}
});
useMutation를 사용하여 지출 데이터를 업데이트
const mutationDelete = useMutation({
mutationFn: deleteExpense,
onSuccess: () => {
queryClient.invalidateQueries(['expenses']);
}
});
useMutation를 사용하여 지출 데이터를 삭제
import { useAuth } from '../context/AuthContext';
// ...
export default function CreateExpense({ addExpense }) {
const { user } = useAuth(); // 현재 로그인한 사용자 정보 가져오기
// ...
const handleSubmit = (e) => {
e.preventDefault();
if (!user) {
alert('로그인이 필요합니다.');
return;
}
addExpense({
date,
item,
amount,
description,
createdBy: user.nickname,
userId: user.id
});
// ...
};
// ...
}
지출 데이터를 생성할 때, 현재 로그인 한 사용자의 정보를 포함시켜서 addExpense 함수로 전달
// 지출 항목 추가
export const addExpense = async (expense) => {
const response = await axios.post(BASE_URL, expense);
return response.data;
};
지출 데이터를 생성할 때 createdBy와 userId 정보를 포함하여 addExpense 함수로 전달
이건 위 코드에서도 보이기 때문에 생략하겠다.
crud 파트에서 사용하는 모습을 확인 가능하기 때문에 생략하겠다.
react-router-dom 을 이용하여 로그인화면과 회원가입화면의 라우터 설정을 먼저 해봅시다.
로그인창에서는 회원가입 버튼을 클릭하면 회원가입창으로, 회원가입창에서는 로그인 버튼을 누르면 로그인창으로 이동 되도록 구현해 보세요.
아이디는 4~10글자로, 비밀번호는 4~15글자로, 닉네임은 1~10글자로 제한하세요.
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router>
<AuthProvider>
<div>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</div>
</AuthProvider>
</Router>
</QueryClientProvider>
);
}
export default App;
import { Link } from 'react-router-dom';
const Login = () => {
return (
<div style={formContainerStyle}>
<form onSubmit={handleSubmit} style={formStyle}>
<h2 style={titleStyle}>로그인</h2>
{/* ... */}
<Link to="/register" style={registerLinkStyle}>회원가입</Link>
</form>
</div>
);
};
export default Login;
import { Link } from 'react-router-dom';
const Register = () => {
return (
<div style={formContainerStyle}>
<form onSubmit={handleSubmit} style={formStyle}>
<h2 style={titleStyle}>회원가입</h2>
{/* ... */}
<Link to="/login" style={registerLinkStyle}>로그인</Link>
</form>
</div>
);
};
export default Register;
const handleSubmit = async (e) => {
e.preventDefault();
if (credentials.id.length < 4 || credentials.id.length > 10) {
setError('아이디는 4~10글자로 입력해주세요.');
return;
}
if (credentials.password.length < 4 || credentials.password.length > 15) {
setError('비밀번호는 4~15글자로 입력해주세요.');
return;
}
try {
await login(credentials);
} catch (error) {
setError(error.message);
}
};
import axios from 'axios';
const API_URL = 'https://moneyfulpublicpolicy.co.kr';
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const login = async (credentials) => {
try {
const response = await axios.post(`${API_URL}/login`, credentials);
localStorage.setItem('token', response.data.accessToken);
setUser(response.data);
} catch (error) {
throw new Error('로그인 실패: ' + error.response.data.message);
}
};
const register = async (credentials) => {
try {
await axios.post(`${API_URL}/register`, credentials);
} catch (error) {
throw new Error('회원가입 실패: ' + error.response.data.message);
}
};
// Other functions...
return (
<AuthContext.Provider value={{ user, setUser, login, logout, register }}>
{children}
</AuthContext.Provider>
);
};
login 함수와 register 함수가 axios를 사용하여 JWT 인증서버에 요청을 보냄.
const handleSubmit = async (e) => {
e.preventDefault();
if (credentials.id.length < 4 || credentials.id.length > 10) {
setError('아이디는 4~10글자로 입력해주세요.');
return;
}
if (credentials.password.length < 4 || credentials.password.length > 15) {
setError('비밀번호는 4~15글자로 입력해주세요.');
return;
}
if (credentials.nickname.length < 1 || credentials.nickname.length > 10) {
setError('닉네임은 1~10글자로 입력해주세요.');
return;
}
try {
await register(credentials);
alert('회원가입 성공! 로그인 페이지로 이동합니다.');
navigate('/login');
} catch (error) {
setError(error.message);
}
};
성공 및 실패시 응답
const login = async (credentials) => {
try {
const response = await axios.post(`${API_URL}/login`, credentials);
localStorage.setItem('token', response.data.accessToken);
setUser(response.data);
} catch (error) {
throw new Error('로그인 실패: ' + error.response.data.message);
}
};
로그인은 AuthContext.jsx의 login 함수에서 구현되어 있으며, 로그인 성공 시 accessToken을 로컬스토리지에 저장
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
axios.get(`${API_URL}/user`, {
headers: {
Authorization: `Bearer ${token}`
}
}).then(response => {
setUser(response.data);
}).catch(() => {
localStorage.removeItem('token');
navigate('/login');
});
}
}, [navigate]);
새로고침 시 로그인 상태 유지
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
axios.get(`${API_URL}/user`, {
headers: {
Authorization: `Bearer ${token}`
}
}).then(response => {
setUser(response.data);
}).catch(() => {
localStorage.removeItem('token');
navigate('/login');
});
}
}, [navigate]);
로그인 유지 및 accessToken 유효성 검사
const handleSubmit = async (e) => {
e.preventDefault();
if (credentials.id.length < 4 || credentials.id.length > 10) {
setError('아이디는 4~10글자로 입력해주세요.');
return;
}
if (credentials.password.length < 4 || credentials.password.length > 15) {
setError('비밀번호는 4~15글자로 입력해주세요.');
return;
}
try {
await login(credentials);
} catch (error) {
setError(error.message);
}
};
const login = async (credentials) => {
try {
const response = await axios.post(`${API_URL}/login`, credentials);
localStorage.setItem('token', response.data.accessToken);
setUser(response.data);
navigate('/'); // 홈화면으로 이동
} catch (error) {
throw new Error('로그인 실패: ' + error.response.data.message);
}
};
import React from 'react';
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
import { useAuth } from '../context/AuthContext';
const Layout = () => {
const { user } = useAuth();
return (
<div>
{user && <Navbar />}
<Outlet />
</div>
);
};
export default Layout;
return (
<nav style={navStyle}>
<div style={leftNavStyle}>
<Link to="/">HOME</Link>
<Link to="/profile">내 프로필</Link>
</div>
<div style={rightNavStyle}>
<img src={user.avatar} alt="avatar" style={avatarStyle} />
<span>{user.nickname}</span>
<button onClick={logout} style={logoutButtonStyle}>로그아웃</button>
</div>
</nav>
);
const logout = () => {
localStorage.removeItem('token');
setUser(null);
navigate('/login'); // 로그아웃 시 로그인 페이지로 이동
};
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
const Profile = () => {
const { user, updateProfile } = useAuth();
const [nickname, setNickname] = useState(user.nickname);
const [avatar, setAvatar] = useState(null);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const formData = new FormData();
formData.append('nickname', nickname);
if (avatar) {
formData.append('avatar', avatar);
}
await updateProfile(formData);
alert('프로필 업데이트 완료!');
} catch (error) {
setError(error.message);
}
};
return (
<div style={formContainerStyle}>
<form onSubmit={handleSubmit} style={formStyle}>
<h2 style={titleStyle}>프로필 변경</h2>
<div style={inputContainerStyle}>
<label htmlFor="nickname">닉네임</label>
<input
type="text"
name="nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
style={inputStyle}
/>
</div>
<div style={inputContainerStyle}>
<label htmlFor="avatar">프로필 이미지</label>
<input
type="file"
name="avatar"
onChange={(e) => setAvatar(e.target.files[0])}
style={inputStyle}
/>
</div>
{error && <p style={errorStyle}>{error}</p>}
<button type="submit" style={buttonStyle}>업데이트</button>
</form>
</div>
);
};
const updateProfile = async (profileData) => {
const token = localStorage.getItem('token');
try {
const response = await axios.patch(`${API_URL}/profile`, profileData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`
}
});
setUser(response.data);
} catch (error) {
throw new Error('프로필 업데이트 실패: ' + error.response.data.message);
}
};
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
export default function CreateExpense({ addExpense }) {
const [date, setDate] = useState('');
const [item, setItem] = useState('');
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
const { user } = useAuth();
const handleSubmit = (e) => {
e.preventDefault();
if (user) {
addExpense({ date, item, amount, description, createdBy: user.nickname, userId: user.id });
} else {
alert('로그인이 필요합니다.');
}
setDate('');
setItem('');
setAmount('');
setDescription('');
};
return (
<form onSubmit={handleSubmit} style={formStyle}>
<div style={inputContainerStyle}>
<label style={labelStyle}>날짜</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
style={inputStyle}
/>
</div>
<div style={inputContainerStyle}>
<label style={labelStyle}>항목</label>
<input
type="text"
placeholder="지출 항목"
value={item}
onChange={(e) => setItem(e.target.value)}
style={inputStyle}
/>
</div>
<div style={inputContainerStyle}>
<label style={labelStyle}>금액</label>
<input
type="number"
placeholder="지출 금액"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={inputStyle}
/>
</div>
<div style={inputContainerStyle}>
<label style={labelStyle}>내용</label>
<input
type="text"
placeholder="지출 내용"
value={description}
onChange={(e) => setDescription(e.target.value)}
style={inputStyle}
/>
</div>
<button type="submit" style={buttonStyle}>추가</button>
</form>
);
}
import { v4 as uuidv4 } from 'uuid';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useAuth } from '../context/AuthContext';
export const useExpenses = () => {
const { user } = useAuth();
const queryClient = useQueryClient();
const addExpense = async (newExpense) => {
const response = await axios.post('http://localhost:5000/expenses', {
id: uuidv4(),
...newExpense,
createdBy: user.nickname,
userId: user.id,
});
return response.data;
};
const { mutate: addNewExpense } = useMutation(addExpense, {
onSuccess: () => {
queryClient.invalidateQueries(['expenses']);
},
});
return {
addNewExpense,
// ...
};
};
import { v4 as uuidv4 } from 'uuid';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
const API_URL = 'http://localhost:5000/expenses';
export const useExpenses = () => {
const queryClient = useQueryClient();
const fetchExpenses = async () => {
const response = await axios.get(API_URL);
return response.data;
};
const addExpense = async (newExpense) => {
const response = await axios.post(API_URL, {
id: uuidv4(),
...newExpense,
});
return response.data;
};
const updateExpense = async (updatedExpense) => {
const { id } = updatedExpense;
const response = await axios.put(`${API_URL}/${id}`, updatedExpense);
return response.data;
};
const deleteExpense = async (id) => {
await axios.delete(`${API_URL}/${id}`);
};
const { data: expenses, isLoading, isError } = useQuery('expenses', fetchExpenses);
const { mutate: addNewExpense } = useMutation(addExpense, {
onSuccess: () => {
queryClient.invalidateQueries('expenses');
},
});
const { mutate: modifyExpense } = useMutation(updateExpense, {
onSuccess: () => {
queryClient.invalidateQueries('expenses');
},
});
const { mutate: removeExpense } = useMutation(deleteExpense, {
onSuccess: () => {
queryClient.invalidateQueries('expenses');
},
});
return {
expenses,
isLoading,
isError,
addNewExpense,
modifyExpense,
removeExpense,
};
};
axios와 react-query를 사용하여 CRUD 기능을 구현
모든 API 호출이 axios를 통해 이루어지며, 데이터 fetching, 추가, 수정, 삭제는 react-query를 통해 관리
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { useExpenses } from '../hooks/useExpenses';
const EditExpense = ({ expense }) => {
const { user } = useAuth();
const { modifyExpense } = useExpenses();
const [formData, setFormData] = useState({ ...expense });
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
if (user.id === expense.userId) {
modifyExpense(formData);
} else {
alert('작성자만 수정할 수 있습니다.');
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields... */}
<button type="submit">수정</button>
</form>
);
};
export default EditExpense;