네트워크 요청과 CORS

345·2023년 11월 12일

자바스크립트로 서버에 네트워크 요청을 보내고, 새로운 정보를 받아올 수 있습니다.
서버에 요청한 정보를 비동기적으로 받아오는 방법에 대해 알아봅시다.

🔔 fetch 로 네트워크 요청하기

fetch() 는 대부분의 모던 브라우저가 지원하는 전역 메서드로,
URL 주소에 HTTP 요청을 보내고 응답을 프라미스로 받아옵니다.

let promise = fetch(url, [options])
  • 반환값: 프라미스
  • url: 요청을 보낼 URL
  • options: 요청에 적용할 설정을 담은 객체, 아무것도 없으면 GET 메서드로 요청
    • method: GET 이나 POST 등의 요청 메서드
    • headers: 요청 헤더
    • body: 요청 본문

응답 사용하기

프라미스는 바로 반환되지만, 요청에 대한 응답이 왔다는 뜻은 아닙니다.
응답은 두 단계를 거쳐 진행됩니다.

let response = await fetch(url, options); // 응답 헤더와 함께 이행됨
let result = await response.json(); // json 본문을 읽음
  1. 요청 보내고 성공 여부 확인
  2. 받아온 응답을 읽어오기

  1. fetch 가 URL 에 네트워크 요청을 보내고 프라미스를 반환

fetch 가 반환하는 프라미스는 요청에 대한 Response 로 이행하는데,
서버에서 응답 헤더를 받으면 이행 상태가 됩니다.

응답에 오류가 있어도 무조건 이행되므로, 이행된 프라미스에서 응답 헤더의 코드를 보고
요청이 성공했는지 실패했는지 확인해야 합니다.

다음과 같은 프로퍼티로 HTTP 상태를 확인할 수 있습니다.

  • status: HTTP 상태 코드 확인
  • ok: 불린 값, 상태코드가 200번대일 경우 true(성공)
let response = await fetch(url);

if (response.ok) { // HTTP 상태 코드가 200~299일 경우
  // 응답 본문을 받음
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

  1. 프라미스에서 추가 메서드를 호출하여 응답 본문 받기

response 에서 메서드를 사용하여 응답 본문을 읽어옵니다.
사용하는 메서드는 다음과 같습니다.

  • text(): 응답을 텍스트로 반환
  • json(): 응답을 JSON 형태로 파싱
    • 자바스크립트 객체로 이행됨
  • formData(): 응답을 FormData 객체로 반환
  • blob(): 응답을 Blob 형태로 반환
    +arrayBuffer(): 응답을 ArrayBuffer 형태로 반환

메서드들은 프라미스를 반환하는데,
Response 스트림에서 본문을 파싱한 결과로 이행됩니다.
파싱이 완료되면 프라미스는 이행 상태가 됩니다.

이때, 본문을 메서드로 읽어왔다면 본문은 이미 소비되기 때문에,
fetch 로 얻어온 응답을 여러 번 소비하는 건(한 응답에 여러 번 메서드를 적용하여 여러 형태로 얻어오는건) 불가능합니다.

let response = await fetch(url);

let commits = await response.json(); // 응답 본문을 읽고 JSON 형태로 파싱

alert(commits[0].author.login);

응답 헤더

응답 헤더는 response.headers키-값 으로 저장됩니다.
이는 유사 맵으로, 맵에서 지원하는 메서드를 비슷하게 지원합니다.

let response = await fetch(url);

// 헤더 일부를 추출
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// 헤더 전체를 순회
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

요청 헤더

fetch() 에 headers 옵션을 사용하면 요청보낼 때 쓸 헤더를 설정 가능합니다.

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

이렇게... headers 에 헤더 정보가 담긴 객체를 넘겨줍니다.
이 방식으로 설정 불가능한 헤더도 존재합니다.

  • Proxy- 나 Sec- 로 시작하는 이름의 헤더
  • Date
  • Cookie
  • Accept-Charset
  • Host
  • Origin
  • Connection
  • 그 외...

이는 안전성을 위해서입니다.


POST 요청

fetch() 로 요청시 메서드를 따로 지정해주지 않으면
GET 메서드로 요청을 보내게 됩니다.

만약 그 외의 메서드로 요청을 보내려면 추가 옵션을 지정합니다.

  • method: HTTP 메서드, (ex. POST)
  • body: 요청 본문에 담아 서버에 보낼 데이터, 다음과 같은 형식 중 하나로 보내야 함
    • 문자열(ex. JSON)
    • FormData 객체 (form/multipart 로 전송)
    • Blob 이나 BufferSource: 바이너리 데이터 전송
    • URLSearchParams
  • headers 에서 Content-Type 설정: body 에 담을 데이터 형식을 명시함
    • 본문이 문자열이면 Content-Type 헤더가 text/plain;charset=UTF-8 로 기본 설정됨
    • JSON 을 전송하면, Content-Type 을 application/json 으로 설정해줘야 함

다음과 같이 사용하여 POST 요청으로, body 에 데이터를 담아 보낼 수 있습니다.

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

이런식으로 사용합니다.


이미지 전송하기

Blob 이나 BufferSource 객체를 이용하여 바이너리 데이터를 전송할 수 있습니다.

async function submit() {
  let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
  let response = await fetch('/article/fetch/post/image', {
    method: 'POST',
    body: blob
  });

  // 전송이 잘 되었다는 응답이 오고 이미지 사이즈가 얼럿창에 출력됩니다.
  let result = await response.json();
  alert(result.message);
}

위처럼... blob 객체를 만들어
body 에 전달하고 있습니다.

Blob 객체는 내장 타입을 가지므로 Content-Type 을 명시적으로 설정해주지 않아도 됩니다.
toBlob 에 의해 image/png 가 자동으로 설정됩니다.


fetch 중단하기

fetch() 로 보낸 요청을 중간에 중단할 수 있습니다.
이를 위해 빌트인 객체인 AbortController 를 사용합니다.


AbortController

AbortController 는 fetch 뿐만 아니라 다른 비동기 작업 중단에도 사용 가능합니다.
컨트롤러는 다음과 같이 구성됩니다.

let controller = new AbortController();
  • abort() 메서드
  • signal 프로퍼티: 이벤트 리스너 등록 가능

동작은 다음과 같이 이뤄집니다.

  1. controller.signalabort 이벤트 리스너를 등록
  2. abort() 메서드를 호출하여 이벤트 발생

abort() 메서드를 호출하면 controller.signalabort 라는 이벤트를 방출하고,
controller.signal.aborted 프로퍼티가 true 가 됩니다.

간단하게 보면 abort 라는 이벤트를 감지하는 대상과,
그 이벤트를 처리하는 메서드로 이루어져 있습니다.


fetch 에 AbortController 적용하기

AbortController 는 그냥 보면 별 기능이 없어 보이는데,
fetch 의 옵션에 등록해주면 abort 를 탐지하여 에러를 만듭니다.

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});
  • 옵션의 signalcontroller.signal 을 등록

이렇게 signal 을 설정해주면, controller 가 abort() 메서드를 호출했을 때
signal 로부터 abort 이벤트를 받아 요청을 중단합니다.

fetch 가 중단 되면, 프라미스는 AbortError 라는 에러로 reject 됩니다.

// 1초 후 abort 
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('/article/fetch-abort/demo/hang', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') { // 중단 핸들링
    alert("Aborted!");
  } else {
    throw err;
  }
}

✅ CORS

CORS 는 Cross-Origin Resource Sharing 의 줄임말로,
접속 사이트와 origin 이 다른 url 에 요청을 보낼 경우에
특수 헤더를 추가하도록 하는 정책입니다.

만약 origin 이 다른데 특수 헤더가 없다면 요청은 실패합니다.

origin 이란 ❓

origin 은 도메인(hostname), 프로토콜(scheme), 포트 의 세 가지로 결정되는 데요,
이 셋 중 어느 하나가 다르면 서로 다른 origin 을 가졌다고 합니다.

  • http 와 https (프로토콜이 다름)
  • http://example.comhttp://myapp.com (도메인이 다름)
  • http://example.com:80http://example.com:8080 (포트가 다름)

이렇게는 모두 다른 origin 을 가집니다.

그러니 만약 A 사이트에서 B 사이트로 요청을 보내려면,
CORS 를 따라야 요청이 성공하겠죠?


안전한 요청

크로스 오리진 요청은 안전한 요청과 그 외의 요청으로 구분되는데요,
안전한 요청은 다음 두 조건을 모두 충족하는 요청입니다,

  1. 안전한 메서드 사용: GET 이나 POST, HEAD 등
  2. 안전한 헤더 사용

안전한 헤더 목록은 다음과 같습니다.

  • Accept
  • Accept-Language
  • Content-Language
  • 값이 application/x-www-form-urlencoded 이나 multipart/form-data, text/plain 인 Content-Type

이 두 조건을 충족하지 않으면 안전하지 않은 요청이 됩니다.


요청 보내기

크로스 오리진 요청을 보내면 브라우저가 항상 Origin 이라는 헤더를 요청에 추가합니다.
Origin 헤더에는 요청한 페이지의 오리진(도메인, 프로토콜, 포트) 정보가 담기게 됩니다.

  • https://javascript.info/page 에서 https://anywhere.com/request 에 요청을 보낼 때
GET /request
Host: anywhere.com
Origin: https://javascript.info
...

요청을 받은 서버는 요청 헤더의 Origin 을 검사합니다.
그리고 요청에 동의하면 응답에 특별한 헤더 Access-Control-Allow-Origin 을 추가합니다.
이 헤더엔 허가된 오리진에 대한 정보나 * 이 명시됩니다.
만약 오리진 정보나 * 이 들어가지 않으면 응답이 실패하게 됩니다.

브라우저는 응답하는 서버와 요청 측 사이에서 중계 역할을 하며,
다음과 같은 일을 수행합니다.

  1. 크로스 오리진 요청 시 Origin 확인
  2. 서버에서 받은 응답에 Access-Control-Allow-Origin 를 확인하여 서버가 크로스 오리진 요청을 허용했는지 확인

응답 받기

크로스 오리진 요청이 이뤄지면 자바스크립트는 안전하다고 분류되는 응답 헤더에만 접근 가능합니다.

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

이 외의 헤더에 접근하면 에러가 발생합니다.
하지만 서버에서 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

위처럼... 서버가 허용한 헤더는 접근 가능합니다.


안전하지 않은 요청

안전하지 않은 요청을 보낸 경우, 요청은 바로 전달 되지 않습니다.
대신 브라우저가 preflight 라는 사전 요청을 서버에 보내 권한을 확인합니다.

  • 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 과정 생략 가능

이렇게 preflight 요청에 대한 응답으로 권한을 확인받는 과정을 거친 후에야 본래의 요청을 보냅니다.
이때, preflight 요청이 성공했더라도 본 요청에 대한 응답에는 Access-Control-Allow-Origin 헤더를 꼭 붙여줘야 합니다.

이렇게 사전요청-응답, 본 요청-응답 과정이 끝나야 실제 응답을 읽을 수 있습니다.


🎟️ 자격 증명

자바스크립트로 크로스 오리진 요청을 보내면 HTTP 인증이나 쿠키와 같은
자격 증명을 요청과 함께 전송하지 않습니다.

사용자 동의 없이 인증 정보를 전송하는 경우를 방지하기 위함입니다.
만약 쿠키 등 자격 증명을 함께 전송하고 싶다면, 명시적으로 허용해줘야 합니다.


Request.credentials

요청에서 credentials 는 크로스 오리진 요청의 경우
자격 증명 정보를 함께 전달할 지 여부를 지정합니다.
다음과 같은 값을 사용 가능합니다.

  • omit: 절대 쿠키를 전송하거나 받지 않음
  • same-origin: 기본값. URL 이 호출과 동일한 오리진이라면, 자격 증명을 전송
  • include: 크로스 오리진 요청이어도 항상 자격 증명을 전송

fetch 의 credentials

  • fetch 에서 credentials: "include" 옵션 추가
fetch('http://another.com', {
  credentials: "include"
});

이렇게 하면 URL 에 대한 인증 쿠키가 요청과 함께 전송됩니다.

자격 증명 정보가 담긴 요청을 서버가 받아들인다면,
응답에 다음과 같이 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 에는 * 을 쓸 수 없고, 정확한 오리진 정보를 명시해야 합니다.


HTMLElement 의 credentials

  • element(audio, img, link, script, video 등) 의 crossorigin 속성

HTML 요소에 crossorigin 속성을 사용하여 CORS 요청 처리 방식을 명시합니다.
crossorigin 속성은 다음과 같은 값을 가질 수 있습니다.

  • anonymous: 기본값, credentials 가 same-origin 으로 지정
  • use-credentials: credentials 가 include 로 지정

즉, 다음처럼 anonymous 로 지정하거나 빈 값을 할당하면 쿠키 등으로 자격 증명을 교환하지 않습니다.

<script
  src="https://example.com/example-framework.js"
  crossorigin></script>
profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

0개의 댓글