2021년 12월 21일

CORS

fetch로 요청을 보내게 될 사이트가 현재 접속 사이트와 다르다면 요청이 실패할 수 있습니다.

직접 http://example.com라는 사이트에 요청을 보내봅시다.

try {
  await fetch('http://example.com');
} catch(err) {
  alert(err); // TypeError: Failed to fetch
}

요청이 실패한 것을 확인할 수 있네요.

왜 요청이 실패하는지 알기 위해선 도메인·프로토콜·포트 세 가지에 의해 결정되는 오리진(origin) 이라는 핵심 개념을 알아야 합니다.

도메인이나 서브도메인, 프로토콜, 포트가 다른 곳에 요청을 보내는 것을 Cross-Origin Request(크로스 오리진 요청)라고 합니다. 크로스 오리진 요청을 보내려면 리모트 오리진에서 전송받은 특별한 헤더가 필요합니다.

이러한 정책을 'CORS(Cross-Origin Resource Sharing, 크로스 오리진 리소스 공유)'라고 부릅니다.

왜 CORS가 필요한가에 대한 짧은 역사

CORS는 악의를 가진 해커로부터 인터넷을 보호하기 위해 만들어졌습니다.

지금부턴 어떤 사건 때문에 CORS가 만들어졌는지를 짧은 역사를 통해 살펴보겠습니다.

과거 수 년 동안, 한 사이트의 스크립트에서 다른 사이트에 있는 콘텐츠에 접근할 수 없다는 제약이 있었습니다.

이런 간단하지만 강력한 규칙은 인터넷 보안을 위한 근간이었습니다. 보안 규칙 덕분에 해커가 만든 웹 사이트 hacker.com에서 gmail.com에 있는 메일 박스에 접근할 수 없던 것이죠. 사람들은 이런 제약 덕분에 안전하게 인터넷을 사용할 수 있었습니다.

그런데 이 당시의 자바스크립트는 네트워크 요청을 보낼 수 있을 만한 메서드를 지원하지 않았습니다. 자바스크립트는 웹 페이지를 꾸미기 위한 토이 랭귀지 수준이었죠.

하지만 많은 웹 개발자들이 강력한 기능을 원하기 시작하면서 위와 같은 제약을 피해 다른 웹 사이트에 요청을 보내기 위한 트릭들을 만들기 시작했습니다.

폼 사용하기

트릭 중 하나로 <form>이 사용되곤 했습니다. 개발자들은 <form>안에 <iframe>을 넣어 <form>을 전송했습니다. 이렇게 해 현재 사이트에 남아있으면서 네트워크 요청을 보냈었죠. 예시를 살펴봅시다.

<!-- 폼 target -->
<iframe name="iframe"></iframe>

<!-- 자바스크립트를 사용해 폼을 동적으로 생성하고 보냄-->
<form target="iframe" method="POST" action="http://another.com/…">
  ...
</form>

이 당시엔 네트워크 관련 메서드가 없었지만, 폼은 어디든 데이터를 보낼 수 있다는 특징을 이용해 폼으로 다른 사이트에 GET, POST 요청을 보냈었습니다. 하지만 다른 사이트에서 <iframe>에 있는 콘텐츠를 읽는 것은 금지되었기 때문에 응답을 읽는 것은 불가능했습니다.

개발자들은 iframe과 페이지 양쪽에 특별한 스크립트를 심어 이런 제약 역시 피할 수 있는 트릭을 만들었습니다. 이렇게 iframe을 사용한 트릭은 오리진이 다른 사이트간에도 양방향 통신이 가능하도록 해주었죠. 어떤 스크립트를 사용하면 가능한지에 대해선 다뤄도 의미가 없기 때문에 상세한 내용은 넘어가도록 하겠습니다.

스크립트 사용하기

script 태그를 사용하는 것 역시 제약을 피하기 위한 트릭이었습니다. script 태그의 src 속성값엔 도메인 제약이 없기 때문입니다. 이 특징을 이용하면 어디서든 스크립트를 실행할 수 있습니다.

script 태그를 사용해 <script src="http://another.com/…"> 형태로 another.com에 데이터를 요청하게 되면 'JSONP(JSON with padding)'라 불리는 프로토콜을 사용해 데이터를 가져오게 됩니다.

단계별로 살펴보면 다음과 같습니다.

날씨 정보가 저장되어 있는 http://another.com에서 데이터를 가져와야 한다고 가정하겠습니다.

  1. 먼저 서버에서 받아온 데이터를 소비하는 전역 함수 gotWeather를 선언합니다.

    // 1. 날씨 데이터를 처리하는데 사용되는 함수를 선언
    function gotWeather({ temperature, humidity }) {
      alert(`temperature: ${temperature}, humidity: ${humidity}`);
    }
  2. 다음으로 src="http://another.com/weather.json?callback=gotWeather"를 속성으로 갖는 <script> 태그를 만듭니다. 1에서 만든 함수를 URL 매개변수 callback의 값으로 사용하였습니다.

    let script = document.createElement('script');
    script.src = `http://another.com/weather.json?callback=gotWeather`;
    document.body.append(script);
  3. 리모트 서버 another.com은 클라이언트에서 필요한 날씨 데이터와 함께 gotWeather(...)를 호출하는 스크립트를 동적으로 생성합니다.

    // 서버로부터 다음과 같은 형태의 응답이 클라이언트로 전송됩니다.
    gotWeather({
      temperature: 25,
      humidity: 78
    });
  4. 리모트 서버에서 받아온 스크립트가 로드 및 실행되면 gotWeather가 호출됩니다. gotWeather는 현재 페이지에서 만든 함수이고 서버에서 받은 데이터도 있기 때문에 원하는 결과를 얻을 수 있습니다.

이런 꼼수를 쓰면 보안 규칙을 어기지 않으면서도 양방향으로 데이터를 전달할 수 있습니다. 양쪽에서 동의한 상황이라면 해킹도 아니죠. 아직도 이런 방식을 사용해 통신을 하는 서비스가 있습니다. 이 방식은 오래된 브라우저도 지원합니다.

그러던 와중에 브라우저측 자바스크립트에도 네트워크 관련 메서드가 추가됩니다.

처음 네트워크 요청 메서드가 등장했을 때엔 크로스 오리진 요청이 불가능했습니다. 하지만 긴 논의 끝에 크로스 오리진 요청을 허용하기로 결정합니다. 대신 크로스 오리진 요청은 서버에서 명시적으로 크로스 오리진 요청을 ‘허가’ 했다는 것을 알려주는 특별한 헤더를 전송받았을 때만 가능하도록 제약을 걸게 됩니다.

안전한 요청

크로스 오리진 요청은 크게 두 가지 종류로 구분됩니다.

  1. 안전한 요청(safe request)
  2. 그 외의 요청(안전한 요청이 아닌 요청)

안전한 요청은 그 외의 요청 대비 만들기 쉽습니다. 그러니 먼저 안전한 요청부터 살펴보도록 합시다.

안전한 요청은 다음과 같은 두 가지 조건 모두를 충족하는 말 그대로 안전한 요청입니다.

  1. 안전한 메서드(safe method) – GET이나 POST, HEAD를 사용한 요청
  2. 안전한 헤더(safe header) – 다음 목록에 속하는 헤더
    • Accept
    • Accept-Language