진형주@hyoungjoojin · Sep 13, 2026Claude Code(으)로 작성

로그인은 되는데 새로고침하면 풀린다 — 범인은 SameSite였다

로컬에서는 멀쩡히 되던 로그인이 프로덕션에만 올리면 깨졌다. 증상이 묘했다.

  • 로그인 요청은 200을 돌려준다

  • 응답에 Set-Cookie: SESSION=...도 분명히 있다

  • 그런데 새로고침하면 /auth/me가 401

  • 개발자 도구 Application 탭에는 쿠키가 아예 없다

서버는 쿠키를 줬는데 브라우저가 안 받은 것이다. 서버 로그만 보면 절대 안 보이는 종류의 문제였다.

원인

로컬과 프로덕션의 도메인 구성이 달랐다.

웹

API

로컬

localhost:3000

localhost:8080

프로덕션

sync.skkil.org

api.skkil.org

포트만 다른 localhost는 같은 사이트(same-site)로 취급된다. 그런데 sync.skkil.org와 api.skkil.org는 크로스 사이트다.

Spring Session이 기본으로 내려주는 쿠키는 SameSite=Lax다. Lax는 크로스 사이트 XHR에 쿠키를 붙이지 않는다. 즉 브라우저는 규칙대로 정확히 동작했고, 로컬 환경이 이 차이를 가려주고 있었을 뿐이다.

로컬에서 재현이 안 되는 게 이 버그의 본질이었다.

해결

두 가지 선택지가 있었다.

1. 쿠키를 크로스 사이트로 허용한다

server:
  servlet:
    session:
      cookie:
        same-site: none
        secure: true

SameSite=None은 Secure 없이는 브라우저가 무시한다. 둘은 반드시 같이 간다. 프론트도 맞춰야 한다.

fetch(url, { credentials: "include" })

서버 CORS 설정에 allowCredentials(true)와 명시적 오리진도 필요하다. allowedOrigins("*")와 allowCredentials(true)는 함께 쓸 수 없다.

2. 같은 사이트로 만든다 (선택한 쪽)

nginx에서 /api를 같은 도메인으로 프록시해 크로스 사이트 상황 자체를 없앴다.

location /api/ {
    proxy_pass http://server:8080/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

SameSite=Lax를 그대로 두고도 동작하고, CORS 설정 자체가 필요 없어진다. 보안 기본값을 낮추지 않는 쪽이라 이걸 택했다.

배운 것

  • 로컬에서 재현되지 않는 버그는 환경 차이가 원인이다. 코드를 더 들여다봐도 안 나온다. 로컬과 프로덕션의 차이 목록부터 적었어야 했다.

  • 쿠키가 안 붙을 때 확인 순서: Set-Cookie 헤더가 응답에 있는가 → 브라우저가 저장했는가 → 다음 요청에 붙는가. 셋은 다른 단계고, 우리는 두 번째에서 끊겼다.

  • SameSite=None은 항상 Secure와 한 쌍이다.

로컬 → 프로덕션 환경 차이를 하나로 정리하는 문서가 팀에 없다는 게 진짜 문제였다. 별도로 만들기로 했다.

댓글

아직 댓글이 없습니다.