로컬 삽질 분투기 (2편): CORS와의 사투와 개발자 도구의 배신 😱

kiwon kim·2025년 4월 8일

Frontend

목록 보기
25/30

부제: CORS 헤더는 맞는데 왜 응답이 안 보일까? 범인은 내부에 있었다!

(1편 요약) 로컬 환경(http://localhost:3000)에서 외부 API(https://api-something.com) 호출 시 쿠키 저장 문제의 원인(Secure, Domain 속성)을 찾았지만, 여전히 응답 본문이 보이지 않는 문제가 있었습니다. API URL 직접 접속 시 본문이 보이는 것을 확인하고, 이것이 크로스 오리진(CORS) 문제임을 인지했습니다.


이제 CORS와의 본격적인 싸움이 시작되었습니다. 특히 쿠키 기반 인증을 위해 사용했던 axios({ withCredentials: true }) 옵션이 상황을 더 복잡하게 만들었습니다.

2.1 CORS와 withCredentials의 복잡성

withCredentials: true 옵션은 크로스 오리진 요청에 쿠키 같은 자격 증명을 포함시키는 대신, 서버에 더 엄격한 CORS 규칙 준수를 요구합니다.

클라이언트 요청 예시 (axios):

// axios 요청 시 withCredentials 설정
axios.get('https://api-something.com/api/user', {
  withCredentials: true // 쿠키를 함께 보내기 위해 true로 설정
})
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.error('API 요청 오류:', error);
  // CORS 에러는 여기서 잡히지 않을 수 있음 (네트워크 레벨에서 차단)
});

서버 응답 헤더 요구사항: withCredentials: true 요청을 받은 서버는 응답 시 다음 헤더를 반드시 포함해야 합니다.

Access-Control-Allow-Origin: http://localhost:3000  # '*' 불가, 정확한 오리진 명시
Access-Control-Allow-Credentials: true             # 반드시 'true'
Vary: Origin                                     # Origin에 따라 응답이 달라짐을 명시 (권장)

디버깅 CORS:

  1. 콘솔 확인: 이상하게도 콘솔에는 명시적인 CORS 오류 메시지가 보이지 않았습니다.
  2. 네트워크 탭 확인:
    • 사전 요청 (OPTIONS Preflight Request): 비단순 요청 시 브라우저가 보내는 OPTIONS 요청과 그 응답 헤더(Access-Control-Allow-Methods, Access-Control-Allow-Headers)를 확인해야 합니다.
      # OPTIONS 요청에 대한 서버 응답 헤더 예시
      Access-Control-Allow-Origin: http://localhost:3000
      Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
      Access-Control-Allow-Headers: Content-Type, X-Custom-Header # 요청에 포함된 비단순 헤더 명시
      Access-Control-Allow-Credentials: true # withCredentials 사용 시 필요할 수 있음
      Access-Control-Max-Age: 86400
    • 본 요청 응답 헤더: 실제 요청(GET/POST 등)의 응답 헤더를 확인했습니다. 놀랍게도 Access-Control-Allow-Origin: http://localhost:3000Access-Control-Allow-Credentials: true 가 모두 올바르게 설정되어 있었습니다!

상황은 다시 미궁 속으로 빠졌습니다. CORS 설정은 맞는 것 같은데 왜 본문은 여전히 보이지 않고, 네트워크 탭에는 "Failed to load response data..." 라는 메시지만 덩그러니 있을까요?

크로스 오리진이 아닌 같은 도메인인 경우

같은 도메인으로 요청을 보내고 있다면, Axios에서 withCredentials: true 옵션을 사용하지 않아도 기본적으로 브라우저가 자동으로 해당 도메인의 쿠키를 요청 헤더에 포함시켜서 보냅니다.

이것이 브라우저의 표준 동작 방식입니다. Same-Origin (동일 출처) 정책 하에서는 별도의 설정을 요구하지 않습니다.

따라서, 프론트엔드 애플리케이션과 API 서버가 동일한 프로토콜, 도메인, 포트를 사용하고 있다면 withCredentials 설정 없이도 쿠키가 잘 전송될 것입니다.

요약:

💡요약:
같은 도메인 요청: withCredentials: true 필수 아님. 브라우저가 자동으로 쿠키 전송.
다른 도메인 요청: withCredentials: true 필수. + 서버 CORS 설정 필요.

💡 교훈: withCredentials: true는 CORS를 더 복잡하게 만듭니다. 서버의 응답 헤더(사전 요청과 본 요청 모두)를 철저히 검증해야 하며, 콘솔 에러 메시지만으로 단정하기 어려울 수도 있습니다.

2.2 개발자 도구의 함정 - Preserve log

계속되는 미스터리에 지쳐갈 때쯤, 네트워크 탭의 오류 메시지 "Failed to load response data: No resource with given identifier found"에 주목했습니다. 이 메시지를 구글링한 결과, 예상치 못한 범인을 찾았습니다!

원인: 크롬 개발자 도구의 'Preserve log'(로그 보존) 옵션이 켜진 상태에서, 네트워크 요청이 완료된 후 페이지를 이동하거나 새로고침하고 나서 이전 요청의 응답 본문을 보려고 할 때 발생하는 알려진 이슈였습니다.

브라우저는 메모리 관리 등의 이유로 페이지 이동 후에는 이전 요청의 실제 응답 본문 데이터를 폐기할 수 있습니다. 'Preserve log'는 로그 목록은 유지하지만, 본문 데이터까지 영구 보존하는 것은 아니었던 것이죠. 나중에 로그 항목을 클릭하면 이미 사라진 데이터를 참조하려다 해당 오류 메시지를 표시하는 것이었습니다.

결국, CORS 헤더가 올바름에도 응답 본문이 보이지 않았던 현상은 (적어도 개발자 도구 상에서는) CORS 문제가 아니라 개발자 도구의 특정 동작 방식 때문이었습니다.

💡 교훈: 개발자 도구는 강력하지만 완벽하지 않습니다! 특정 옵션('Preserve log')이나 버그가 문제 해결을 방해할 수 있습니다. 도구의 특성을 이해하고 다른 브라우저(Firefox 등)나 콘솔 테스트(Workspace, console.log) 등 보조적인 방법을 활용하는 것이 좋습니다.

2.3 새로운 활로 모색: 토큰 기반 인증으로!

쿠키 방식의 CORS 설정과 SameSite 속성 등의 복잡함, 그리고 개발자 도구 이슈까지 겪고 나자, 저는 과감히 인증 방식을 전환하기로 결정했습니다. 바로 토큰 기반 인증입니다. 서버가 Set-Cookie 대신 응답 본문에 토큰을 주고, 클라이언트는 이를 localStorage에 저장 후 요청 시 Authorization 헤더에 담아 보내는 방식이죠.


(3편에서 계속...) 다음 편에서는 토큰 기반 인증 방식의 구체적인 구현 코드 예시, 이 방식이 왜 갑자기 작동했는지에 대한 추측, 토큰 방식의 CORS 고려사항, 그리고 쿠키와 토큰 방식의 보안 비교 및 최종 교훈으로 이 길고 길었던 디버깅 여정을 마무리하겠습니다.

profile
FOR_THE_BEST_DEVELOPER

0개의 댓글