본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
문서와 리소스 로딩 챕터에서 간단하게 CORS
정책에 대해 이야기 한 적이 있다. 이는 브라우저를 사용하는 사용자를 위해 설정된 보안 정책으로 어떤 도메인에서 다른 도메인으로 네트워크 요청을 보낼 때 발생하는 권한 관련 이슈이다. 해당 챕터에서는 fetch
메서드를 사용할 때 발생할 수 있는 CORS
이슈에 대해 조금 더 자세히 살펴보고 관련 권한을 획득하여 정상적인 요청을 주고 받을 수 있는 방법에 대해 알아보자.
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // TypeError: Failed to fetch
}
fetch
로 요청을 보내게 될 사이트가 현재 접속 사이트와 다른 경우엔 요청이 실패할 수 있다. 이는 오리진(Origin
)이 다를때 발생하는 이슈인데, 오리진은 도메인/프로토콜/포트 세 가지에 의해 결정되는 일련의 주소를 말한다. 서로 다른 오리진끼리 요청을 전송하는 것을 Cross-Origin Request
라고 부르는데, 해당 요청을 정상적으로 전송하기 위해서는 리모트 오리진에서 특별한 헤더를 사전에 전송해 주어야 한다. 이러한 정책을 CORS(Cross-Origin Resource Sharing, 크로스 오리진 리소스 공유)
라고 부른다.
CORS
는 앞서 이야기한 바와 같이 브라우저를 사용하는 유저를 보호하기 위해 고안된 정책이다. 과거 수 년 동안, 한 사이트의 스크립트에서 다른 사이트에 있는 콘텐츠에 접근할 수 없다는 제약이 존재했다. 이와 같은 제약은 당연히 인터넷 보안을 위한 근간이었는데, 이를 통해 악의를 가진 해커가 만든 웹 사이트에서 다른 도메인으로 함부로 접근하는 행위 등을 차단할 수 있었다. 때문에 브라우저 사용자는 비교적 안전하게 인터넷을 사용할 수 있었다.
그러나 이 당시 자바스크립트는 네트워크 요청을 보낼 수 있을 만한 메서드를 지원하지 않았다. 당시에는 자바스크립트가 그저 웹 페이지를 조금 더 동적으로 꾸밀 수 있는 토이 랭귀지 수준이었기 때문이다.
하지만 많은 웹 개발자들이 강력한 기능을 원하기 시작하며, 이러한 제약을 피해 다른 웹 사이트에 요청을 보내기 위한 여러 트릭을 만들기 시작했다.
폼 사용하기
가장 대표적인 트릭 중 하나는 <form>
요소를 사용하는 것이었다. <form>
요소 안에 <iframe>
을 넣어 폼 전송을 했는데, 이를 통해 현재 사이트에 남아있으면서 네트워크 요청을 보내는 작업이 가능했다. 아래의 코드는 폼 요소를 통해 요청을 보내고 그 응답을 현재 페이지에 있는 <iframe>
에 받아오도록 하는 예시이다.
<!-- 폼 target -->
<iframe name="ifame"></iframe>
<!-- 자바스크립트를 사용해 폼을 동적 생성하고 전송 -->
<form target="iframe" method="POST" action="http://another.com">
...
</form>
이 당시엔 네트워크 관련 메서드가 없었지만, 폼은 어디서든 데이터를 보낼 수 있다는 특징을 이용해 폼으로 다른 사이트에 GET
, POST
요청을 전송할 수 있었다. 하지만 다른 사이트에서 <iframe>
에 있는 콘텐츠를 읽는 것은 금지되었기 때문에 응답을 읽는 것은 불가능했다.
그러나 개발자들은 iframe
과 페이지 양쪽에 특별한 스크립트를 심어 이런 제약 또한 우회할 수 있는 트릭을 만들었다. 이를 통해 오리진이 서로 다른 사이트 간에도 제약 없이 양방향 통신이 가능하도록 구현할 수 있었다. 어떤 식으로 우회하는 코드를 구현했는지는 따로 소개하지 않겠다.
스크립트 사용하기
script
태그를 사용하는 것 역시 제약을 피하기 위한 일종의 트릭으로 사용할 수 있었다. script
태그의 src
속성값에는 도메인 제약이 없기 때문이다. 이는 script
태그가 src
경로에 있는 대상을 호출한 결과를 포함시키는 것이 아니라 그저 실행시키는 역할이기 때문에 도메인 제약이 적용되지 않는다. 이 특징을 이용해 어디서든 스크립트를 실행할 수 있다. 이처럼 HTML 태그 중 몇몇은 외부 리소스에 접근하더라도 CORS
가 적용되지 않는 것들이 있다. 대표적으로 script
와 img
태그가 그러하다.
script
태그를 사용해 <script src="http://another.com">
형태로 다른 오리진에 데이터를 요청하게 되면 JSONP(JSON with Padding)
이라고 불리는 프로토콜을 사용해 데이터를 가져오게 된다.
이 과정을 단계별로 살펴보면 다음과 같다. 먼저 날씨 정보가 저장되어 있는 http://another.com
에서 데이터를 가져와야 한다고 가정해보자.
gotWeather
를 선언// JSONP 프로토콜에서는 서버에서 내려준 콜백함수가
// HTML에서 미리 정의되어 있어야 함
function gotWeather({ temperature, humidity }) {
alert(`temp: ${temperature}, hum: ${humidity}`);
}
src="http://another.com/weather.json?callback=gotWeather"
을 속성으로 갖는 script
태그를 만들고, 1번에서 만든 함수를 callback
매개변수로 지정let script = document.createElement('script');
script.src = `http://another.com/weather.json?callback=gotWeather`;
document.body.append(script);
another.com
은 클라이언트에서 필요한 날씨 데이터와 함께 gotWeather(...)
를 호출하는 스크립트를 동적 생성// 서버에서 전달하는 데이터
// 콜백함수에 감싸진 형태의 JSON 데이터
// 이 형식을 곧 JSONP라고 일컫음
gotWeather({
temperature: 26,
humidity: 78,
});
JSONP
는CORS
가 활성화 되기 이전 데이터 요청 방법으로 서버에서JSON
형식의 데이터를 전달할 때 콜백함수로 감싸서 전달하는 형식이다. 이는script
태그가 도메인 제약이 적용되지 않기에 외부 도메인에 요청을 보낼 수 있고 이에 대한 결과값을 저장하기 위해 고안된 방법이다.CORS
정책이 고안된 후에는 보안 상의 이유로 오늘날엔 거의 사용되지 않는 기술이기 때문에 더 상세하게 다루지는 않으려 한다.
이런 꼼수를 쓰면 보안 규칙을 위배하지 않으면서 양방향 데이터 전송이 가능했다. 양쪽에서 인지하고 동의한 상태라면 해킹으로 볼 수도 없다. 아직도 오래된 브라우저나 서비스에서는 이러한 방식을 사용해서 통신을 주고 받는 경우도 있다.
그러던 와중에 브라우저측 자바스크립트에 네트워크 관련 메서드가 추가되면서 CORS
정책 역시 추후에 만들어졌다. 초기에는 네트워크 메서드를 사용해서 크로스 오리진 요청이 불가했는데, 긴 논의 끝 이를 허용하기로 하되 대신 서버에서 명시적으로 이를 허가했다고 알려주는 특별한 헤더를 전달받은 경우에만 가능하도록 제약을 만들었고, 이를 CORS
정책이라 부른다.
크로스 오리진 요청은 크게 두 가지 종류로 구분된다.
safe request
)unsafe request
로 간주됨)안전한 요청의 경우엔 그 외의 요청 대비 만들기 간단하다. 다음 두 가지 조건을 모두 충족하는 경우 안전한 요청으로 분류한다.
safe method
) : GET
/POST
/HEAD
safe header
) : 다음 목록에 속하는 헤더Accept
: 요청 전송 시 기대하는 타입의 데이터Accept-Language
: 요청 전송 시 기대하는 언어Content-Language
: 사용자 자체의 언어를 뜻함. 요청이나 응답이 무슨 언어인지와는 관련 없음Content-Type
의 값이 application/x-www-form-urlencoded
/ multipart/form-data
/ text/plain
에 속하는 경우위 두 조건을 만족하지 않는 모든 요청은 안전하지 않은 요청(unsafe request
)으로 간주된다. 즉 PUT
메서드를 사용한다거나 헤더에 API-Key
가 명시되어 있다면 모두 안전하지 않은 요청으로 분류된다.
안전한 요청과 그렇지 않은 요청의 근본적인 차이는 특별한 방법을 사용하지 않고 form
이나 script
를 사용해 요청을 만들 수 있는지 아닌지르 구분한다. 이러한 태그를 사용해 요청을 전송하는 행위는 아주 오래전부터 존재했기에, 오래된 웹서버라도 안전한 요청은 당연히 처리할 수 있다.
만약 표준이 아닌 헤더가 들어있거나 안전하지 않은 메서드(PUT
, PATCH
, DELETE
등)를 사용한 요청은 안전한 요청이 될 수 없다. 왜냐하면 아주 오래 전에는 자바스크립트를사용해 이런 요청을 보내는 것이 불가능했기 때문이다. 따라서 오래된 서버 입장에서는 이러한 요청이 웹 페이지에서는 불가하기 때문에, 무언가 특별한 다른 곳에서 왔을거라고 생각하고 작업을 처리했다.
하지만 시간이 지나고 네트워크 관련 메서드가 지원되면서 개발자가 자바스크립트로 안전하지 않은 요청을 보낼 수 있게 되자, 브라우저는 안전하지 않은 요청을 서버에 전송하기 전에 preflight
요청을 먼저 전송하고, 서버가 크로스 오리진 요청을 받을 준비가 되어있는지 사전에 체크하는 방식을 사용하게 되었다.
이때 서버에서 크로스 오리진 요청은 허용하지 않는다는 정보를 담은 헤더가 응답으로 오게되면, 안전하지 않은 요청은 서버로 전송되지 않는다. 이것이 CORS
정책이 등장하기 까지의 배경과 역사이다. 이젠 CORS
정책을 준수하며 데이터를 주고 받기 위해 어떤 작업이 필요한지 살펴보도록 하자.
크로스 오리진 요청을 보낼 경우 브라우저는 항상 Origin
이라는 헤더를 요청에 추가한다. 만약 https://javascript.info/page
에서 https://anywhere.com/request
으로 요청을 보낸다고 가정해보자. 둘은 서로 다른 도메인이기 때문에 크로스 오리진 요청에 해당한다. 따라서 헤더는 다음과 같은 형태를 띈다.
GET /request
Host: anywhere.com
Origin: https://javascript.ino
...
위와 같이 Origin
헤더 정보에는 요청이 이뤄지는 페이지 경로(.../page
)가 아닌 오리진(도메인/프로토콜/포트) 정보만 담긴다.
서버는 요청 헤더에 있는 Origin
을 검사하고, 요청을 받아들이기로 합의된 상태라면 특별한 헤더인 Access-Control-Allow-Origin
을 응답에 추가해 반환한다. 해당 헤더에는 허가된 오리진에 대한 정보나, 모든 오리진에 대해 허용하는 경우 *
가 명시되어 있다. 해당 헤더에 허가된 오리진 또는 *
가 들어있다면 응답은 정상적으로 반환되고 그렇지 않은 경우에는 응답이 실패한다.
브라우저는 이 과정에서 중재 역할을 한다.
Origin
에 값이 제대로 설정, 전송되었는지 확인Access-Origin-Allow-Origin
이 있는지를 확인하고, 관련 오리진 정보가 명시되어 있는지 체크. 명시되어 있다면 자바스크립트를 사용해 응답에 정상접근이 가능하고, 아닌 경우 에러 발생서버에서 크로스 오리진 요청을 허용한 경우 preflight
요청에 대한 응답은 다음과 같은 형태를 띈다.
200 OK
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
크로스 오리진 요청이 이뤄진 경우, 자바스크립트는 기본적으로 안전한 응답 헤더로 분류되는 헤더에만 접속할 수 있다. 안전한 응답 헤더는 다음과 같다.
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
일부 HTTP 응답 헤더는
HTTP/1.0
프로토콜에서 사용되는 경우로 오늘날HTTP/3.0
이 표준으로 채택된 기점으로 더 이상 사용되지 않는 목록이 있을 수 있다.
앞서 fetch
메서드를 사용할 때 다운로드 진행 상황을 추적할 때 Content-Length
헤더를 이용하는 경우를 살펴보았다. Content-Length
는 응답 본문 크기 정보를 담고 있는 헤더로 안전한 응답 헤더 목록에 속하지 않는다. 때문에 이에 접근하는데 특별한 권한이 필요하기에, 만약 문제가 발생한다면 관련 권한 획득 이슈일 확률이 높다.
자바스크립트를 사용해 안전하지 않은 응답 헤더에 접근하려면 서버에서 Access-Control-Expose-Headers
라는 헤더를 보내주어야 한다. 해당 헤더에는 자바스크립트 접근을 허용하는 안전하지 않은 헤더 목록이 담겨있고, 여러 개의 헤더를 콤마로 구분해 명시할 수 있다.
200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length, API-Key
응답 헤더 Access-Control-Expose-Headers
목록에 각각 Content-Length
와 API-Key
가 명시되어 있기 때문에 자바스크립트로 해당 헤더에 대한 접근이 가능하다.
요즘엔 RESTFul API
라는 이름 하에 GET
, POST
메서드 외에도 PATCH
, PUT
, DELETE
와 같은 메서드를 용도에 맞게 사용해 요청을 전송할 수 있다. 그런데 해당 메서드들은 비교적 최근에 생긴 것으로, 과거에는 웹 페이지에서 GET
과 POST
메서드만 사용해 요청을 보낼 수 있었다. 때문에 오래된 웹 서버의 경우엔 아직 새로운 메서드를 다룰 수 없는 경우도 종종 있다. 때문에 이런 서버들은 GET/POST
이외의 요청이 들어오는 경우 브라우저에서 보낸 요청이 아니라고 판단하고 접근 권한을 따로 확인한다.
이런 혼란을 피하기 위해 브라우저는 안전하지 않은 요청이 이뤄지는 경우, 서버에 바로 요청을 보내지 않고 preflight
요청이라는 사전 요청을 서버에 미리 보내고 권한 여부를 체크한다. 이때 preflight
요청을 보낼 때 사용하는 메서드가 OPTIONS
이다. 해당 메서드는 서버에서 지원하는 메서드를 확인할 때 보내는 요청인데, 이를 통해 서버가 어떤 parameters
를 포함한 요청을 보내도 되는지에 대한 응답을 줄 수 있는지 확인할 수 있다.
preflight
요청은 OPTIONS
메서드와 함께 두 헤더가 함께 들어가며, 이때 본문은 비어있는 형태이다.
Access-Control-Request-Method
헤더 : 안전하지 않은 요청에서 사용하는 메서드 정보가 담겨있다.Access-Control-Request-Headers
헤더 : 안전하지 않은 요청에서 사용하는 헤더 목록이 담겨있다.안전하지 않은 요청을 허용하기로 협의했다면 서버는 본문이 비어있고 상태 코드가 200
인 응답을 다음과 같은 헤더와 함께 브라우저에 반환한다.
Access-Control-Allow-Origin
: *
(모든 오리진)이나 요청을 보낸 오리진Access-Control-Allow_Methods
: 허용된 메서드 정보Access-Control-Allow-Headers
: 허용된 헤더 목록Access-Control-Max-Age
: 퍼미션 체크 여부를 몇 초간 캐싱할 지 명시. 퍼미션 정보가 캐싱된 경우 브라우저는 일정 기간 동안 preflight
요청을 생략하고 안전하지 않은 요청을 전송 가능이 일련의 과정을 그림으로 나타내면 아래와 같다.
실제로 안전하지 않은 크로스 오리진 요청을 보내는 코드를 단계별로 나누어 살펴보자. 아래 예시에서는 PATCH
메서드를 사용했는데, 이는 주로 데이터를 갱신할 때 사용하는 메서드이다. 각 메서드에 대한 자세한 설명은 추후 다른 포스트에서 소개하겠다.
let response = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret',
}
});
일단 위 요청이 안전한 요청이 아닌 이유는 세 가지가 있다.
PATCH
메서드 사용Content-Type
이 application/json
에 해당API-Key
사용 1단계: preflight
요청
본 요청을 보내기 전에 브라우저는 자체적으로 다음과 같은 preflight
요청을 서버에 전송한다.
OPTIONS /scrvice.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type, API-Key
2단계: preflight
응답
서버는 상태코드 200
과 함께 다음과 같은 헤더를 담은 응답을 보낸다.
Access-Control-Allow-Methods: PATCH
Access-Control-Allow-Headers: Content-Type, API-Key
위 응답이 오지 않는다면 더 이상 서버로 다음 요청을 보낼 수가 없다. 때문에 본 요청에서는 에러가 발생할 것이다. 웹 애플리케이션의 규모가 커짐에 따라 미래에 PATCH
메서드외에 다른 메서드를 사용해 요청을 전송하는 경우가 생길 수 있다. 이를 대비하기 위해선 Access-Control-Allow-Methods
와 Access-Control-Allow-Headers
에 원하는 메서드와 헤더를 추가하면 된다.
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
위와 같이 서버에서 prefilght
응답이 오게 되면 브라우저는 PATCH
메서드가 허용되었는지 확인하고 관련 헤더 역시 허용되었는지 확인 후 본 요청을 서버에 전송하게 된다.
참고로 Access-Control-Max-Age
헤더가 응답으로 오는 경우 prefilght
허용 여부가 헤더와 함께 캐싱되기 때문에, 브라우저는 헤더 값에 명시한 초 동안 새로운 preflight
요청을 보내지 않는다. 이는 HTTP
통신 자체가 기본적으로 stateless
방식이기 때문에 이전 요청을 기억하지 않아 발생하는 이슈이다. 매번 새로운 preflight
요청을 보내는 것은 보안적으로 강력하지만, 자원을 사용하는 입장에서는 비효율적이므로 별도의 값을 캐싱해두어 해당 요청을 생략할 수 있다.
3단계: 실제요청
preflight
요청이 성공적으로 이루어진 후에야 브라우저는 본 요청을 서버로 전송한다. 이후 프로세스는 안전한 요청이 이루어질 때와 모두 동일하다. 본 요청은 크로스 오리진 요청이기 때문에 Origin
헤더가 붙는다.
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
4단계: 실제응답
서버에선 본 요청에 대한 응답으로 Access-Control-Allow-Origin
헤더를 반드시 붙여주어야 한다. 이는 prefilght
요청이 성공했더라도 항상 동반되어야 하는 작업이다. 앞서 말했다싶이 HTTP
통신은 stateless
하기 때문에 이전 정보를 기억하지 못하기 때문이다.
Access-Contorl-Allow-Origin: https://javascript.info
이 모든 과정이 끝나야 자바스크립트를 사용해 실제 응답에 접근할 수 있다.
preflight
요청은 무대 밖에서 일어나기 때문에 자바스크립트를 사용해 중간에 개입할 수 없는 영역이다. 따라서 자바스크립트는 본 요청에 대한 응답을 받아올 때에만 어떤 작업을 수행할 수 있다. 서버에서 크로스 오리진 요청을 허용하지 않는다면 에러가 발생한다.
위 일련의 과정은 순수 자바스크립트 메서드만 이용했을 때 서버에서 같이 처리해주어야 하는 작업이다. 오늘날
fetch
메서드 대신 조금 더 편의성과 기능적인 부분이 강화된axios
라이브러리를 사용하는 것처럼, 서버쪽에서도cors
와 같은 라이브러리를 사용해서 일일이 헤더를 설정하지 않고도CORS
관련 설정을 수행할 수 있다.
자바스크립트로 크로스 오리진 요청을 보내는 경우, 기본적으로 쿠키나 HTTP 인증 같은 자격 증명(credential
)이 함께 전송되지는 않는다. HTTP 요청의 경우 대개 쿠키가 함께 전송되는데, 자바스크립트를 사용해 만든 크로스 오리진 요청은 예외에 해당한다. 쿠키와 인증 정보가 무엇인지는 다음 챕터에서 자세히 다루도록 하자.
따라서 fetch('http://another.com')
과 같은 요청에는 anther.com
과 관련된 쿠키 정보는 함께 서버로 전송되지 않는다. 이러한 예외가 생긴 이유는 자격 증명과 함께 전송되는 요청은 보안과 매우 밀접한 관련을 맺고 있기 때문이다. 크로스 오리진 요청 시 자격 증명을 자동으로 보내게 된다면, 사용자의 동의 없이 자바스크립트를 사용해 민감한 정보에 접근할 수 있게 된다.
하지만 그럼에도 불구하고, 서버에서 이러한 자격 증명 정보를 받아야 할 때가 있다. 대표적으로 유저의 로그인 정보를 받아, 자동 로그인 등과 같은 기능을 구현하려는 경우가 있다. 이런 경우에 서버에서 이를 허용하고자 한다면, 자격 증명이 담긴 헤더를 명시적으로 허용하겠다는 별도의 세팅을 해주어야 한다. 만약 fetch
메서드에 자격 증명 정보를 함께 전송하고자 한다면, credentials: "include"
옵션을 추가하면 된다.
fetch('http://another.com', {
credentials: 'include',
});
이렇게 추가적인 옵션을 명시하면 fetch
로 서버에 요청을 보낼 때 antoher.com
에 대응하는 쿠키 정보 역시 같이 전송되게 된다. 자격 증명 정보가 담긴 요청을 서버에서 받아들이기로 동의했다면, 서버는 응답에 Access-Control-Allow-Origin
헤더와 함께 Access-Control-Allow-Credentials: true
헤더를 추가해서 보내게 된다.
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
자격 증명이 함께 전송되는 요청을 보낼 땐 Access-Control-Allow-Origin
에 *
을 쓸 수 없다. 정확한 오리진 정보가 명시되어야만 자격 증명이 정상적으로 오고 갈 수 있다. 이러한 제약이 있기 때문에 어떤 오리진에서 관련 요청이 왔는지에 대한 정보를 서버가 신뢰할 수 있기 때문이다.
자격 증명을 허용하는 옵션은 네크워크 메서드에 따라 조금씩 다를 수 있다. 하지만 보통
credential
이란 키워드가 포함되어 있고 그 목적은 모두 동일하다.
Origin
헤더는 조금 더 강력한 신뢰성을 가진 헤더 정보이다. 비슷한 정보를 가진 헤더로Referer
가 있는데, 해당 헤더는 보통Origin
보다 조금 더 자세한 정보를 담고 있다.Referer
는 주로 네트워크 요청을 처음 시작한 페이지의 전체 URL 정보를 담기 때문이다. 그렇지만Referer
는 여러 이유로 생략이 될 수 있는 헤더이다. 일단 명세서에 따르면 HTTP 헤더에서 선택 사항에 해당하는 값이고,fetch
메서드에서는 별도로Referer
를 막을 수 있는 옵션을 제공한다. 또한 보안 수준이 높은 프로토콜에서 낮은 프로토콜을 사용해 접근하는 경우(HTTPS ➡ HTTP
)엔Referer
가 없다. 이처럼Referer
는 신뢰할 수 없는 정보를 담을 수 있는 가능성이 있기 때문에 HTTP 헤더 명세서에는Origin
이 추가되었고, 이 값을 활용해 크로스 오리진 요청을 수행한다.
앞서 fetch
메서드에 대해 살펴보았다. 하지만 그 외에도 fetch
메서드에 명시할 수 있는 옵션은 여러가지가 있다. 해당 옵션들은 사실 잘 사용되지 않는 경우가 대부분이지만, 나중을 위해 지원하는 옵션들을 간단하게라도 훑고 넘어가보도록 하자.
fetch
메서드에서 사용할 수 있는 옵션에는 다음과 같은 것들이 있다
let response = fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'text/plain; charset=UTF-8'
},
body: undefined,
referrer: 'about:client',
referrerPolicy: 'no-referrer-when-downgrade',
mode: 'cors',
credentials: 'same-origin',
cache: 'default',
redirect: 'follow',
integrity: '',
keepalive: false,
signal: undefined,
window: window
});
앞서 method
, headers
, body
그리고 siganl
에 대해서는 충분히 살펴보았다. 그 외 각각의 옵션에 대해 하나씩 살펴보도록 하자.
해당 옵션은 fetch
메서드가 HTTP Referer
헤더를 설정하는 방법을 제어한다. 일반적으로 해당 헤더는 자동으로 설정 되며 요청이 처음 만들어진 페이지의 경로를 포함한다. 대부분의 경우 해당 정보는 중요하게 취급되지 않는다. 이를 대신해서 Origin
헤더에 더 신뢰할 수 있는 도메인 정보가 들어가기 때문이다. 보안을 위해서라면 해당 옵션을 사용하지 않는 것이 때때로 더 유용할 수도 있다.
재미있는 점은 HTTP 헤더의 이름은
Referer
이다. 원래 영단어도referrer
이 맞고fetch
메서드의 옵션도referrer
로 명시하고 있는데, 헤더가Referer
인 이유는 단순 오타로 이미 돌이키기엔 하위 호환성이 깨질 것을 우려하여 계속Referer
헤더로 유지하고 있다.
Referer
요청 헤더는 현재 요청된 페이지의 링크 이전 웹 페이지 주소를 포함하는 용도로 활용이 가능하다. 해당 헤더를 통해 사람들이 어디로부터 와서 방문 중인지 분석이 가능하다. referrer
옵션을 사용하면 현재 오리진 내에서 Referer
를 설정하거나 제거할 수 있다.
만약 Referer
를 설정하지 않으려면 빈 문자열을 보내면 된다.
fetch('/page', {
referrer: "" // no Referer header
});
현재 오리진 내에서 또 다른 경로를 지정해서 Referer
헤더를 설정할 수 있다.
fetch('/page', {
referrer: 'https://javascript.info/anotherpage'
});
referrerPolicy
옵션은 Referer
헤더를 위한 일반적인 규칙을 설정할 수 있다. 요청은 다음과 같이 3가지 타입으로 구분할 수 있다.
HTTPS
➡ HTTP
)정확한 Referer
값을 설정할 수 있는 referrer
옵션과 달리 referrerPolicy
옵션은 브라우저에게 각 요청 타입에 대한 일반적인 규칙을 알려준다. 규칙 유형은 다음과 같은 것들이 있다.
no-referrer-when-downgrade
: 대개 기본값이며 항상 완전한 Referer
이 전송된다. 다만 HTTPS
프로토콜에서 HTTP
프로토콜로의 전송은 예외이다.no-referrer
: Referer
를 어떠한 경우에도 전송하지 않는다.origin
: Referer
에 있는 오리진만 전송하고 전체 경로는 전송하지 않는다. 예를 들어 http://site.com/path
인 경우엔 http://site.com
만 전송된다.origin-when-cross-origin
: 동일 오리진일 경우 Referer
전체가 전송된다. 그러나 크로스 오리진 요청일 경우엔 오리진만 전송된다.same-origin
: 동일 오리진이라면 완전한 Referer
를 전송한다. 그러나 크로스 오리진 요청이라면 no-referrer
과 동일하다.strict-origin
: 오리진만 전송하고 만약 고수준 보안에서 저수준 보안으로의 요청이라면 Referer
자체를 전송하지 않는다.strict-origin-when-cross-origin
: 동일 오리진이라면 완전한 Referer
을 전송하지만 크로스 오리진 요청에서는 오직 오리진만 전송한다. 또한 고수준 보안에서 저수준 보안 프로토콜로의 요청이면 아무것도 전송하지 않는다.unsafe-url
: 항상 완전한 Referer
을 전송한다. 이는 HTTPS ➡ HTTP
요청일 때도 마찬가지이다.
referrerPolicy
의 기본값은 브라우저별로 조금씩 다르다. 또한 동일 브라우저여도 버전별로 다른 경우가 있다. 예를 들어 크롬의 경우 85버전 이후로 기본값은strict-origin-when-cross-origin
으로 변경되었다.
위 referrerPolicy
에 대한 값을 테이블로 나타내면 아래와 같다.
Value | To same Origin | To Anther Origin | HTTPS ➡ HTTP |
---|---|---|---|
no-referrer | - | - | - |
no-referrer-when-downgrade | full | full | - |
origin | origin | origin | origin |
origin-when-cross-origin | full | origin | origin |
same-origin | full | - | - |
strict-origin | origin | origin | - |
strict-origin-when-cross-origin | full | origin | - |
unsafe-url | full | full | full |
Referer
헤더에 너무 많은 정보가 들어있다면 보안상의 이유로 좋지 않다. 해당 정보를 이용해 어느 사이트에서 누가 방문한건지에 대한 중요 정보를 누군가 파악하고 이를 악용할 수 있기 때문이다. 따라서 보통 권고되는 값은 strict-origin-when-cross-origin
이다.
Referer
관련 정책은 fetch
메서드에만 한정된 것이 아니다. Referrer-Policy
HTTP 헤더를 사용하는 전체 페이지 또는 <a rel="norefferer">
과 같이 HTML 태그에도 적용되는 정책이다.
mode
옵션은 경우에 따라 크로스 오리진 요청을 방지하는 보호 기능 역할을 한다.
cors
: 기본값으로 크로스 오리진 요청을 항상 허용한다.same-origin
: 크로스 오리진 요청은 금지된다.no-cors
: 아주 단순한 크로스 오리진 요청만을 허용한다.해당 옵션은 서드 파티 라이브러리로부터 fetch
메서드에 명시할 URL을 가져오는 경우 유용할 수 있다.
credentials
옵션은 fetch
메서드가 쿠키나 HTTP 인증과 같은 자격 증명 정보를 요청에 포함해 전송할 수 있는지 없는지에 대한 여부를 결정한다.
same-origin
: 기본값으로 크로스 오리진 요청에는 자격 증명 정보를 전송하지 않는다.include
: 항상 자격 증명 정보를 같이 전송한다. 이때 서버로부터 Accept-Control-Allow-Credentials
헤더를 응답 받아야 한다.omit
: 어떠한 경우에도 보내지 않는다.기본적으로 fetch
요청은 표준 HTTP-캐싱
을 사용한다. 즉 일반적인 HTTP 요청과 마찬가지로 Expires
, Cache-Control
과 같은 헤더를 준수하며 If-Modified-Since
와 같은 값을 전송할 수 있다.
cahe
옵션을 사용하면 HTTP 캐시를 무시하거나 용도를 쓰임에 맞게끔 미세하게 조정할 수 있다.
default
: fetch
메서드는 표준 HTTP-캐싱
규칙과 헤더를 준수no-store
: HTTP 캐시를 완전히 무시. 헤더를 If-Modified-Since
, If-None-Match
, If-Unmodified-Since
, If-Match
, If-Range
와 같은 조건부 헤더로 설정했을때 기본값reload
: HTTP 캐시로 부터 결과를 가져오지 않지만 응답값으로 캐시를 갱신no-cache
: 만약 캐시된 응답이 있다면 조건부 요청을 아니라면 일반 요청 생성force-cache
: 최근 캐시된 정보가 아니더라도 HTTP 캐시로 부터 응답을 사용. 만약 캐시된 응답이 HTTP 캐시에 없다면 일반적인 HTTP 요청처럼 작동only-if-cached
: force-cache
와 동일하나 캐시된 응답이 없다면 에러 발생요청 이후 리다이렉트와 관련된 옵션을 설정할 수 있다.
manual
: 리다이렉트를 허용하지 않음error
: 리다이렉트 응답을 에러로 처리follow
: 리다이렉트 응답을 허용Integrity
옵션은 응답이 미리 알려진 체크섬과 일치하는지에 대한 여부를 확인할 수 있다. 여기서 체크섬은 SHA-256
, SHA-384
, SHA-512
와 같은 해시 알고리즘으로 브라우저별로 조금씩 다를 수 있다. 만약 fetch
메서드를 사용해 파일을 다운로드 할 때 SHA-256
알고리즘에 abcdef
와 같은 체크섬을 사용하는 것을 알고있다면 다음과 같이 일치여부를 확인해볼 수 있다.
fetch('http://site.com/file', {
integrity: 'sha256-abcdef'
});
일치하는 경우 정상적으로 요청이 전송되어 응답을 받아볼 수 있지만, 불일치의 경우엔 에러가 발생한다.
keepalive
옵션은 요청이 해당 요청을 생성한 웹 페이지보다 더 오래 생존할 수 있음을 명시한다.
예를 들어 현재 방문자를 통해 사용자 경험을 분석하고 개선하려고 한다. 이때 방문자가 해당 페이지를 떠나는 경우 우리는 마우스 클릭과 같은 통계 정보를 수집할 수 있을 것이다. 사용자가 페이지를 떠나면 window.onload
를 통해 이를 캐치하고 관련 정보를 서버에 전송해야 한다.
이때 일반적으로는 웹 페이지를 떠나는 경우 관련 네트워크 요청은 자동으로 모두 중단된다. 하지만 우리가 필요한 정보는 유저가 웹 페이지를 떠날 때 그 정보를 서버에 전송하는 것이므로 요청이 중단되지 않고 정상적으로 계속 수행되어야 한다. 이럴때 keepalive
옵션을 true
로 설정하면 백그라운드에서 계속 네트워크 요청이 돌아가도록 할 수 있다.
window.onunload = function() {
fetch('/analytics', {
method: 'POST',
body: "statistics",
keepalive: true
});
};
물론 여기에는 몇 가지 한계가 존재한다.
요청을 통해 전송 가능한 데이터 크기는 최대 64KB
이다. 때문에 사용자가 페이지를 떠날 때 한번에 관련 데이터를 보내려면 크기제한으로 요청이 실패할 수 있다. 그러한 이유로 사용자 데이터가 너무 커지지 않도록 중간중간 정기적으로 패킷 단위로 서버에 전송할 필요가 있다.
해당 옵션은 모든 keepalive
를 사용하는 네트워크 요청에 공동 적용된다. 즉 병렬적으로 네트워크 요청을 사용하는 경우, 해당 요청들이 보내는 데이터의 총합 역시 64KB
를 넘을 수 없다.
문서가 unload
되는 경우엔 당연히 요청에 대한 응답을 처리할 수 없다. 보통 통계 자료를 전송하는 경우엔 문제가 되지 않는 경우가 많다. 일반적으로 이에 대한 응답은 보통 빈 응답을 보내는 경우가 많기 때문이다.
자바스크립트는 URL
내장 클래스를 제공한다. 이를 이용해 보다 편리하게 URL을 만들고 파싱할 수 있다. URL
클래스 자체는 네트워크 메서드와 관련된 기능을 지원하지 않는다. 따라서 문자열을 사용해서 URL 경로를 작성하는 것과 기능적으로 아주 큰 차이가 있지는 않다. 그러나 URL
클래스를 사용하면 특정 상황에서 도움을 얻을 수 있다.
URL
생성자를 이용해 URL 객체를 생성하는 문법은 다음과 같다.
let url = new URL(url, [base]);
url
: 전체 URL 경로 또는 일부 경로 (일부 경로는 base
가 명시된 경우, 이를 기준으로 전체 경로가 생성)base
: 생략가능하며, 기준이 될 URL 경로를 지정 가능예를 들어 다음 두 코드는 서로 같은 URL 객체를 생성한다.
let url1 = new URL('https://javascript.info/profile/admin');
let url2 = new URL('/profile/admin', 'https://javascript.info');
또는 생성된 URL 객체를 베이스로 삼아 또 다른 URL 객체를 생성할 수 있다.
let url = new URL('https://javascript.info/profile/admin');
let newURL = new URL('tester', url);
console.log(newURL); // https://javascript.info/profile/tester
그 외 URL 객체가 지원하는 추가적은 프로퍼티가 있다.
href
: 전체 URL 경로로 url.toString()
과 동일protocol
: 통신 프로토콜로 :
까지 포함한 문자열host
: 포트넘버까지 포함한 호스트 주소 (포트가 명시되지 않은 경우 hostname
만 출력)pathname
: 추가 경로search
: 파라미터 문자열로 ?
으로 시작하는 범위 포함hash
: #
으로 시작하는 해시값 문자열...
만약 HTTP Authentication
정보가 있는 경우 user
와 password
프로퍼티를 사용할 수도 있다. (eg. http://login:password@site.com
)
let url = new URL('https://javascript.info/url');
console.log(url.protocol); // https:
console.log(url.host); // javascript.info
console.log(url.pathname); // /url
이렇게 만들어진 URL 객체는 네트워크 요청 메서드에 전달하는 문자열 경로대신 사용할 수 있다. 대부분 URL 문자열이 사용되는 모든 곳에서 URL 객체를 대신 사용할 수 있다. 이는 URL 객체가 알아서 전체 경로를 담은 문자열로 변환되기 때문이다.
URL
클래스를 이용하면 보다 편리하게 세부 항목별로 접근이 가능함을 알아보았다. 이 자체도 꽤나 좋은 기능이지만, 그렇다고 해서 문자열로 경로를 적는 방식에 비해 압도적으로 편리하다고 생각이 들지 않을 수 있다. 그러나 URL 객체를 이용해서 query
와 params
를 경로에 추가할 때 보다 편리한 변환 기능을 제공해준다.
검색 params
를 URL 경로에 지정하여 URL 객체를 만들고 싶다고 가정해보자. 예를 들면 https://google.com/search?query=Javascript
와 같은 URL을 만들고 싶다. 당연히 아래와 같이 전체 경로를 적어줄 수 있다
let url = new URL(`https://google.com/search?query=Javascript`);
이때 파라미터로 지정되는 글자에 스페이스가 있다거나, 아니면 비-라틴어 계열 특수문자가 포함되어 있는 경우엔 정상적으로 브라우저가 이를 이해할 수 없기 때문에 인코딩되어야 한다. 이때 url.searchParams
객체에서 지원하는 여러 메서드를 통해 변환 작업을 위임할 수 있다.
append(name, vlaue)
: name
- value
쌍으로 파라미터 추가 delete(name)
: name
을 가진 파라미터 제거get(name)
: name
에 해당하는 파라미터 읽기getAll(name)
name
을 가진 모든 파라미터 읽기 (?user=John&user=Peter
)has(name)
: name
에 해당하는 파라미터가 있는지 검사set(name, value)
: 파라미터를 name
- value
쌍으로 설정/교체 sort()
: name
에 따른 파라미터 정렬...
url.searchParams
는 Map
과 유사한 이터러블 객체이기 때문에 for...of
를 통해 순회도 가능하다.
let url = new URL('https://google.com/search');
// 특수문자 느낌표(!)와 공백 사용 -> 아래처럼 변환
url.searchParams.set('q', 'test me!');
console.log(url);
// https://google.com/search?q=test+me%21
// 특수문자 콜론(:) 사용 -> 아래처럼 변환
url.searchParams.set('tbs', 'qdr:y');
console.log(url);
// https://google.com/search?q=test+me%21&tbs=qdr%3Ay
for(let [name, value] of url.searchParams) {
console.log(`${name}=${value}`);
// q=test me!, tbs=qdr:y
}
위에서 살펴본 것과 같이 URL
객체에서 지원하는 프로퍼티를 통해 경로 인코딩을 자동으로 적용할 수 있다. RFC3986
명세에는 URL에서 허용하는 문자와 허용하지 않는 문자 목록이 명시되어 있다. 허용되지 않는 문자는 반드시 인코딩이 수행되어야 한다. 이를 일일이 변환하려고 한다면 매우 번거로운 작업이 될 것이다. url.searchParams
객체를 이용하면 이를 자동으로 변환할 수 있기 때문에 매우 편리하다.
// 키릴 문자를 사용하는 경우 자동 인코딩 적용
let url = new URL('https://ru.wikipedia.org/wiki/Тест');
url.searchParams.set('key', 'ъ');
alert(url); //https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82?key=%D1%8A
이때 URL
객체가 등장하기 전에는 모든 URL 경로는 문자열로 표현했다. 그런데 브라우저가 인식할 수 있는 URL 경로는 한정되어 있고, 이를 벗어난 경우 인코딩이 일어나야 하는데 이는 사람이 일일이 적용하기 매우 힘들다. 오늘날엔 URL
객체를 통해 이 작업을 쉽사리 해결할 수 있지만 과거에는 다음과 같은 별도의 메서드를 사용했다.
encodeURI
: URL 전체를 인코딩decodeURI
: 인코딩 된 URL을 다시 디코딩encodeURIComponent
: search parameters
, hash
와 같은 URL 일부만 인코딩decodeURIComponent
: 일부 인코딩 된 URL을 다시 디코딩이때 encodeURI
와 encodeURIComponent
사이에는 어떤 차이가 있을까?
encodeURI
는 명세에 허용되지 않은 문자에 대해서만 인코딩을 적용한다. 그러나 encodeURIComponent
는 허용되지 않은 문자는 물론 #
, $
, &
, +
, ,
, :
와 같은 특수기호 역시 인코딩을 진행한다는 차이가 있다.
let url1 = encodeURI('http://site.com/привет');
alert(url1);
// http://site.com/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82
let url2 = encodeURIComponent('http://site.com/привет')
alert(url2);
// http%3A%2F%2Fsite.com%2F%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82
encodeURIComponent
은 만약 파라미터에 값으로 지정되는 값이 특수문자를 포함하고 있는 경우에 유용하다. 예를 들어 ?music=Rock&Roll
처럼 파라미터가 지정된 경우 Rock&Roll
자체가 하나의 값이 되어야 한다. 그러나 &
기호는 search parameter
끼리 구분하는 기호로 사용되기 때문에 단순히 금지된 문자만 인코딩하여 진행한다면 music=Rock
으로 인식될 수 있다. 때문에 &
까지 인코딩하여 하나의 덩어리로 만들면 정확하게 값을 추출할 수 있다.
다만 해당 메서드들은 예전에 만들어졌고 최신 명세를 반영하지 못하는 경우가 있으므로 가급적 URL
객체를 사용해 인코딩을 진행하는 것을 추천한다. 예를 들어 IPv6
버전의 경로는 해당 메서드들이 만들어 졌을때 명세에 추가되지 않았기 때문에 제대로 된 인코딩을 진행하지 못한다.
XMLHttpRequest
는 앞에서 살짝 언급이 된 바 있다. fetch
메서드가 ES6
에서 소개되기 전에는 XMLHttpRequest
객체를 이용해서 AJAX
통신을 수행했다.
XML
이라는 이름을 가지고 있지만, XML
타입의 데이터 뿐만 아니라 어떤 데이터도 전송이 가능하다. 즉 XMLHttpRequest
을 통해서도 파일을 다운로드/업로드 할 수 있으며 관련 진행 상황 역시 추적할 수 있다.
오늘날엔 fetch
메서드로 인해 더 이상 XMLHttpRequest
는 잘 사용하지 않는다. 만약 XMLHttpRequest
를 사용한다면 옛날 소스코드일 확률이 높다. 그럼에도 불구하고 모던 웹 개발에서 XMLHttpRequest
를 사용한다면 크게 다음 3가지 이유를 꼽을 수 있다.
fetch
를 지원하지 않기 때문fetch
가 지원하지 않는 아주 특수한 기능을 사용해야 하는 경우XMLHttpRequest
는 이제 더 이상 잘 사용되지 않지만 간단하게 어떤 식으로 사용했는지를 살펴보고, 3번 관점에서 어떤 특수 기능이 있는지를 알아보도록 하자.
XMLHttpRequest
는 fetch
메서드와는 달리 사용방법이 조금 복잡하다. 일단 XMLHttpRequest
는 비동기와 동기 방식을 모두 지원하는데, 먼저 비동기 방식부터 알아보도록 하자.
XMLHttpRequest
객체를 생성해야 한다.let xhr = new XMLHttpRequest();
XMLHttpRequest
객체를 생성한 후 초기화를 수행한다. open()
메서드를 사용해서 관련 옵션을 설정한다. 이름은 open
이지만 이는 아직 요청을 보내는 단계가 아니라는 점에 주의해야 한다.xhr.open(method, URL, [async, user, password]);
method
: HTTP 메소드로 보통 GET/POST
URL
: URL 경로로 URL
객체도 사용 가능async
: false
인 경우 동기, 기본값은 true
user, password
: HTTP auth
가 필요한 경우 로그인 관련 정보 (잘 사용하지 않음)send()
메서드를 이용한다. POST
메서드인 경우 별도로 body
를 지정해 전송할 수 있다.xhr.send([body]);
xhr
이벤트를 사용해 응답을 수신한다. 다음과 같이 3가지의 이벤트를 자주 사용한다.load
: 요청이 완료되어 응답이 완전히 다운로드 된 경우 발생 (400
이나 500
과 같은 통신 에러가 나도 발생)error
: 요청이 정상적으로 처리되지 않은 경우 발생. 네트워크 에러나 유효하지 않은 URL인 경우progress
: 응답이 다운로드 되는 와중에 주기적으로 발생xhr.onload = function() {
console.log(`Loaded: ${xhr.status} ${xhr.response}`);
}
xhr.onerror = function() {
console.log(`Network error`);
}
xhr.onprogress = function(evnet) {
// event.loaded : 얼마나 다운로드 받았는지 (실시간 변경)
// event.lengthComputable : 서버에서 Content-Length 헤더를 보낸 경우 true
// event.total : lengthComputable이 true인 경우 응답의 총 크기
console.log(`Received ${event.loaded} of ${event.total}`);
}
이처럼 이벤트 핸들러 방식으로 네트워크 요청을 다루고 있는 것을 볼 수 있다. fetch
메서드에 비해 관련 처리 작업이 조금 불편한 것을 체감할 수 있다. 다음은 실제로 어떤 경로에 요청을 보내고 이에 대한 응답을 받아 처리하는 과정을 살펴보자.
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/load');
xhr.send();
xhr.onload = function() {
if(xhr.status !== 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
// e.g. 404: Not Found
} else {
alert(`Done, got ${xhr.response.length} bytes`);
}
}
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`);
// no Content-Length
}
}
xhr.onerror = function() {
alert("Request failed");
}
이때 서버로부터 응답을 받으면 xhr
객체에서 다음과 같은 프로퍼티를 사용해 접근할 수 있다.
status
: HTTP 통신 상태코드. 특이하게 0
을 출력할 때가 있는데 이는 HTTP 통신 관련 에러가 아닌 경우로 중간에 중단된 경우 등을 의미
statusText
: HTTP 통신 상태 메시지로 코드가 200
인 경우 OK
, 404
인 경우 Not Found
, 403
인 경우 Forbidden
등의 문자열
response
: 서버로부터 받은 응답 (오래된 스크립트의 경우 responseText
를 사용하기도 함)
또 별도로 타임아웃 프로퍼티를 설정해 줄 수 있다. 주어진 시간 내에 요청이 성공하지 못하면 timeout
이벤트가 발생한다.
xhr.timeout = 10000; // 10초 (10000ms)
xhr.responseType
프로퍼티를 사용해 응답 포맷을 설정할 수 있다. 이는 fetch
메서드에서 Content-Type
을 지정하는 것과 동일하다.
""
: 기본값으로 문자열 형식"text"
: 문자열 형식"arraybuffer"
: ArrayBuffer
형식"blob"
: Blob
형식"document"
: XML document
형식"json"
: JSON
형식이고 자동으로 파싱됨let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/json');
xhr.responseType = 'json';
xhr.send();
xhr.onload = function () {
let responseObj = xhr.response;
alert(responseObj.message);
}
XMLHttpRequest
객체는 진행 상황에 따라 상태가 변하는데, 이 상태값은 xhr.readyState
프로퍼티로 접근할 수 있다. 명세에 따른 모든 상태값은 다음과 같다.
UNSENT = 0; // 초기 상태값
OPENED = 1; // open 호출 시 상태값
HEADERS_RECEIVED = 2; // 응답헤더를 받았을 때 상태값
LOADING = 3; // 응답을 다운로드 하는 과정의 상태값
DONE = 4; // 요청이 완료된 경우 상태값
XMLHttpRequest
객체를 통한 네트워크 요청은 따라서 0 ➡ 1 ➡ 2 ➡ 3 ➡ 3 ➡ ... ➡ 3 ➡ 4
순서로 진행된다. 이는 readystatechange
이벤트로 추적할 수 있다.
xhr.onreadystatechange = function () {
if (xhr.readyState === 3) {
// loading
}
if (xhr.readyState === 4) {
// request finished
}
};
XMLHttpRequest
객체를 통해 네트워크 요청을 다룰 때 readystatechange
이벤트 리스너를 통해 관리하는 것은 매우 오래된 코드에서 볼 수 있다. 이는 이전에는 load/error/progress
와 같은 이벤트가 고안되지 않았기 때문에 이 같은 방식을 사용해 처리했던 것인데, 오늘날 XMLHttpRequest
객체를 사용한다면 load/error/progress
이벤트를 통해 관련 처리를 하는 것이 더 좋다.
fetch
메서드의 경우엔 별도로 AbortController
객체를 통해 요청을 중단할 수 있었지만, XMLHttpRequest
의 경우엔 자체 메서드를 지원한다.
xhr.abort();
해당 메서드를 호출하면 abort
이벤트를 발생시킴과 동시에 xhr.status
의 값은 0
이 된다.
open
메서드에서 async
옵션을 false
로 지정하면 동기식 네트워크 요청을 수행한다. 동기식으로 요청을 하게 되면 요청을 보내고 응답을 받아올 때까지 모든 자바스크립트 실행이 블록킹된다.
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // instead of onerror
alert("Request failed");
}
만약 응답 본문의 크기가 크다면 이를 완전히 다운로드하기 까지 브라우저의 동작이 멈출 수도 있다. 예를 들어 몇몇 브라우저는 스크롤 기능이나 클릭 기능이 마비될 수 있다. 때문에 사실상 동기 방식 네트워크 요청은 오늘날 거의 사용하지 않는 방식이다.
XMLHttpRequest
객체는 커스텀 헤더를 전송하거나 응답으로부터 헤더 정보를 읽는 것을 모두 지원한다. HTTP 헤더를 위한 메서드는 다음과 같이 3가지가 있다.
setRequestHeader(name, value)
요청 헤더를 주어진 name
과 상응하는 value
로 설정
xhr.setRequestHeader('Content-Type', 'application/json');
이때 오직 브라우저가 관리하는 헤더는 설정할 수 없다. 또한 setRequestHeader
메서드로 설정된 헤더는 다시 되돌릴 수 없다. 헤더가 한 번 설정되고 나면 이를 지우거나 덮어씌우는 것이 불가하다.
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
// 이 경우 `X-Auth`는 456으로 덮어씌워 지는 것이 아니라
// X-Auth: 123, 456 으로 계속 값이 추가된다.
getResponseHeader(name)
name
에 해당하는 응답 헤더를 반환
xhr.getResponseHeader('Content-Type');
getAllResponseHeaders()
Set-Cookie
와 Set-Cookie2
를 제외한 모든 응답 헤더를 반환
xhr.getAllResponseHeaders();
// Cache-Control: max-age=31536000
// Content-Length: 4260
// Content-Type: image/png
// Date: Sat, 08 Sep 2012 16:53:16 GMT
이때 반환되는 헤더들은 모두 "\r\n"
이스케이프 문자를 통해 구분된다. 따라서 이를 이용해 다음과 같이 키-밸류 쌍으로 헤더를 구조화할 수 있다.
let headers = xhr.getAllResponseHeaders()
.split('\r\n')
.reduce((result, current) => {
let [name, value] = current.split(':');
result[name] = value;
return result;
}, {});
console.log(headers['Content-Type']);
fetch
메서드를 통해 POST
요청 때 FormData
를 보낼 수 있었던 것 처럼 XMLHttpRequest
객체를 통해서도 FormData
전송이 가능하다.
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
let formData = new FormData(document.forms.person);
formData.append("middle", "Lee");
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
xhr.onload = () => alert(xhr.response);
</script>
폼 전송을 통해 요청을 보내는 경우엔 multipart/form-data
타입으로 인코딩이 이루어진다.
FormData
형식 외에도 fetch
에서 보낼 수 있었던 대부분의 데이터 형식 역시 모두 요청이 가능하다. JSON
형식의 데이터를 전송하는 경우는 다음과 같다.
let xhr = new XMLHttpRequest();
let json = JSON.stringify({
name: "John",
surname: "Smith"
});
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
fetch
메서드의 경우 다운로드 절차를 추적하는 것은 가능했지만, 업로드의 경우는 추적할 수 없는 한계가 있었다. 이때 XMLHttpRequest
에서는 업로드를 추적하는 것이 가능하다고 했었는데 해당 기능을 알아보자.
POST
메서드로 서버에 요청을 보내는 경우엔 어떤 데이터를 서버에 업로드 하는 소요가 발생하고 만약 이를 추적하고 싶다면 fetch
대신 XMLHttpRequest
객체를 이용해야 한다. 위에서 살펴본 xhr.onprogress
이벤트 핸들러도 마찬가지로 오직 다운로드 관련 진행 상황만 추적이 가능하다. 업로드 절차를 추적하기 위해서는 별도의 객체를 사용해야 한다.
XMLHttpRequest
는 따로 uplaod
객체 프로퍼티를 지원한다. 해당 객체는 다시 여러 이벤트를 지원하는데 이를 사용해서 업로드 상황을 추적할 수 있다.
xhr.upload
는 xhr
에서 발생하는 이벤트와 유사한 이벤트를 가지고 있다.
loadStart
: 업로드 시작 시 발생progress
: 업로드가 진행되고 있는 도중 주기적으로 발생abort
: 업로드가 중단되면 발생error
: HTTP 통신 외적인 오류 시 발생load
: 업로드가 성공적으로 완료되었을 시 발생timeout
: 설정한 업로드 시간이 초과되었을 시 발생loadend
: 업로드가 성공이든 에러이든 상관없이 종료되면 발생xhr.uplaod.onprogress = function(event) {
alert(`Uploaded: ${event.loaded} of ${event.total} bytes`);
}
xhr.upload.onload = function() {
alert(`Upload finished successfully`);
}
xhr.upload.onerror = function() {
alert(`Error during the upload: ${xhr.status}`);
}
역시 실제 경로에 요청을 보내고, 이에 대한 업로드 과정을 추적하는 코드를 살펴보자.
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
xhr.onloadend = function() {
if (xhr.status == 200) {
console.log("success");
} else {
console.log("error " + this.status);
}
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
XMLHttpRequest
은 fetch
이전에 만들어진 네트워크 관련 객체이지만 크로스 오리진 이슈는 그보다 훨씬 이전에 있었기 때문에 동일하게 크로스 오리진 요청이 가능하다. 관련 정책은 모두 fetch
에서 다룬 내용과 동일하다.
XMLHttpRequest
역시 기본적으로는 자격 증명 정보를 전송하지 않기 때문에, 해당 정보를 같이 서버에 전송하기 위해서는 xhr.withCredentials
를 true
로 설정해주어야 한다.
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');
...
error
, abort
, timeout
, load
이벤트들은 서로 상호 배타적이다. 때문에 한 번에 하나의 이벤트만 발생할 수 있다.