이번 포스트에서는 구현한 로그인 기능에 대해 소개하고자 한다.
구현된 로그인 화면
사용자로부터 아이디와 비밀번호를 입력받아 일치하는 값이 있는지 확인하고, AccessToken과 RefreshToken을 발급받는다.
이때 예전에 설정했던 const api를 기억하는가?
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
});
여기에서 쿠키 관련 옵션을 true로 했었다.
그 이유는 대부분의 경우에서 LocalStorage나 SessionStorage에서 토큰을 저장하고, 사용하는데
export const login = async (
username: string,
password: string
): Promise<string> => {
const response = await api.post('/api/v1/auth/login', {
username,
password,
});
const { accessToken, refreshToken } = response.data;
api.defaults.headers['Authorization'] = `Bearer ${accessToken}`;
Cookies.set('accessToken', accessToken, {
expires: 7,
secure: true,
httpOnly: true,
});
return accessToken;
};
나는 보안을 더 강화하여 프론트단에서 정보 노출이 되는 것을 최대한 막고 싶었다.
따라서 관련값을 쿠키로 저장하고, 전역으로 관리해 어디서든 쉽게 값을 받아오게 하였다.
관련 함수는 아래와 같다.
export const getTokens = () => {
const { accessToken } = useAuthStore.getState();
return { accessToken };
};
export const setTokens = (accessToken: string) => {
useAuthStore.getState().setTokens(accessToken);
};
export const resetTokens = () => {
useAuthStore.getState().clearTokens();
};
사용자가 페이지에 오래 머무는 등으로 인해, 토큰이 만료되면 axios의 interceptors를 활용해 값을 다시 받아오도록 하였다.
api.interceptors.request.use(
(config) => {
const accessToken = useAuthStore.getState().accessToken;
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const newAccessToken = await refreshAccessToken();
if (newAccessToken) {
api.defaults.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
}
}
return Promise.reject(error);
}
);
이때 페이지를 새로고침하거나 나갔다 다시 들어오는 경우 로그인이 풀리는 경우가 발생했었다.
따라서 쿠키에 저장하고 있는 액세스토큰을 사용하여, 위의 경우(새로고침, 나갔다 들어오는)에 새로 랜더링될 때
쿠키에 액세스 토큰이 있는지를 판별하여, 로그인 상태를 유지하도록 하였다.
export const initializeAuth = async () => {
try {
const response = await api.post('/api/v1/auth/refresh');
const { accessToken } = response.data;
useAuthStore.getState().setTokens(accessToken);
} catch {
throw new Error(`세션이 유효하지 않습니다.`);
}
};
__root.tsx
useEffect(() => {
async function initAuth() {
try {
await initializeAuth();
const { accessToken } = useAuthStore.getState();
setAuthState({
isInitialized: true,
isAuthenticated: !!accessToken,
});
} catch {
setAuthState({
isInitialized: true,
isAuthenticated: false,
});
}
}
initAuth();
}, []);
토큰값에 따라 protected route 설정은 다음과 같다.
const { accessToken } = useAuthStore.getState();
const publicRoutes = [
'/auth/login',
'/auth/sign-up/finish',
'/auth/sign-in',
'/auth/sign-up/sns',
];
if (!accessToken && !publicRoutes.includes(location.pathname)) {
navigate({ to: '/auth/login' });
} else if (
(location.pathname === '/' || location.pathname === '/auth/login') &&
accessToken
) {
navigate({ to: '/dashboard' });
}
}, [authState.isInitialized, location.pathname, navigate]);
비밀번호 보임/숨김 처리
React-Icons를 사용하였고, showPassword 값에 따라 아이콘과, 값들이 보여지게 하였다.
const [showPassword, setShowPassword] = useState(false);
// ...
<input
type={
type === 'password' ? (showPassword ? 'text' : 'password') : 'email'
}
placeholder={placeholder}
value={value}
onChange={(e) => onChange && onChange(e.target.value)}
/>
{type === 'password' && (
<S.EyeIconContainer onClick={handleShow}>
{showPassword ? (
<FaRegEyeSlash size={20} />
) : (
<IoEyeOutline size={20} />
)}
</S.EyeIconContainer>
)}
이메일에 따른 입력이나 비밀번호 유효성 검사는 이전에 활용했던 아래 코드를 사용하였다.
import { z } from 'zod';
export type LoginSchemaType = {
email: string;
password: string;
};
const LoginSchema = z.object({
email: z.string().email('유효한 이메일 주소를 입력해주세요'),
password: z
.string()
.regex(
/^(?=.*[A-Za-z])(?=.*\d).{8,}$/,
'비밀번호를 숫자, 영문 포함 8자리 이상으로 입력해주세요'
),
});
'패스트캠퍼스 데브캠프' 카테고리의 다른 글
파이널 프로젝트 - (8) aws s3 정적 배포 (0) | 2025.04.04 |
---|---|
파이널 프로젝트 - 내 프로젝트 (7) - Tanstack-Query useInfinity (0) | 2025.04.04 |
파이널 프로젝트 - 회원가입 (6) - 카카오/구글 로그인 (0) | 2025.04.04 |
파이널 프로젝트 - 회원가입 (5) - 이메일 인증 (0) | 2025.04.03 |
파이널 프로젝트 - 회원가입 (4) - 회원정보 입력 (0) | 2025.04.03 |