[TYPESCRIPT] CORS 설정, SameSite/도메인 정책, 개발/운영 환경 분리 - 쿠키 기반 인증이 운영에서 안 흔들리는 기준

 

쿠키 기반 인증(특히 Refresh Token을 HttpOnly 쿠키로 두는 구조)을 도입하면 프론트/백엔드에서 가장 많이 겪는 문제가 바로 CORS, SameSite, 도메인입니다.

개발 환경에서는 잘 되던 것이 운영에서 깨지거나, 반대로 운영에서는 잘 되는데 로컬에서만 안 되는 경우도 흔합니다. 대부분 원인은 “브라우저 쿠키 정책 + CORS 정책 + 배포 도메인 구조”가 맞지 않아서입니다.

실무에서 자주 쓰는 기준을 중심으로 다음을 정리합니다.

  • CORS 설정의 핵심 포인트(credentials, origin)
  • SameSite/도메인 정책(서브도메인, 쿠키 스코프)
  • 개발/운영 환경 분리(로컬, 스테이징, 프로덕션)

예시는 Node/NestJS 계열 기준으로 설명하지만, Spring/Express 등에서도 원리는 동일합니다.

 

 

CORS는 “브라우저가 서버에 거는 제약”

CORS는 서버 기능이 아니라 브라우저의 보안 정책입니다. 핵심은 “다른 출처(origin)로 요청할 때, 브라우저가 응답을 JS에 전달해도 되는지”를 판단하는 규칙입니다.

쿠키 기반 인증을 쓸 때는 CORS의 다음 조합이 사실상 필수입니다.

  • 클라이언트 요청: credentials: "include"
  • 서버 응답: Access-Control-Allow-Credentials: true
  • 서버 응답: Access-Control-Allow-Origin* 불가 (정확한 origin 지정)

여기서 가장 많이 하는 실수는 Allow-Origin을 *로 두고 credentials를 켜는 것입니다. 이 조합은 브라우저가 보안상 차단합니다.

 

프론트 설정: 쿠키를 보내려면 항상 include

fetch/RTK Query/Axios 모두 개념은 같습니다. 쿠키(Refresh Token)를 요청에 포함시키려면 다음이 필요합니다.

1. fetch / RTK Query

fetch("/api/auth/refresh", {
  method: "POST",
  credentials: "include",
});

2. Axios

axios.post("/api/auth/refresh", {}, { withCredentials: true });

서버에서 Set-Cookie를 내려줘도, 클라이언트가 credentials 설정을 안 하면 쿠키가 저장/전송되지 않는 케이스가 생깁니다.

 

 

3. 서버 CORS 설정의 실무 기준

서버에서는 “허용할 origin 목록”을 명확히 관리해야 합니다. 개발/스테이징/운영이 섞이면 반드시 사고가 납니다.

1. 허용 origin을 화이트리스트로

const allowedOrigins = [
  "http://localhost:5173",
  "http://localhost:3000",
  "https://stg.example.com",
  "https://app.example.com",
];

app.enableCors({
  origin: (origin, cb) => {
    // 같은 출처(예: 서버 to 서버) 요청은 origin이 undefined일 수 있음
    if (!origin) return cb(null, true);
    if (allowedOrigins.includes(origin)) return cb(null, true);
    return cb(new Error("Not allowed by CORS"), false);
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization"],
});

포인트:

  • origin을 함수로 두면 환경별 제어가 쉬움
  • credentials: true는 쿠키 인증에서 필수
  • Authorization 헤더를 쓰면 allowedHeaders에 포함

 

SameSite는 “쿠키가 언제 전송되는가”를 결정

SameSite는 쿠키가 “다른 사이트 요청”에 포함될지 결정합니다. 대충 두고 넘어가면 개발/운영 중 한쪽에서 반드시 깨집니다.

  • Strict: 거의 같은 사이트 내에서만 전송 (가장 엄격)
  • Lax: 일반적인 웹앱에서 많이 쓰는 균형점 (대부분의 cross-site 상황 제한)
  • None: cross-site 요청에도 전송 허용 (단, Secure 필수)

실무 결론은 보통 다음 두 가지 중 하나입니다.

  • 프론트/백이 같은 사이트로 묶일 수 있으면Lax (또는 Strict)로 유지
  • 프론트/백이 완전히 다른 도메인이면 → None + Secure + CSRF 전략 고려

중요: SameSite=None은 반드시 Secure(HTTPS)여야 브라우저가 쿠키를 저장합니다.

 

도메인 정책: “쿠키가 어느 호스트에 붙는가”

쿠키는 기본적으로 발급한 호스트에만 붙습니다. 여기서 자주 하는 실수는 “서브도메인 간 쿠키 공유”를 대충 기대하는 것입니다.

예를 들어 아래 구조를 생각해보겠습니다.

  • 프론트: app.example.com
  • API: api.example.com

이 경우 쿠키를 공유하려면 보통 다음처럼 설정합니다.

  • Domain=.example.com (모든 서브도메인에 전송)
  • Path=/

반대로, API 도메인에서만 쿠키를 쓰고 싶다면 Domain을 지정하지 않고 호스트 전용 쿠키로 두는 방식도 있습니다. 다만 이 경우 프론트가 직접 쿠키를 읽지 않아도 되므로(HttpOnly), 운영 정책상 더 안전하다고 보는 팀도 많습니다.

 

 

개발/운영 환경 분리에서 가장 중요한 것

실무에서 “인증 쿠키가 안 붙는다” 문제의 80%는 환경 분리가 깔끔하지 않아서 생깁니다.

  • 로컬은 http인데 운영은 https
  • 운영은 SameSite=None인데 로컬은 Secure가 없어 저장이 안 됨
  • allowedOrigins에 로컬 주소가 빠짐
  • 스테이징 도메인이 운영과 쿠키 Domain 정책이 다름

따라서 환경 분리는 “코드 if문”이 아니라 도메인 구조 + 쿠키 정책 + CORS 정책을 세트로 맞춰야 합니다.

 

 

실무에서 자주 쓰는 환경별 권장 구성

1. 로컬 개발

  • 가능하면 프론트/백을 같은 출처로 맞추기(프록시 사용)
  • 쿠키: SameSite=Lax, Secure=false (http라서)
  • CORS: localhost origins만 허용

Vite 프록시로 “같은 출처처럼” 만드는 방식이 가장 편합니다.

2. 스테이징

  • 운영과 동일한 https + 도메인 구조로 맞추기
  • 쿠키: 운영과 동일 정책(Secure=true)
  • CORS: stg 도메인만 화이트리스트

3. 운영

  • 쿠키: HttpOnly + Secure + SameSite 정책 명확히
  • CORS: app 도메인만 허용(가급적 최소)
  • 도메인: app/api 서브도메인 구조라면 Domain 정책을 확정

 

디버깅 체크리스트(문제 발생 시)

인증 쿠키가 안 붙을 때는 아래 순서로 보면 대부분 해결됩니다.

  • 브라우저 DevTools > Network에서 응답에 Set-Cookie가 실제로 내려왔는지
  • Application > Cookies에 쿠키가 저장됐는지 (저장 자체가 안 되면 SameSite/Secure 문제일 가능성이 큼)
  • 요청에 Cookie 헤더가 포함되어 있는지 (credentials 미설정일 가능성)
  • 응답 헤더에 Access-Control-Allow-Origin이 정확히 찍혔는지
  • Access-Control-Allow-Credentials=true인지
  • preflight(OPTIONS)가 실패하는지(allowedHeaders/methods 확인)

특히 “쿠키가 저장되지 않는다”는 경우는 대부분 SameSite=None인데 Secure가 아니거나, 도메인/경로가 맞지 않는 케이스입니다.

 

 


 

 

쿠키 기반 인증은 보안적으로 강력하지만, 그만큼 브라우저 정책(CORS/SameSite/도메인)을 정확히 맞춰야 운영에서 안정적으로 굴러갑니다.

  • CORS: credentials를 쓰면 origin은 반드시 명시(와일드카드 금지)
  • SameSite: cross-site 필요 여부에 따라 Lax/None 선택, None은 Secure 필수
  • Domain: 서브도메인 공유 여부를 확정하고 cookie scope를 명시
  • 환경 분리: 로컬/스테이징/운영을 “같은 정책 세트”로 관리