웹 개발을 하다 보면 인증(Authentication)에서 막히는 순간이 온다. "로컬에서는 되는데 배포하면 안 돼요", "쿠키가 안 구워져요", "CORS 에러가 떠요". 이 글은 Better Auth, Cloudflare Workers, Next.js 환경을 포함하여, 웹 인증의 밑바닥 원리부터 실무 아키텍처까지 모든 정보를 총정리한다.
1. 쿠키 vs JWT: 무엇을 선택해야 할까?
인증 시스템(Better Auth 등)을 구성할 때 가장 먼저 마주하는 선택지다.
✅ 쿠키(Cookie) 기반 세션 (추천)
브라우저 환경에서는 이 방식이 **표준(Standard)**이다.
- 동작: 로그인 성공 시 서버가
Set-Cookie헤더를 보냄 → 브라우저는 이후 모든 요청에 자동으로 쿠키를 붙여 보냄.
- 서버 상태: 세션 ID가 DB(D1, Redis 등)에 저장됨.
- 장점:
- 보안:
HttpOnly,Secure옵션으로 XSS 방어 가능. - 제어: 서버에서 세션을 삭제하면 즉시 로그아웃(Revoke) 가능.
- Better Auth 기본값: Workers + D1 조합 시 이 방식이 기본이다.
✅ JWT (JSON Web Token)
- 동작:
Authorization: Bearer <token>헤더를 클라이언트가 직접 붙여서 보냄.
- 장점: Stateless(DB 조회 없음). 모바일 앱이나 서버 간 통신(S2S)에 적합.
- 단점: 한 번 발급된 토큰은 만료 전까지 취소하기 어려움(Blacklist 관리 필요). 토큰 저장 위치에 따른 보안 취약점 존재.
2. 쿠키의 해부: "어떤 인증은 되고, 어떤 건 안 되는 이유"
"쿠키가 안 붙어요"라는 말은 쿠키의 주소(Scope) 설정이 틀렸다는 뜻이다. 서버가
Set-Cookie를 보낼 때 결정된다.핵심 속성 3가지
- Domain (어디서 쓸 것인가?)
- 지정 안 함 (기본값): Host-Only Cookie. 쿠키를 구운 그 도메인(
api.myapp.com)에서만 사용 가능. 서브도메인 공유 불가. 가장 안전. - 지정함 (
Domain=myapp.com):myapp.com및 모든 서브도메인(api.myapp.com,www.myapp.com) 공유 가능. - 주의:
Domain=.myapp.com의 앞 점(.)은 최신 브라우저에서 무시된다.myapp.com과 동일. - 상위 전송 불가:
api.myapp.com에서 구운 쿠키는 상위인myapp.com으로 절대 전송되지 않음. 공유하려면 상위 도메인(myapp.com)으로 설정해야 함.
- Path (경로)
- 보통
Path=/로 설정하여 모든 경로에서 사용.
- SameSite (누가 보낼 수 있는가?) - 가장 중요
- CSRF(사이트 간 요청 위조) 방어를 위한 속성.
속성 | 동작 방식 | 설명 |
Lax (기본) | Same-Site + Top Level Navigation(GET) 허용 | 일반적인 웹사이트 표준. 링크 클릭 이동은 허용, fetch/iframe 등은 차단. |
Strict | Same-Site Only | 외부에서 들어오는 모든 요청(링크 클릭 포함)에 쿠키 차단. UX 안 좋음. |
None | Cross-Site 허용 | 모든 곳에서 전송 가능. 단, Secure(HTTPS) 필수. |
Sheets로 내보내기
3. SameSite와 보안 시나리오 (Deep Dive)
"SameSite=Lax면 안전한가요?", "GET 요청은 다 되나요?"에 대한 명확한 답.
Q. Lax에서 fetch(GET)은 되나?
아니오. (Cross-Site일 경우)
- 시나리오:
evil.com접속 → JS로fetch("https://my-community.com/api/me")실행.
- 결과: 쿠키 전송 안 됨. 로그인 풀린 상태로 응답 옴.
- 예외: 사용자가
<a>태그를 눌러서 페이지를 **이동(Navigate)**하는 경우(Top-level Navigation)에는 쿠키가 붙음.
Q. evil.com에서 내 서버로 요청을 100번 날리면 조회수가 오르나?
- Lax일 때: 쿠키가 안 붙으므로 "비로그인 조회수"라면 오를 수 있음.
- None일 때: 쿠키가 붙어서 날아감. "로그인 유저 조회수"가 오를 수 있음.
- CORS와의 관계: CORS 때문에
evil.com이 응답(결과)을 읽지는 못함. 하지만 서버 내부에서 **로직(조회수 증가, 좋아요, 송금)**은 실행될 수 있음. 이것이 CSRF 공격.
Q. Public Suffix List (PSL)란?
a.vercel.app과b.vercel.app은 같은vercel.app인데 쿠키 공유가 될까?
- 정답: 절대 안 됨.
- 브라우저는 **PSL(Public Suffix List)**에 등록된 도메인(
com,co.kr,vercel.app,github.io등)을 기준으로 도메인을 격리함.
- 따라서
vercel.app은 마치.com처럼 취급되어, 사용자 간 쿠키 공유가 원천 차단됨.
4. 도메인 아키텍처별 전략
내 서비스의 도메인 구조에 따라 인증 전략이 완전히 달라진다.
Case 1: 단일 도메인 / 서브도메인 (Best Practice)
- 구조:
myapp.com(프론트) +api.myapp.com(백엔드)
- 설정:
- Cookie:
Domain=.myapp.com,SameSite=Lax,HttpOnly,Secure
- 특징: 쿠키 공유 가능, 보안 좋음, CSRF 위험 낮음.
Case 2: 완전 분리 도메인 (Difficult)
- 구조:
frontend.com+api.backend.com
- 설정:
- Cookie:
SameSite=None,Secure
- 특징: 쿠키 공유 불가능(Domain 불일치).
None사용으로 인해 CSRF 취약. 추가 보안 대책(CSRF Token) 필수.
- 주의:
frontend.com에서 쿠키를 구우면backend.com으로는 절대 전송되지 않음.
Case 3: BFF (Backend For Frontend) 패턴 (Next.js 추천)
- 구조: 브라우저 ↔
frontend.com(Next.js 서버) ↔api.backend.com
- 설정:
- 브라우저는
frontend.com쿠키만 가짐 (SameSite=Lax,HttpOnly). - Next.js 서버가 API 호출 시 토큰을 헤더에 붙여서 중계.
- 특징: 브라우저 보안 이슈(CORS, Cross-Site Cookie)를 서버 사이드에서 모두 해결. 가장 추천하는 구조.
5. 저장소 보안: Cookie vs LocalStorage vs Memory
"토큰을 어디에 저장해야 안전한가?"
저장소 | XSS 취약점 | CSRF 취약점 | 특징 |
HttpOnly 쿠키 | 안전 (JS 접근 불가) | 취약 (자동 전송됨) | SameSite 설정과 CSRF 방어 필수. 가장 권장됨. |
일반 쿠키 | 취약 (JS 접근 가능) | 취약 | LocalStorage와 다를 바 없음. 쓰지 말 것. |
LocalStorage | 매우 취약 (영구 탈취) | 안전 (자동 전송 안 됨) | XSS 한 번 터지면 토큰 영구 탈취. |
메모리 (JS 변수) | 비교적 안전 (새로고침 시 증발) | 안전 | 가장 안전하나, 새로고침 시 로그인이 풀림. |
Sheets로 내보내기
🏆 실무 추천 조합 (Hybrid)
- Refresh Token:
HttpOnly Cookie에 저장 (브라우저 닫아도 유지, XSS 방어).
- Access Token: 메모리(JS 변수)에 저장.
- 동작: 앱 시작 시(또는 새로고침 시) Refresh Token으로 Access Token을 재발급받아 메모리에 적재.
6. 결론 및 체크리스트
- 기본은 쿠키다: 웹 앱이라면
HttpOnly Cookie세션 방식을 써라. JWT + LocalStorage는 보안 구멍이다.
- SameSite=Lax가 기본이다:
GET요청으로 상태를 변경(DB 수정)하게 만들지 마라.Lax는 GET 이동을 허용한다.
- 도메인이 다르면 BFF를 고려하라:
frontend.com과backend.com이 다르다면 Next.js 서버를 프록시로 쓰는 BFF 패턴이 정신 건강과 보안에 이롭다.
- CORS와 CSRF는 다르다: CORS 설정을 잘했다고 CSRF가 막히는 게 아니다.
SameSite=None을 쓴다면 반드시 CSRF 토큰이나 Origin 검증을 추가하라.
- PSL을 기억하라:
a.vercel.app과b.vercel.app은 남남이다. 쿠키 공유하려 하지 마라.