tech/project

[Auth] 로그인 화면 주도권을 누구에게 줄 것인가

sunm2n 2026. 1. 19. 01:33

로그인 화면의 주도권을 백과 프론트 중 어디에서 가지고 있을 것인가에 대해서 auth2 로그인을 할 때 구현 방식이 달라진다.

 

서버 사이드 리다이렉트(302 응답)과 JSON으로 URL 반환(프론트에서 이동) 이렇게 2가지의 방식으로 분류할 수 있다.

 

최근 SPA(React, vue등)에서는 후자의 방식이 더 좋다고 한다. 왜 그런지 알아보자

 

 

1. 두 방식의 작동 원리 비교

1-1. 서버 사이드 리다이렉트 (Server-Side Redirect) - 전통적인 방식

Spring Security의 기본 설정(oauth2-client)이 주로 이 방식이다. 

  1. Front: 사용자가 '구글 로그인' 버튼( <a> 태그)을 클릭한다.
  2. Back: 요청을 받자마자 HTTP 302 Redirect 응답과 함께 Location: https://accounts.google.com/... 헤더를 내려보낸다.
  3. Browser: 브라우저가 자동으로 해당 Location으로 이동한다.

1-2.  URL 반환 방식 (JSON Response) - SPA 친화적 방식

REST API 설계 철학에 더 가깝다.

  1. Front: 사용자가 버튼을 클릭하면 axios나 fetch로 백엔드 API를 호출한다.
    • GET /api/auth/login-urls
  2. Back: 리다이렉트 대신 JSON 데이터를 응답한다.
  3. Front: 응답받은 URL을 이용해 자바스크립트로 페이지를 이동시킨다.
    • window.location.href = response.data.google;

 

2. 왜 SPA에서는 URL 반환 방식이 더 유연한가?

SPA(Single Page Application) 환경에서 JSON 반환 방식이 선호되는 핵심 이유는 "제어권(Control)"이 프론트엔드에 있기 때문이다.

2-1. 매끄러운 UX (로딩 처리 등)

  • Redirect 방식: 사용자가 링크를 누르는 순간 브라우저가 하얗게 변하거나, 구글 페이지로 넘어가는 동안 잠시 멈춘 듯한 느낌을 받을 수 있다.
  • JSON 방식: 프론트엔드에서 API를 호출하는 동안 "로그인 페이지로 이동 중..." 같은 로딩 스피너를 띄울 수 있다. 사용자는 시스템이 반응하고 있다고 느낀다.

2-2. 로그인 전 상태 저장 (State Preservation)

  • 사용자가 보고 있던 페이지, 장바구니 상태, 혹은 "로그인 후 돌아올 페이지 경로(redirectUrl)" 등을 localStorage나 sessionStorage에 저장하는 로직을 이동 직전에 수행하기 편하다.
  • 예: saveCurrentPath(); window.location.href = url;

2-3. 팝업(Popup) 로그인 구현 가능

  • JSON으로 URL만 받아오면, 프론트엔드 개발자는 이를 window.location.href로 현재 창에서 열지, window.open()으로 팝업 창에서 열지 선택할 수 있다.
  • 서버가 강제로 302를 보내면 팝업 처리가 까다로워진다.

2-4. 에러 핸들링의 통일성

  • 만약 OAuth 공급자(구글, 깃허브) 서버 설정 문제로 URL 생성에 실패했다면?
    • Redirect: 서버 에러 페이지(Whitelabel Error Page)가 뜰 수 있다.
    • JSON: 백엔드가 500 에러와 메시지를 JSON으로 주면, 프론트엔드는 "일시적인 오류입니다."라는 Toast 메시지나 모달을 띄워 아름답게 대처할 수 있다.

 

3. 비교 요약표

특징 서버 사이드 리다이렉트 (302) URL 반환 (JSON)
구현 난이도 쉬움 (Spring Security 기본 동작) 중간 (URL 생성 컨트롤러 별도 작성 필요)
제어권 브라우저/백엔드 프론트엔드 (React/Vue)
페이지 이동 즉시 이동 (깜빡임 발생 가능) JS로 제어 (window.location)
용도 SSR(Thymeleaf, JSP), 간단한 프로젝트 SPA(React, Vue), 앱(Flutter, iOS)
확장성 낮음 높음 (팝업, 모바일 웹뷰 등 대응 유리)

 

 

4. 코드 예시 (Spring Boot)

만약 JSON 방식을 채택한다면, Spring Security가 자동으로 해주는 리다이렉트를 막고, 직접 URL을 생성해서 내려주는 컨트롤러가 필요하다.

 

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final ClientRegistrationRepository clientRegistrationRepository;

    // 로그인 링크 목록을 JSON으로 반환
    @GetMapping("/login-urls")
    public ResponseEntity<Map<String, String>> getLoginUrls() {
        Map<String, String> loginUrls = new HashMap<>();

        // Google URL 생성
        String googleUrl = getAuthorizationUrl("google");
        loginUrls.put("google", googleUrl);

        // Github URL 생성
        String githubUrl = getAuthorizationUrl("github");
        loginUrls.put("github", githubUrl);

        return ResponseEntity.ok(loginUrls);
    }

    private String getAuthorizationUrl(String provider) {
        ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(provider);
        
        // 실제로는 state, nonce 등 파라미터 구성 로직이 더 필요할 수 있습니다.
        // Spring Security 내부 로직을 참고하여 URL을 빌드합니다.
        return "https://accounts.google.com/o/oauth2/v2/auth?client_id=" 
                + clientRegistration.getClientId() 
                + "&response_type=code&scope=..." 
                + "&redirect_uri=" + clientRegistration.getRedirectUri();
    }
}

 

만약 리다이렉트 방식을 선택한다면?

 

// AuthController.java
// 코드 없음. 텅 비어 있음.
// 스프링 시큐리티가 알아서 하니까!

 

 

  • 리다이렉트 방식: "아 귀찮아, 로그인 페이지 가는 건 그냥 스프링 네가 알아서 넘겨." (구현 개꿀, 하지만 커스텀 힘듦)
  • JSON 방식: "프론트야, 내가 URL 조립해서 줄 테니까, 네가 띄우고 싶은 타이밍에 띄워." (구현 귀찮음, 하지만 프론트랑 협업하기 좋고 API 명세가 깔끔함)

좀 더 쉽게 풀어쓰자면

 

리다이렉트 방식 (Spring Security 기본)

"스프링 필터가 알아서 납치한다"

  • 누가 조립하나? : 스프링 시큐리티의 내부 필터(OAuth2AuthorizationRequestRedirectFilter)가 알아서 조립한다.
  • 어떻게 주나? : 백엔드 로직(Controller)으로 들어오기도 전에, 필터 단계에서 프론트엔드에게 "여기로 당장 이동해!(HTTP 302)" 하고 명령을 내려버린다.
  • 흐름: 구글 → 백엔드(직접 받음)
  • 구글이 로그인이 끝나면, 프론트를 거치지 않고 백엔드 서버 주소로 바로 들어온다.
  • 프론트엔드는 중간에 끼어들 틈이 없다. 백엔드가 혼자 북 치고 장구 치고 다 한다.

URL 반환 방식 (JSON 응답)

"내가(개발자가) 직접 조립해서 데이터로 건네준다"

  • 누가 조립하나? : 작성자님이 만든 Service나 Controller에서 코드(Java)로 직접 문자열을 붙여서 조립한다.
  • 어떻게 주나? : 명령(302)이 아니라 정보(JSON)로 준다. "자, 이게 구글 로그인 주소야. 이걸로 이동시키든 말든 프론트 네가 알아서 해~" 하고 텍스트만 던져준다.

 

  • Front: 구글 로그인 후 받은 '인증 코드(Code)'를 백엔드로 던져준다. (POST /api/login/google)
  • Back: 받은 코드를 가지고 구글 서버에 가서 "이거 진짜냐?" 하고 물어본다.
  • Back: 구글이 "어, 진짜네" 하고 확인해주면(토큰 발급), 그때 우리 서비스의 로그인(JWT 발급)을 완료한다.

 

 

 

5. JSON 방식이 보안적으로 더 우수한 이유

 

해당 방식이 보안측면에서도 더 우수하다.

 

정확히 말하면 SPA(React/Vue) 환경에서 발생할 수 있는 보안 구멍(CORS, 쿠키 문제)을 막기에 훨씬 유리하다.

 

리다이렉트 방식을 SPA에 억지로 끼워 맞추려다 보면 오히려 보안 설정을 해제해야 하는 상황이 올 수 있다.

 

리다이렉트 방식을 A방식, JSON 반환 방식을 B방식이라 하면

 

5-1. "쿠키 지옥(Cookie Hell)"과 CORS 이슈 회피

사실 이게 가장 큰 이유이다. A 방식을 쓰면 세션 쿠키에 의존해야 하는데, 여기서 보안 구멍이 뚫리기 쉽다.

  • A 방식의 문제점:
    • 스프링 시큐리티의 기본 리다이렉트 방식은 과정 중에 JSESSIONID 같은 쿠키를 사용해서 사용자를 임시로 식별
    • 그런데 프론트(localhost:3000)와 백엔드(localhost:8080)의 도메인이나 포트가 다르면, 브라우저의 강력한 보안 정책(SameSite, Secure 등) 때문에 쿠키가 차단
    • 위험한 해결책: 개발자들이 이걸 해결하겠다고 SameSite=None으로 설정을 풀거나, 보안 수준을 낮춰버리는 경우가 생김  → 해킹에 취약해짐.
  • B 방식의 장점:
    • 쿠키를 안씀
    • 프론트가 코드를 받아서 백엔드에 POST 요청의 Body(JSON)로 담아 보냄
    • 브라우저의 까다로운 쿠키 정책과 싸울 필요가 없으니, 보안 설정을 억지로 낮출 필요도 없음

 

5-2. "코드 탈취(Interception)" 방지: PKCE 활용 가능

 

B 방식을 쓰면 PKCE(Pixie, 픽시)라는 강화된 보안 기술을 적용하기가 구조적으로 매우 자연스럽다.

  • 시나리오: 해커가 사용자의 브라우저를 엿보고 있다가, 구글에서 돌아오는 '인증 코드'를 중간에서 가로챘다고 가정해 보자
  • A 방식: 서버가 주도하므로 프론트엔드 단에서 추가적인 검증 로직을 넣기가 까다로움
  • B 방식 (PKCE 적용 시):
    1. 출발 전: 프론트가 랜덤한 암호(Verifier)를 생성해서 들고 있음
    2. 도착 후: 백엔드에 코드를 줄 때, 아까 만든 암호도 같이 줌
    3. 검증: 백엔드는 "이 코드가 아까 그 브라우저가 요청한 게 맞는지" 암호로 대조
    4. 결과: 해커가 '인증 코드'를 훔쳐도, 프론트가 메모리에 숨겨둔 '암호'가 없어서 로그인을 못함

요약: B 방식은 프론트엔드에서 인증 흐름을 제어하므로, 최신 보안 표준인 PKCE를 구현하여 "중간에 코드를 도둑맞아도 안전한 상태"를 만들기 좋다.

5-3. CSRF(위조 요청) 공격 방어의 용이성

  • A 방식: 전통적인 방식은 쿠키 기반이므로 CSRF 공격(해커가 사용자 몰래 로그인된 상태를 이용해 요청을 날리는 것)에 취약할 수 있다. 그래서 CSRF Token 설정을 복잡하게 해야 한다.
  • B 방식:
    • 최종적으로 발급받은 JWT(액세스 토큰)를 프론트엔드가 변수(메모리)에 저장하거나, API 요청 시 Header(Authorization: Bearer ...)에 담아 보냄
    • 헤더에 담는 방식은 쿠키 방식보다 CSRF 공격을 하기가 기술적으로 훨씬 어렵다. (해커가 스크립트로 헤더를 조작하기 어렵기 때문)

 

6. 결론

  • 간단하고 빠르게 구현하고 싶다면: 리다이렉트 방식 (Spring 기본 사용)
  • 프론트엔드에서 세밀한 UX 제어가 필요하다면: JSON 방식

 

필자도 지금까지 리다이렉트 방식으로 구현을 해왔다.(사실 JSON 방식을 몰랐음)

 

하지만 추후 확장성이나 보안적인 측면에서도 JSON 방식이 더 좋은거 같다.

 

그러나 모든 것은 trade-off가 존재한다.

 

만약에 빠르게 MVP를 출시해야 한다면 기존에 구현해봤던 리다이렉트 방식을 채택하는 것도 무조건 나쁘다고 할 수 없다.

 

다만 로그인을 구현할 때 이러한 방식도 있으니 고려해보고 한다면 더 훌륭한 로그인이 완성되지 않을까 하는 생각이다.