문제 상황
소셜 로그인할 때 팝업 창이 열리잖아요? 그런데 이 팝업이 문제였어요.
// 원래 이런 식으로 했어요
const popup = window.open('구글로그인페이지');
// 팝업이 닫혔는지 확인하려고
if (popup.closed) {
console.log('팝업이 닫혔어요!');
}
그런데 에러가 났어요! 😱
Cross-Origin-Opener-Policy policy would block the window.closed call.
COOP
COOP = 브라우저의 보안 정책
쉽게 말해서:
- "다른 사이트의 팝업 상태를 함부로 확인하지 마!"
- 구글 로그인 페이지 → 우리 사이트로 돌아올 때
- 브라우저가 "보안상 popup.closed 확인 금지!" 라고 막아버림
즉, 팝업이 닫혔는지 알 수 없게 됨 😵
폴백
폴백 = 플랜 B, 대안책
예를 들어:
- 플랜 A: 팝업 상태 확인
- 플랜 B: 다른 방법으로 확인 ← 이게 폴백!
해결
1단계: 에러 잡기
// ❌ 원래 코드 (에러 남)
if (popup.closed) {
console.log('팝업 닫혔어요');
}
// ✅ 수정된 코드 (에러 안 남)
try {
if (popup.closed) {
console.log('팝업 닫혔어요');
}
} catch (error) {
console.log('팝업 상태 확인 못함... 다른 방법 써야겠다');
}
2단계: 대안책 마련
try {
// 방법1: 팝업 상태 직접 확인
if (popup.closed) {
handlePopupClosed();
}
} catch (error) {
// 방법2: 서버에 "로그인 됐나요?" 물어보기
checkCurrentUser().then(success => {
if (success) {
console.log('어? 로그인되어 있네요! 성공한 것 같아요');
resolve({ success: true });
}
});
}
3단계: 완전한 해결책
// 여러 방법을 동시에 사용!
// 방법1: 팝업에서 메시지 받기
window.addEventListener('message', (event) => {
if (event.data.type === 'OAUTH_SUCCESS') {
console.log('팝업에서 성공 메시지 받았어요!');
}
});
// 방법2: 1초마다 팝업 상태 체크
setInterval(() => {
try {
if (popup.closed) {
console.log('팝업 닫혔어요!');
}
} catch (error) {
// 방법3: 서버에 물어보기 (대안책)
checkCurrentUser().then(success => {
if (success) {
console.log('서버에서 확인해보니 로그인되어 있어요!');
}
});
}
}, 1000);
핵심
// 이게 핵심이에요!
const checkCurrentUser = async () => {
// 서버에 "지금 로그인되어 있나요?" 물어보기
const response = await fetch('/api/auth/me');
if (response.ok) {
return true; // "네, 로그인되어 있어요!"
}
return false; // "아니요, 로그인 안되어 있어요"
};
🔄 실제 동작 과정
- 팝업 열기: 구글 로그인 페이지
- 사용자 로그인: 구글에서 로그인 완료
- 서버에 토큰 저장: 쿠키에 저장됨
- 팝업 상태 확인:
- 성공하면: 팝업 닫힘 확인
- 실패하면: 서버에 "로그인됐나요?" 물어보기
- 결과: 어떤 방법이든 로그인 성공 확인!
비유
상황: 친구가 편의점에 갔는데 언제 돌아오는지 모름
방법1: 친구한테 전화해서 "언제 와?" (팝업 상태 확인) 방법2: 친구가 문자로 "다 샀어!" (메시지 받기) 방법3: 집 앞에 가서 "어? 친구 신발이 있네?" (서버에서 확인)
➡️ 여러 방법을 써서 친구가 돌아왔는지 확인하는 것!
이렇게 해서 팝업 문제를 해결했어요! 😊
CORS와 COOP의 차이
🌍 CORS (Cross-Origin Resource Sharing)
언제 문제가 되나요?
다른 도메인의 API를 호출할 때
// 우리 사이트: localhost:5173
// 서버: localhost:8082
fetch('http://localhost:8082/api/auth/login') // ← 여기서 CORS 에러!
무슨 문제인가요?
Access to fetch at 'localhost:8082' from origin 'localhost:5173'
has been blocked by CORS policy
브라우저: "다른 도메인에서 온 요청이야! 서버가 허용했는지 확인해야겠어!"
어떻게 해결하나요?
// 서버에서 "이 도메인은 허용해줄게!" 라고 설정
@CrossOrigin(origins = "http://localhost:5173")
🚪 COOP (Cross-Origin-Opener-Policy)
언제 문제가 되나요?
다른 도메인의 팝업창 상태를 확인할 때
// 팝업 열기: 구글 로그인 (google.com)
const popup = window.open('https://accounts.google.com/oauth/...');
// 팝업 상태 확인하려고 할 때
if (popup.closed) { // ← 여기서 COOP 에러!
console.log('팝업 닫혔어요');
}
무슨 문제인가요?
Cross-Origin-Opener-Policy policy would block the window.closed call
브라우저: "다른 도메인의 창 정보를 함부로 보지 마! 보안 위험해!"
어떻게 해결하나요?
// 직접 확인 대신 다른 방법 사용
try {
if (popup.closed) {
handleClosed();
}
} catch (error) {
// 서버에 물어보기 (대안책)
checkCurrentUser();
}
차이점
구분 CORS COOP| 문제 상황 | API 호출할 때 | 팝업 상태 확인할 때 |
| 에러 위치 | 네트워크 요청 | JavaScript 코드 |
| 해결 방법 | 서버 설정 변경 | 클라이언트 코드 변경 |
비유
CORS = 가게 출입 정책
👤 손님: "이 가게 들어가도 돼요?"
🏪 가게: "신분증 확인... 네, 들어오세요!"
➡️ 서버가 "이 도메인 허용!" 해줘야 함
COOP = 타인 집 정보 보호
👤 나: "옆집 창문이 닫혀있나 확인해볼까?"
🚫 브라우저: "안돼요! 남의 집 정보는 볼 수 없어요!"
➡️ 다른 방법으로 확인해야 함
문제
1. CORS 문제 (403 에러)
GET http://localhost:8082/api/auth/oauth/kakao/url 403 (Forbidden)
해결: Spring Security에서 CORS 허용 설정
2. COOP 문제 (팝업 접근 차단)
Cross-Origin-Opener-Policy policy would block the window.closed call
해결: try-catch + 서버 상태 확인 방식
핵심 차이
- CORS: "다른 사이트 API 호출 금지!" ➡️ 서버에서 허용해주면 됨
- COOP: "다른 사이트 창 정보 접근 금지!" ➡️ 우회 방법 써야 함
둘 다 보안을 위한 정책이지만, 발생하는 상황과 해결법이 완전히 달라요! 😊
원래 소셜로그인을 팝업으로 구현할 때 이렇게 구현하는게 일반적이야?
아니요! 일반적으로는 훨씬 간단하게 구현해요. 😅
🤔 일반적인 소셜 로그인 방식들
1. 리다이렉트 방식 (가장 일반적 ⭐)
// 버튼 클릭 시 현재 창에서 바로 이동
const googleLogin = () => {
window.location.href = 'http://localhost:8082/api/auth/oauth/google/url';
};
장점:
- 구현 매우 간단
- 팝업 차단 문제 없음
- 모바일에서도 완벽 동작
단점:
- 현재 페이지를 벗어남 (뒤로가기로 돌아와야 함)
2. SDK 방식 (React 라이브러리)
// react-google-login 같은 라이브러리 사용
import GoogleLogin from 'react-google-login';
<GoogleLogin
clientId="your-google-client-id"
onSuccess={handleSuccess}
onFailure={handleFailure}
/>
장점:
- 페이지 이동 없음
- 라이브러리가 다 해줌
단점:
- 각 소셜마다 다른 라이브러리 필요
3. 간단한 팝업 방식
// 대부분은 이 정도로만 해요
const popup = window.open(oauthUrl);
const interval = setInterval(() => {
if (popup.closed) {
clearInterval(interval);
window.location.reload(); // 그냥 새로고침!
}
}, 1000);
😰 우리가 복잡하게 한 이유
문제 1: 팝업 + 백엔드 연동
- 보통은 프론트엔드에서 SDK 쓰거나
- 리다이렉트 방식 씀
- 우리는 백엔드 OAuth + 팝업 조합 선택
문제 2: COOP 정책
- 옛날에는 popup.closed 잘 됐음
- 최근 브라우저들이 보안 강화
- 새로운 정책 때문에 복잡해짐
문제 3: JWT + HttpOnly Cookie
- 일반적으로는 localStorage 사용
- 우리는 보안을 위해 HttpOnly Cookie 선택
- 더 복잡한 상태 관리 필요
🌟 실제로 많이 쓰는 방식들
방식 1: NextAuth.js (Next.js)
import { signIn } from 'next-auth/react';
<button onClick={() => signIn('google')}>
구글 로그인
</button>
끝! 😎
방식 2: Firebase Auth
import { signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider);
이것도 끝! 🎉
방식 3: 리다이렉트 (가장 흔함)
// 그냥 페이지 이동
window.location.href = '/auth/google';
// 로그인 후 메인 페이지로 리다이렉트
📊 업계 현황
대기업들은 어떻게 할까?
- 구글: 리다이렉트 방식
- 페이스북: SDK + 팝업 (하지만 간단함)
- 깃허브: 리다이렉트 방식
- 네이버: 리다이렉트 방식
스타트업들은?
- Firebase Auth 많이 씀 (구글 것)
- Auth0, Supabase 같은 서비스
- NextAuth.js (오픈소스)
🤷♂️ 그럼 우리가 잘못한 거야?
아니요! 오히려 더 좋은 방식을 택한 거예요:
우리의 장점들
✅ 보안: HttpOnly Cookie (XSS 방지)
✅ UX: 페이지 이동 없음
✅ 제어권: 백엔드에서 모든 로직 관리
✅ 확장성: 모든 소셜 로그인 통합 가능
일반적인 방식의 한계
❌ 보안: localStorage 사용 (XSS 위험)
❌ UX: 페이지 이동 필요
❌ 의존성: 외부 라이브러리에 의존
🎯 결론
일반적: 간단하지만 보안/UX 타협
우리 방식: 복잡하지만 보안/UX 모두 챙김
// 일반적 방식
const login = () => window.location.href = '/auth/google';
// 우리 방식 (복잡하지만 더 좋음)
const login = () => {
// 팝업 + COOP 처리 + 쿠키 + 폴백...
}
대신 우리가 겪은 문제들이 앞으로 표준이 될 가능성 높음! 💪
최신 보안 정책을 모두 반영한 미래지향적 구현이라고 볼 수 있어요! 🚀
js에서 구현하지 않고 백엔드에서 구현할때의 장점
🛡️ 보안 장점 (가장 중요!)
1. Client Secret 보호
// ❌ 프론트엔드 (위험)
const clientSecret = "abcd1234secret"; // 모든 사용자가 볼 수 있음!
fetch(`https://oauth.google.com/token`, {
body: {
client_id: "...",
client_secret: clientSecret // 노출됨!
}
});
// ✅ 백엔드 (안전)
@Value("${spring.social.google.client-secret}")
private String clientSecret; // 서버에서만 알 수 있음
문제: 프론트엔드 코드는 사용자가 다 볼 수 있어요!
- 개발자 도구 열면 모든 코드 노출
- Client Secret이 공개되면 누구나 우리 앱인 척 할 수 있음
2. 토큰 안전한 저장
// ❌ 프론트엔드
localStorage.setItem('token', 'abc123'); // XSS 공격 위험
// ✅ 백엔드
Cookie refreshCookie = new Cookie("refreshToken", token);
refreshCookie.setHttpOnly(true); // JavaScript 접근 불가!
XSS 공격: 악성 스크립트가 토큰을 훔쳐갈 수 있음
🎯 제어권과 관리
1. 모든 OAuth 제공자 통합 관리
// ✅ 백엔드: 한 곳에서 모든 소셜 로그인 관리
@Service
public class OAuthService {
public LoginResponse processOAuth(String provider, String code) {
switch(provider) {
case "GOOGLE": return handleGoogle(code);
case "KAKAO": return handleKakao(code);
case "NAVER": return handleNaver(code);
}
}
}
// ❌ 프론트엔드: 각각 다른 SDK 필요
import GoogleLogin from 'react-google-login';
import KakaoLogin from 'react-kakao-login';
import NaverLogin from 'react-naver-login';
// 각각 다른 설정, 다른 콜백...
2. 사용자 데이터 자동 저장/연동
// ✅ 백엔드: OAuth 성공 즉시 DB에 저장
public LoginResponse processOAuth(String provider, String code) {
// 1. OAuth 토큰 받기
String accessToken = getAccessToken(code);
// 2. 사용자 정보 가져오기
OAuthUserInfo userInfo = getUserInfo(accessToken);
// 3. 우리 DB에 저장/업데이트
User user = saveOrUpdateUser(userInfo);
// 4. 우리 JWT 토큰 생성
return generateJWT(user);
}
// ❌ 프론트엔드: 여러 번 API 호출 필요
const googleSuccess = async (response) => {
// 1. 구글에서 받은 정보
const googleUser = response.profileObj;
// 2. 우리 서버에 전송
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(googleUser)
});
// 3. 다시 로그인 API 호출...
};
🔄 API 및 의존성 관리
1. OAuth API 버전 대응
// ✅ 백엔드: 한 곳에서 API 버전 관리
@Service
public class GoogleOAuthService {
private final String GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
private final String GOOGLE_USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
// API 변경되면 여기서만 수정
}
// ❌ 프론트엔드: 모든 클라이언트에서 업데이트 필요
// 구글이 API 변경하면 모든 사용자가 브라우저 새로고침해야 함
2. 에러 처리 통합
// ✅ 백엔드: 모든 OAuth 에러를 우리 형식으로 통일
try {
return oauthService.process(provider, code);
} catch (OAuth2Exception e) {
log.error("OAuth failed", e);
return ResponseEntity.badRequest().body("로그인 실패: " + e.getMessage());
}
📱 크로스 플랫폼 지원
백엔드 구현시
✅ 웹 (React) → 동일한 API 사용
✅ 모바일 앱 (iOS/Android) → 동일한 API 사용
✅ 다른 프론트엔드 (Vue, Angular) → 동일한 API 사용
프론트엔드 구현시
❌ React용 라이브러리
❌ iOS용 SDK
❌ Android용 SDK
❌ Vue용 라이브러리
각각 따로 구현해야 함...
💰 비용과 성능
1. API 호출 최적화
// ✅ 백엔드: 서버-서버 통신 (빠름)
// Google OAuth Server ←→ 우리 서버 (내부망, 빠름)
// ❌ 프론트엔드: 브라우저-서버 통신 (느림)
// 사용자 브라우저 ←→ Google ←→ 우리 서버 (인터넷, 느림)
2. 트래픽 절약
// ✅ 백엔드: 최소한의 데이터만 클라이언트에 전송
return LoginResponse.builder()
.accessToken(jwt)
.name(user.getName())
.role(user.getRole())
.build();
// ❌ 프론트엔드: Google의 모든 응답 데이터 받음
// 프로필 이미지, 이메일, 전화번호 등 불필요한 데이터까지...
🔍 모니터링과 로깅
// ✅ 백엔드: 상세한 로그 수집 가능
@Slf4j
public class OAuthService {
public LoginResponse processOAuth(String provider, String code) {
log.info("OAuth 시작: provider={}, ip={}", provider, getClientIP());
try {
// OAuth 처리
log.info("OAuth 성공: userId={}", user.getId());
return response;
} catch (Exception e) {
log.error("OAuth 실패: provider={}, error={}", provider, e.getMessage());
// 에러 알림, 슬랙 메시지 등...
}
}
}
🎯 결론: 언제 백엔드를 선택해야 하나?
백엔드 OAuth 추천 상황 ✅
- 기업용 서비스 (보안 중요)
- 여러 플랫폼 지원 (웹 + 모바일)
- 사용자 데이터를 자체 DB에 저장
- OAuth 제공자 여러 개 사용
- 장기적 유지보수 고려
프론트엔드 OAuth 추천 상황 ❌
- 개인 프로젝트, 프로토타입
- 빠른 개발이 최우선
- 보안보다 편의성 중시
- 단일 플랫폼만 지원
우리가 선택한 백엔드 방식은 확실히 더 견고하고 안전한 방법이에요! 🛡️✨
리다이렉트방식보다 팝업이 좋은 이유?
🎯 사용자 경험 (UX) 장점
1. 페이지 상태 유지
// ✅ 팝업 방식
const LoginPage = () => {
const [formData, setFormData] = useState({
name: "홍길동",
phone: "010-1234-5678",
address: "서울시 강남구..."
});
const handleSocialLogin = () => {
// 팝업으로 로그인
openOAuthPopup();
// 로그인 후에도 formData 그대로 유지됨! ✅
};
}
// ❌ 리다이렉트 방식
const LoginPage = () => {
const [formData, setFormData] = useState({
name: "홍길동",
phone: "010-1234-5678",
address: "서울시 강남구..."
});
const handleSocialLogin = () => {
window.location.href = "/auth/google";
// 페이지가 바뀌면서 formData 모두 사라짐! ❌
};
}
실제 예시: 회원가입 폼을 절반 작성하고 소셜 로그인하면?
- 팝업: 작성한 내용 그대로 남아있음 ✅
- 리다이렉트: 다시 처음부터 작성해야 함 ❌
2. 멀티태스킹 지원
✅ 팝업 방식:
탭1: 우리 쇼핑몰 (장바구니에 상품 담아둠)
팝업: 구글 로그인
→ 로그인 후 장바구니 그대로 유지
❌ 리다이렉트 방식:
탭1: 우리 쇼핑몰 → 구글 로그인 → 다시 쇼핑몰
→ 장바구니 날아갈 수 있음
3. 즉시 피드백
// ✅ 팝업: 로그인 상태 즉시 반영
const handleOAuthSuccess = () => {
setUser(userInfo); // 즉시 로그인 상태 변경
setCartItems(cartItems); // 장바구니 즉시 업데이트
showWelcomeMessage(); // 환영 메시지 즉시 표시
};
// ❌ 리다이렉트: 페이지 로딩 필요
// 구글 → 우리 사이트로 돌아오면서 로딩 시간 발생
🔄 개발자 경험 (DX) 장점
1. 복잡한 상태 관리가 가능
// ✅ 팝업: 복잡한 앱 상태 유지
const ShoppingApp = () => {
const [cartItems, setCartItems] = useState([]);
const [currentStep, setCurrentStep] = useState('checkout');
const [paymentMethod, setPaymentMethod] = useState('card');
const [deliveryInfo, setDeliveryInfo] = useState({...});
// 소셜 로그인 해도 모든 상태 유지됨!
const handleSocialLogin = async () => {
await oauthLogin('google');
// cartItems, currentStep 등 모든 상태 그대로
};
};
// ❌ 리다이렉트: 상태 관리 복잡
const ShoppingApp = () => {
// 로그인 전에 모든 상태를 어딘가에 저장해야 함
const handleSocialLogin = () => {
// 1. 장바구니를 localStorage에 저장
localStorage.setItem('cart', JSON.stringify(cartItems));
// 2. 현재 단계 저장
localStorage.setItem('step', currentStep);
// 3. 리다이렉트
window.location.href = '/auth/google';
// 4. 돌아왔을 때 모든 걸 복원해야 함...
};
};
2. SPA (Single Page Application)에 적합
// ✅ 팝업: React Router 상태 유지
const App = () => {
return (
<Router>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Router>
);
// 소셜 로그인 해도 현재 라우트 그대로 유지
};
// ❌ 리다이렉트: 라우팅 상태 복잡
// /dashboard에서 로그인하면 → /auth/google → /dashboard?
// 어느 페이지로 돌아가야 할지 별도 관리 필요
📱 모바일 고려사항
팝업의 장점
// ✅ 모바일: 네이티브 앱 같은 경험
// 작은 화면에서도 로그인 창만 뜸
// 뒤로가기 버튼으로 쉽게 취소 가능
리다이렉트의 단점
// ❌ 모바일: 전체 화면 전환
// 사용자가 어디에 있었는지 헷갈림
// 뒤로가기 히스토리가 복잡해짐
🎮 실제 사용 사례 비교
시나리오: 온라인 쇼핑몰
팝업 방식 ✅
1. 상품 페이지에서 장바구니에 담기
2. 결제하려니 로그인 필요
3. 팝업으로 구글 로그인
4. 로그인 성공하면 바로 결제 진행
→ 매끄러운 쇼핑 경험
리다이렉트 방식 ❌
1. 상품 페이지에서 장바구니에 담기
2. 결제하려니 로그인 필요
3. 구글 로그인 페이지로 이동
4. 로그인 후 메인 페이지로...
5. 다시 장바구니를 찾아가야 함
→ 사용자 이탈 가능성 높음
⚖️ 단점도 있어요
팝업의 단점 ❌
// 1. 팝업 차단 프로그램
// 2. 모바일에서 가끔 이상하게 동작
// 3. 복잡한 구현 (COOP 문제 등)
// 4. 접근성 문제 (스크린 리더 등)
리다이렉트의 장점 ✅
// 1. 구현 매우 간단
// 2. 팝업 차단 문제 없음
// 3. 접근성 좋음
// 4. 모바일에서 안정적
🎯 언제 팝업을 선택해야 할까?
팝업 추천 상황 ✅
- 복잡한 SPA 애플리케이션
- 상태 관리가 중요한 서비스 (쇼핑몰, 게임, 에디터)
- 멀티스텝 폼이 있는 서비스
- 실시간 상호작용이 중요한 서비스
리다이렉트 추천 상황 ✅
- 간단한 블로그나 정적 사이트
- 빠른 개발이 우선인 프로토타입
- 접근성이 매우 중요한 공공 서비스
- 모바일 우선 서비스
💡 결론
// 우리가 선택한 이유
const reasons = [
"복잡한 상태 관리 필요", // CMS 시스템
"SPA 특성 활용", // React 애플리케이션
"사용자 경험 최우선", // 끊김 없는 플로우
"기술적 도전 의식" // 최신 기술 적용
];
우리의 선택은 옳았어요! 복잡하지만 더 나은 사용자 경험을 제공하는 방식을 택한 거죠! 🚀✨
프로젝트에서 예상치 못한 문제가 발생했을 때, 이를 해결했던 경험이 있으신가요? 그 과정과 결과를 설명해주세요.
1단계: 파라미터 인식 오류 (500 Internal Server Error)
@GetMapping("/{provider}/url")
public ResponseEntity<?> getOAuthUrl(@PathVariable String provider)
첫 테스트에서 500 에러가 발생했습니다. 에러 로그를 확인해보니:
Name for argument of type [java.lang.String] not specified,
and parameter name information not available via reflection
원인 분석: Spring Boot에서 -parameters 컴파일러 플래그 설정이 누락되어 @PathVariable의 파라미터 이름을 인식하지 못하는 문제였습니다.
2단계: CORS 정책 차단 (403 Forbidden)
파라미터 문제를 해결했지만 이번에는 CORS 에러가 발생했습니다.
Access to fetch at 'localhost:8082' from origin 'localhost:5173'
has been blocked by CORS policy
3단계: COOP 정책 차단 (팝업 상태 확인 불가)
가장 예상치 못했던 문제였습니다. 팝업 창의 상태를 확인하려는 코드에서 에러가 발생했습니다.
Cross-Origin-Opener-Policy policy would block the window.closed call
문제 해결 과정
1단계 해결: 컴파일러 설정 및 명시적 파라미터 지정
// build.gradle 설정 추가
tasks.named('compileJava', JavaCompile) {
options.compilerArgs.add('-parameters')
}
// 코드 수정: 명시적 파라미터 이름 지정
@GetMapping("/{provider}/url")
public ResponseEntity<?> getOAuthUrl(@PathVariable("provider") String provider)
2단계 해결: Spring Security CORS 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
3단계 해결: 다중 폴백 메커니즘 구현
COOP 정책 문제가 가장 복잡했습니다. 기존의 단순한 popup.closed 체크가 브라우저 보안 정책에 의해 차단되어, 다중 폴백 전략을 구현했습니다.
// 방법1: PostMessage API (정상 케이스)
window.addEventListener('message', (event) => {
if (event.data.type === 'OAUTH_SUCCESS') {
resolve(event.data);
}
});
// 방법2: try-catch로 popup.closed 체크 (일부 브라우저)
try {
if (popup.closed) {
handlePopupClosed();
}
} catch (error) {
// 방법3: 서버 상태 확인 (최종 폴백)
checkCurrentUser().then(success => {
if (success) resolve({ success: true });
});
}
🎯 최종 해결책과 결과
안전하고 확장 가능한 아키텍처 완성
// OAuth 결과를 세션에 임시 저장
request.getSession().setAttribute("oauthSuccess", true);
request.getSession().setAttribute("userName", loginResponse.getName());
// HttpOnly Cookie로 토큰 관리
Cookie accessCookie = new Cookie("accessToken", loginResponse.getAccessToken());
accessCookie.setHttpOnly(true);
response.addCookie(accessCookie);
// 프론트엔드: 서버 상태 기반 사용자 정보 확인
const checkCurrentUser = async () => {
const response = await fetch('/api/auth/me', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setUser(data);
return true;
}
return false;
};
성과 및 학습한 점
기술적 성과
- 보안 강화: XSS 공격 방지 (HttpOnly Cookie)
- 사용자 경험 개선: 페이지 이동 없는 매끄러운 로그인
- 브라우저 호환성: 최신 보안 정책 대응
- 확장성: 모든 OAuth 제공자 통합 지원
문제 해결 역량 강화
- 체계적 디버깅: 에러 로그 분석 → 원인 파악 → 해결책 도출
- 대안적 사고: 기존 방법이 막혔을 때 새로운 접근법 시도
- 보안 인식: 최신 브라우저 보안 정책에 대한 이해
- 아키텍처 설계: 문제를 근본적으로 해결하는 구조 변경
교훈
이번 경험을 통해 예상치 못한 문제가 오히려 더 나은 솔루션으로 이끌 수 있다는 것을 배웠습니다. 단순히 문제를 해결하는 것을 넘어서, 보안과 사용자 경험을 모두 고려한 미래지향적인 아키텍처를 구축할 수 있었습니다.
또한 최신 웹 표준과 보안 정책에 대한 깊은 이해의 중요성을 깨달았으며, 앞으로도 변화하는 기술 환경에 능동적으로 대응할 수 있는 역량을 기를 수 있었습니다.
프로젝트에서 예상치 못한 문제가 발생했을 때, 이를 해결했던 경험이 있으신가요? 그 과정과 결과를 설명해주세요.
대학교 상담 시스템 CMS 프로젝트에서 소셜 로그인 기능을 구현하게 되었습니다.
보안과 사용자 경험을 모두 고려하여 백엔드 OAuth + 팝업 방식을 선택했는데, 개발 과정에서 예상치 못한 여러 문제들이 연쇄적으로 발생했습니다.
첫 테스트에서 500 에러가 발생했습니다.
에러 로그를 확인해보니 "Name for argument of type [java.lang.String] not specified"라는 메시지가 나타났는데,
Spring Boot에서 -parameters 컴파일러 플래그 설정이 누락되어 @PathVariable의 파라미터 이름을 인식하지 못하는 문제였습니다.
build.gradle에 컴파일러 설정을 추가하고 @PathVariable("provider")로 명시적 파라미터 지정을 하여 해결했지만, 이번에는 403 Forbidden CORS 에러가 발생했습니다.
이 문제는 Spring Security에서 CorsConfigurationSource를 구성하고 setAllowedMethods와 setAllowedOrigins를 설정하여 해결했습니다.
마지막으로 가장 예상치 못했던 문제가 발생했습니다.
팝업 창의 상태를 확인하려는 popup.closed 코드에서 "Cross-Origin-Opener-Policy policy would block the window.closed call" 에러가 발생했습니다.
이는 최신 브라우저의 COOP(Cross-Origin-Opener-Policy) 보안 정책으로 인한 문제였습니다.
단순한 popup.closed 체크가 차단되어, PostMessage API를 활용한 팝업-부모창 통신과 서버 상태 확인을 조합한 다중 폴백 전략을 구현했습니다.
모든 문제들을 해결하며 결과적으로 안전하고 확장 가능한 아키텍처를 완성할 수 있었습니다.
HttpOnly Cookie를 사용한 토큰 관리로 XSS 공격을 방지하여 보안을 강화하고, 팝업 방식으로 페이지 이동 없는 매끄러운 로그인 플로우를 구현하여 사용자 경험을 개선했습니다.
또한 백엔드에서 OAuth 로직을 통합 관리하여 구글, 카카오, 네이버 등 모든 OAuth 제공자를 일관되게 지원할 수 있는 확장성을 확보했습니다.
이번 경험을 통해 기존 해결책이 막혔을 때 다각도로 접근하는 문제 해결 역량을 기를 수 있었고, 예상치 못한 문제가 오히려 더 나은 솔루션으로 이끌 수 있다는 것을 배웠습니다.
단순히 문제를 해결하는 것을 넘어서, 보안과 사용자 경험을 모두 고려한 미래지향적인 아키텍처를 구축할 수 있었습니다.
또한 최신 웹 표준과 보안 정책에 대한 깊은 이해의 중요성을 깨달았으며, 앞으로도 변화하는 기술 환경에 능동적으로 대응할 수 있는 역량을 기를 수 있었습니다.