1. 로그인 기능, 왜 제대로 설계해야 할까?
프론트엔드 개발에서 로그인은 단순한 폼 이상의 의미를 가집니다.
단지 아이디와 비밀번호를 입력받는 것 같지만,
실제로는 토큰 관리, 에러 처리, 상태 동기화, 보안까지 함께 고려해야 합니다.
많은 초보 개발자가 다음과 같은 문제를 겪습니다.
- 로그인 후 새로고침 시 인증 정보가 사라짐
- 잘못된 비밀번호 입력 시 에러 처리 누락
- 로그인/로그아웃 시 전역 상태 업데이트 누락
이번 강의에서는
React Hook Form + React Query를 결합해
현실적인 로그인 기능을 구현하는 방법을 단계별로 살펴보겠습니다.
2. 기본 세팅
먼저 필요한 라이브러리를 설치합니다.
npm install react-hook-form @tanstack/react-query axios
그리고 전역에서 React Query의 Provider를 설정합니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
const client = new QueryClient();
export default function Root() {
return (
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
);
}
3. 로그인 API 설계 예시
일반적으로 로그인 API는 다음과 같은 구조를 가집니다.
요청(Request)
POST /api/login
{
"email": "user@example.com",
"password": "123456"
}
응답(Response)
{
"accessToken": "eyJhbGciOiJIUzI1...",
"refreshToken": "dGhpcyBpcyByZWZyZXNo...",
"user": {
"id": 1,
"name": "홍길동",
"email": "user@example.com"
}
}
받은 토큰은 일반적으로 로컬스토리지(LocalStorage) 혹은 **쿠키(Cookie)**에 저장합니다.
4. 로그인 폼 구현
아래는 React Hook Form과 React Query를 이용한 로그인 폼 예제입니다.
import { useForm } from 'react-hook-form';
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const mutation = useMutation({
mutationFn: async (data) => {
const res = await axios.post('/api/login', data);
return res.data;
},
onSuccess: (data) => {
localStorage.setItem('accessToken', data.accessToken);
alert(`${data.user.name}님, 환영합니다!`);
},
onError: (error) => {
alert(error.response?.data?.message || '로그인에 실패했습니다.');
},
});
const onSubmit = (formData) => mutation.mutate(formData);
return (
<form onSubmit={handleSubmit(onSubmit)} style={{ width: 320, margin: '0 auto' }}>
<h3>로그인</h3>
<div style={{ marginBottom: '12px' }}>
<label>이메일</label>
<input
{...register('email', {
required: '이메일을 입력해주세요.',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '이메일 형식이 올바르지 않습니다.',
},
})}
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
</div>
<div style={{ marginBottom: '12px' }}>
<label>비밀번호</label>
<input
type="password"
{...register('password', {
required: '비밀번호를 입력해주세요.',
minLength: { value: 6, message: '6자 이상 입력해야 합니다.' },
})}
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
</div>
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? '로그인 중...' : '로그인'}
</button>
{mutation.isError && (
<p style={{ color: 'red' }}>로그인에 실패했습니다. 다시 시도해주세요.</p>
)}
</form>
);
}
export default LoginForm;
5. 인증 상태 전역 관리
로그인 여부를 페이지 전체에서 확인하려면
토큰만 저장해서는 부족합니다.
전역 상태 관리를 도입해야 합니다.
간단한 예로, Zustand를 이용해 로그인 상태를 저장할 수 있습니다.
npm install zustand
// authStore.js
import { create } from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
이제 로그인 성공 시 상태를 업데이트합니다.
import { useAuthStore } from './authStore';
const mutation = useMutation({
mutationFn: (data) => axios.post('/api/login', data),
onSuccess: (data) => {
useAuthStore.getState().login(data.user);
localStorage.setItem('accessToken', data.accessToken);
},
});
로그아웃은 이렇게 간단히 구현할 수 있습니다.
function LogoutButton() {
const { logout } = useAuthStore();
const handleLogout = () => {
localStorage.removeItem('accessToken');
logout();
};
return <button onClick={handleLogout}>로그아웃</button>;
}
6. 인증 상태 유지 (자동 로그인)
사용자가 새로고침해도 로그인 상태를 유지하려면,
앱 로드 시 로컬스토리지에 저장된 토큰을 읽어서 상태를 복원해야 합니다.
import { useEffect } from 'react';
import { useAuthStore } from './authStore';
import axios from 'axios';
function AuthInitializer() {
const { login } = useAuthStore();
useEffect(() => {
const token = localStorage.getItem('accessToken');
if (!token) return;
axios.get('/api/me', { headers: { Authorization: `Bearer ${token}` } })
.then((res) => login(res.data.user))
.catch(() => localStorage.removeItem('accessToken'));
}, [login]);
return null; // 렌더링 X, 초기화 역할만 수행
}
App.js 최상단에 <AuthInitializer />를 추가하면
사용자가 새로고침해도 로그인 상태가 유지됩니다.
7. 보호된 페이지 (Protected Route)
로그인하지 않은 사용자가 특정 페이지에 접근하지 못하게 하려면
“Protected Route”를 설정해야 합니다.
import { Navigate } from 'react-router-dom';
import { useAuthStore } from './authStore';
function PrivateRoute({ children }) {
const { user } = useAuthStore();
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
}
이제 라우터에서 이렇게 사용합니다.
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
8. 에러 처리 & UX 개선 팁
- 서버 에러 메시지 표시하기
- onError: (error) => { alert(error.response?.data?.message || '로그인 실패'); }
- 로딩 중 버튼 비활성화 및 스피너 표시
- <button disabled={mutation.isLoading}> {mutation.isLoading ? '로딩 중...' : '로그인'} </button>
- 자동 포커스 및 Tab 순서 최적화
React Hook Form의 setFocus()를 이용하면 UX가 향상됩니다. - const { setFocus } = useForm(); useEffect(() => setFocus('email'), [setFocus]);
9. 전체 흐름 요약
1️⃣ React Hook Form → 입력값 및 유효성 검사
2️⃣ React Query → 서버 요청 및 상태 관리
3️⃣ Zustand → 로그인 상태 전역 관리
4️⃣ localStorage → 인증 토큰 저장
5️⃣ PrivateRoute → 보호된 페이지 구성
10. 마무리
이번 강의에서는 로그인 기능을 중심으로
폼 처리 → 서버 요청 → 인증 상태 유지까지 전 과정을 다뤘습니다.
핵심 요약:
- React Hook Form은 입력과 검증을 간단하게 관리
- React Query는 서버 통신과 상태를 체계적으로 관리
- Zustand로 인증 상태를 전역에서 유지
- localStorage와 PrivateRoute로 로그인 지속성 보장
다음 강의에서는 실제로 로그인한 사용자의 데이터를 기반으로
대시보드 페이지를 구성하고, React Query의 useQuery로
서버 데이터를 효율적으로 불러오는 방법을 다뤄보겠습니다.
'frontend > react' 카테고리의 다른 글
| [React] React.js 강좌 19. React Router로 대시보드 라우팅 구조 설계하기 (0) | 2025.11.10 |
|---|---|
| [React] React.js 강좌 18. 대시보드 구현 – React Query로 사용자 데이터 효율적으로 불러오기 (0) | 2025.11.10 |
| [React] React.js 강좌 16. React Hook Form × React Query – 폼 데이터 전송의 완벽한 조합 (0) | 2025.11.10 |
| [React] React.js 강좌 15. React Hook Form 완벽 가이드 – 폼 관리의 혁신 (0) | 2025.11.10 |
| [React] React.js 강좌 14. React Query 완벽 가이드 – 서버 상태 관리의 새로운 표준 (0) | 2025.11.10 |