로컬 삽질 분투기 (1편): 사라진 쿠키와 응답을 찾아서

kiwon kim·2025년 4월 8일

Frontend

목록 보기
24/30

부제: 내 localhost 쿠키는 왜 저장되지 않고, API 응답은 보이지 않았을까?

작성일: 2025년 4월 8일

안녕하세요! 웹 개발 여정은 종종 예상치 못한 난관으로 가득 차 있죠. 최근 저는 로컬 개발 환경(http://localhost:3000, Next.js)에서 외부 API 서버(https://api-something.com)와 통신하며 쿠키 기반 인증을 구현하다가, 마치 유령처럼 쿠키가 사라지고 API 응답 본문조차 보이지 않는 기묘한 현상과 씨름해야 했습니다. 배포 환경에서는 멀쩡한 기능이 왜 로컬에서만 말썽일까요? 이 글은 그 원인을 찾아 헤맨 좌충우돌 디버깅 여정의 첫 번째 파트입니다.

가장 먼저 부딪힌 문제는 명백했습니다. API 서버는 분명 Set-Cookie 헤더를 포함한 응답(200 OK)을 보내는데, 브라우저 개발자 도구의 'Application' 탭에는 쿠키가 감쪽같이 사라져 있었습니다. 이 문제를 해결하기 위해 먼저 Set-Cookie 헤더가 무엇인지부터 짚고 넘어가야 했습니다.

Set-Cookie 헤더란 무엇인가?

Set-CookieHTTP 응답 헤더 중 하나로, 웹 서버가 웹 브라우저에게 "이 데이터를 쿠키로 저장해줘!" 라고 요청할 때 사용됩니다. 서버는 이 헤더를 통해 쿠키의 이름, 값뿐만 아니라 쿠키의 동작 방식을 제어하는 여러 속성(Attribute)들을 함께 전달합니다.

주요 속성들은 다음과 같습니다.

  • 이름=값: 저장될 쿠키의 이름과 값입니다. (예: session_id=abc123xyz)
  • Expires=날짜 / Max-Age=초: 쿠키의 만료 시점을 지정합니다.
  • Domain=도메인: 쿠키가 유효한 도메인을 제한합니다.
  • Path=경로: 쿠키가 유효한 서버 내 경로를 제한합니다.
  • Secure: HTTPS 연결에서만 쿠키를 전송하도록 합니다.
  • HttpOnly: JavaScript에서 쿠키에 접근하는 것을 막아 XSS 공격을 일부 방어합니다.
  • SameSite=Strict|Lax|None: 다른 사이트에서 요청 시 쿠키 전송 정책을 결정하여 CSRF 공격을 방어하는 데 중요합니다.

브라우저는 Set-Cookie 헤더를 받으면 이 정보(이름, 값, 속성)를 내부에 저장합니다. 그리고 이후 동일한 서버(Domain, Path 조건 만족 시)로 요청을 보낼 때는 저장된 쿠키 정보를 Cookie 요청 헤더에 담아 자동으로 서버에 전송합니다.


이제 Set-Cookie 헤더의 기본을 알았으니, 왜 제 쿠키가 저장되지 않았는지 그 속성들을 자세히 들여다보았습니다.

디버깅 시작: 개발자 도구 'Network' 탭에서 응답 헤더의 Set-Cookie 값을 샅샅이 뒤졌습니다.

Set-Cookie 헤더 예시:

HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: session_id=abc123xyz; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
Set-Cookie: user_pref=dark; Path=/; Domain=api-something.com; Expires=Wed, 21 Oct 2025 07:28:00 GMT

위 예시를 통해 문제의 원인을 찾을 수 있었습니다.

  1. Secure 속성 발견:

    • Set-Cookie: ...; Secure; ...
    • Secure 속성은 쿠키가 오직 HTTPS 연결을 통해서만 전송되고 저장되도록 강제합니다. 제 로컬 환경은 http://localhost:3000 (HTTP)였기에, 브라우저는 이 session_id 쿠키를 보안상 거부했던 것입니다.
    • 💡 교훈: Secure 속성은 HTTPS 환경을 위한 것입니다. 로컬 HTTP 테스트 시에는 조건부로 제외하거나 로컬에도 HTTPS 환경(예: mkcert)을 구성해야 합니다.

  2. Domain 속성 발견:

    • Set-Cookie: ...; Domain=api-something.com; ...
    • Domain 속성은 쿠키가 유효한 도메인을 지정합니다. 브라우저는 쿠키를 설정하는 서버의 도메인과 Domain 속성 값이 일치하거나 상위 도메인 관계일 때만 쿠키를 저장합니다. http://localhost:3000 에서 Domain=api-something.com 으로 설정된 user_pref 쿠키를 설정하려 하니 당연히 실패할 수밖에 없었습니다.
    • 💡 교훈: Domain 속성은 쿠키 적용 범위를 지정합니다. 로컬과 API 도메인이 다를 때 문제가 될 수 있습니다. 로컬 테스트 시에는 보통 이 속성을 생략하는 것이 좋습니다.

1.2 응답 본문 실종? - 200 OK의 함정

쿠키 저장 문제는 위 속성들로 설명이 가능했지만, 더 이상한 문제가 기다리고 있었습니다. 200 OK 상태 코드와 함께 Content-Length 헤더 값도 상당히 큰데, 정작 네트워크 탭의 'Response' 탭에는 아무런 데이터도 표시되지 않는 것이었습니다!

  • 확인: 'Preview' 탭뿐만 아니라 원시 데이터를 보여주는 'Response' 탭을 확인했습니다. 역시 비어 있었습니다. 서버 로그나 코드 상에도 로컬 환경에서 빈 응답을 보낼 이유는 없어 보였습니다.
  • 교훈: 200 OK는 요청 성공을 의미할 뿐, 응답 본문의 존재를 보장하지는 않습니다. 하지만 Content-Length 헤더는 중요한 단서가 됩니다. 데이터는 분명 왔는데 왜 보이지 않을까요?

1.3 결정적 단서! - CORS 문제 인지

답답한 마음에 API 엔드포인트 URL(https://api-something.com/api/token)을 브라우저 주소창에 직접 입력해 보았습니다. 놀랍게도 응답 본문 데이터가 브라우저 화면에 잘 출력되었습니다!

이 순간 깨달았습니다. 문제는 제 로컬 서버(localhost)가 아니라, http://localhost:3000 (프론트엔드)에서 https://api-something.com (백엔드 API)으로 요청을 보내는 것 자체가 크로스 오리진(Cross-Origin) 요청이라는 사실을요! 브라우저는 보안상의 이유로 스크립트에서 시작된 크로스 오리진 HTTP 요청을 제한합니다. 이것이 바로 CORS(Cross-Origin Resource Sharing) 정책입니다. 응답 본문이 보이지 않았던 현상 뒤에는 CORS가 숨어있을 가능성이 매우 높아졌습니다.

💡 교훈: 프론트엔드와 API의 오리진(프로토콜, 호스트, 포트)을 명확히 파악하고, 오리진이 다르다면 CORS 문제 발생 가능성을 항상 최우선으로 염두에 두어야 합니다.


(2편에서 계속...) 다음 편에서는 이 CORS 문제와 본격적으로 씨름하며 withCredentials 옵션의 복잡성, 그리고 전혀 예상치 못했던 개발자 도구의 함정까지 파헤쳐 보겠습니다. 관련 코드 예시도 함께 살펴볼 예정입니다.


(2편과 3편의 내용은 이전 답변과 동일하게 유지됩니다. Set-Cookie 설명은 1편에만 추가되었습니다.)

profile
FOR_THE_BEST_DEVELOPER

0개의 댓글