현대 웹 사이트와 응용 프로그램에서 매우 일반적인 작업은 전체 새 페이지를 로드할 필요 없이 변경하고픈 부분만 서버에서 받아와 업데이트 하는 것이다.
웹 페이지는 HTML문서와 스타일시트, 스크립트, 이미지파일과 같은 다양한 파일로 구성된다.
웹에서 페이지 로드의 기본 모델은 브라우저가 페이지를 표시하는 데 필요한 파일에 대한 하나 이상의 HTTP 요청을 서버에 요청하고 서버가 요청된 파일로 응답하는 것이다.
다른 페이지를 방문할 경우 브라우저는 새 파일을 요청하고 서버는 이 파일로 응답한다.
이 모델은 많은 사이트에서 잘 작동한다.
그러나 도서관 웹사이트와 같은 데이터 중심적인 웹 사이트를 생각해보자.
특정 장르의 책을 검색하거나 이전에 빌린 책을 기반으로 원하는 책을 추천해 줄 수 있다.
즉, 데이터베이스를 기반으로 사용자 인터페이스가 달라진다.
이 경우, 책이 변경될 때마다 페이지 전체를 업데이트 해야한다.
책이 아닌 나머지 부분은 내용이 그대로임에도 불구하고 말이다.
이는 비효율적이며 열악한 사용자 환경을 초래할 수 있다.
그래서 많은 웹사이트들은 전통적인 모델 대신 자바스크립트 API를 사용하여 서버에 데이터를 요청하고 페이지 로드 없이 페이지 내용을 업데이트 한다.
따라서 사용자가 새 책을 검색할 때 브라우저는 페이지를 업데이트하는 데 필요한 책 데이터만 요청한다.
여기서 메인 API는 Fetch API
이다.
Fetch API
를 사용해 서버에 HTTP 요청을 할 수 있다.
초기에 이 기술은 XML 데이터를 요청하는 경향이 있었기 때문에 Asynchronous JavaScript And XML (Ajax) 로 불렸다.
요즘에는 XML 보다 JSON을 요청을 더 많이 하지만 용어는 Ajax 그대로 사용된다.
Fetch API의 몇 가지 예를 보자.
다음의 파일들이 있다고 가정하자.
실제로는 데이터베이스에 데이터를 요청하겠지만 여기서는 위의 파일들이 js파일과 같은 디렉토리에 있다고 가정하고 진행한다.
<form>
<label for="verse-choose">Choose a verse</label>
<select id="verse-choose" name="verse-choose">
<option>Verse 1</option>
<option>Verse 2</option>
<option>Verse 3</option>
<option>Verse 4</option>
</select>
</form>
const verseChoose = document.querySelector("select");
const poemDisplay = document.querySelector("pre");
verseChoose.addEventListener("change", () => {
const verse = verseChoose.value;
updateDisplay(verse);
});
function updateDisplay(verse) {
verse = verse.replace(" ", "").toLowerCase();
const url = `${verse}.txt`;
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.text();
})
.then((text) => (poemDisplay.textContent = text))
.catch((error) => (poemDisplay.textContent = `Could not fetch verse: ${error}`));
}
updateDisplay("Verse 1");
verseChoose.value = "Verse 1";
response.text()
를 통해 텍스트 문서를 가져온다.
위의 예제를 로컬 파일에서 실행하면 다음과 같은 에러가 나온다.
Access to XMLHttpRequest at '요청한 URL' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.
이 오류 메시지는 브라우저가 로컬 파일에서 비동기적으로 HTTP 요청을 보내는 것을 허용하지 않기 때문에 발생한다.
이는 보안상의 이유로 CORS (Cross-Origin Resource Sharing) 정책에 위배되기 때문이다.
따라서 이 예제를 실행하려면 로컬 테스트 서버를 구축하거나 웹 호스팅 서비스를 이용해야한다.
로컬 파일에서 비동기적으로 HTTP 요청을 보내는 것이 보안 문제를 일으킬 수 있는 이유는 다음과 같다.
로컬 파일은 웹 서버와 다른 출처(origin)를 가지므로 CORS 정책에 따라 다른 출처의 자원을 요청할 때 브라우저가 응답을 차단할 수 있다.
이는 악의적인 웹 사이트가 사용자의 로컬 파일에 접근하거나 수정하는 것을 방지하기 위한 장치다.
로컬 파일은 HTTP 프로토콜이 아닌 file 프로토콜을 사용하므로 브라우저가 HTTP 요청을 처리하는 방식과 다르게 동작할 수 있다.
예를 들어, 쿠키나 헤더 등의 정보를 전송하거나 받지 못할 수 있다.
로컬 파일에서 비동기적으로 HTTP 요청을 보내면 네트워크 오류나 지연 등의 상황에 대응하기 어려울 수 있다.
예를 들어, 서버가 응답하지 않거나 응답이 느리면 브라우저가 타임아웃을 설정하거나 취소하는 기능이 제한될 수 있다.
따라서 로컬 파일에서 비동기적으로 HTTP 요청을 보내는 것은 권장되지 않으며, 가능하면 웹 서버를 통해 요청과 응답을 처리하는 것이 좋다.
테스트 서버에서 웹 서버에 HTTP 요청을 해도 origin이 다른데 그건 왜 되는가?
로컬 파일에서의 요청은 테스트 서버에서의 요청과 달리 CORS 정책에 위반되기 때문에 브라우저단계에서 차단된다.
이는 로컬 파일의 origin이 null이기 때문이다.
null은 허용되는 origin이 아니므로 브라우저는 요청을 보내지 않는다.
Fetch API 에서 CORS를 설정할 수 있는 옵션이 있는데, 그걸 설정해도 HTTP 요청이 안되는가?
fetch api를 사용할 때 CORS를 허용하는 옵션은 다음과 같다.
mode: 'cors'
: 이 옵션은 기본값이며, 요청을 보낼 때 브라우저가 CORS 정책을 적용하도록 한다.
즉, 웹 서버가 CORS 헤더를 응답에 포함시켜야 한다.mode: 'no-cors'
: 이 옵션은 요청을 보낼 때 브라우저가 CORS 정책을 무시하도록 한다.
즉, 웹 서버가 CORS 헤더를 응답에 포함시키지 않아도 된다.
하지만 이 경우 응답은 opaque(불투명)로 처리되어 응답의 내용이나 상태 코드 등을 확인할 수 없다.mode: 'same-origin'
: 이 옵션은 요청을 보낼 때 브라우저가 같은 출처의 리소스만 가져오도록 한다.
즉, 웹 서버의 출처와 요청의 출처가 일치해야 한다.
따라서 로컬 파일에서 fetch api를 사용하여 HTTP 요청을 보내려면mode: 'no-cors'
옵션을 설정해야 하지만, 이 경우 응답의 내용이나 상태 코드 등을 확인할 수 없다.
그러므로 로컬 파일에서 fetch api를 사용하는 것은 권장되지 않는다.
물론, 테스트 서버에서의 요청도 거부될 수 있다.
만약 웹 서버가 CORS 헤더를 응답에 포함시키지 않으면 브라우저는 요청을 거부하고 오류 메시지를 출력한다.
CORS 헤더란 웹 서버가 다른 출처의 요청을 수락할지 여부를 알려주는 정보이다.
예를 들어, 웹 서버가 다음과 같은 헤더를 응답에 추가하면 모든 출처의 요청을 수락한다는 뜻이다.
Access-Control-Allow-Origin: *
이는 사용자의 개인 정보나 데이터를 보호하기 위한 장치이다.
function fetchBlob(product) {
const url = `images/${product.image}`;
fetch(url)
.then( response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then( blob => showProduct(blob, product) )
.catch( err => console.error(`Fetch problem: ${err.message}`) );
}
function showProduct(blob, product) {
const objectURL = URL.createObjectURL(blob);
const image = document.createElement('img');
image.src = objectURL;
image.alt = product.name;
}
blob는 "Binary Large Object"의 약어이며, 이미지 또는 비디오 파일과 같은 큰 파일 같은 객체를 나타낸다.
위의 코드에서 URL.createObjectURL()
를 사용해서 blob을 URL문자열로 변환했다.
URL.createObjectURL()
정적 메서드는 주어진 객체를 가리키는 URL을 DOMString으로 반환한다.
해당 URL은 자신을 생성한 창의 document가 사라지면 함께 사라진다.
직접 해제하려면 revokeObjectURL()
을 호출하면 된다.
오래된 코드에서 XMLHttpRequest라고 불리는 또 다른 API가 HTTP 요청을 하는 것을 볼 수 있다.
이 API는 Fetch API가 나오기 이전에 사용되던 API이며 Ajax를 구현하는 데 널리 사용되었던 최초의 API였다.
구시대의 유물이므로 가능하다면 Fetch API를 사용하자.
그래도 사용법을 알면 좋으니까 Fetch API와 XMLHttpRequest를 서로 비교해보자.
fetch('products.json')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((json) => initialize(json))
.catch((err) => console.error(`Fetch problem: ${err.message}`));
위의 코드를 XMLHttpRequest 버전으로 바꿔보자.
const request = new XMLHttpRequest();
try {
request.open('GET', 'products.json');
request.responseType = 'json';
request.addEventListener('load', () => initialize(request.response));
request.addEventListener('error', () => console.error('XHR error'));
request.send();
} catch (error) {
console.error(`XHR error ${request.status}`);
}
그런데 위의 코드는 잘못되었다!
무려 MDN의 튜토리얼에 나와있는 코드인데 믿기지 않지만 잘못된 코드이다.
try...catch 블록은 XMLHttpRequest 객체가 발생시키는 오류를 잡아내지 못한다.
XMLHttpRequest 객체는 비동기적으로 동작하기 때문에 try...catch 블록이 실행되는 시점에는 아직 요청이 완료되지 않았을 수 있다.
따라서 error 이벤트 핸들러를 사용하여 네트워크 오류나 권한 거부와 같은 요청 오류를 처리하는 것이 좋다.
만약 try...catch 블록을 사용하고 싶다면, XMLHttpRequest 객체의 open() 메서드에 세 번째 인자로 false 값을 전달하여 동기적으로 요청을 보낼 수 있다.
그러면 요청이 완료될 때까지 코드의 실행이 멈추고, 오류가 발생하면 catch 블록에서 잡아낼 수 있다.
하지만 이 방식은 웹 페이지의 성능과 사용성을 저하시킬 수 있으므로 권장하지 않는다.
올바른 코드는 아래와 같다.
const request = new XMLHttpRequest();
request.open('GET', 'products.json');
request.responseType = 'json';
request.addEventListener('load', () => {
if (request.status !== 200) {
console.error(`XHR error ${request.status}`);
}else{
initialize(request.response);
}
});
request.addEventListener('error', () => console.error('XHR error'));
request.send();
[참고] : MDN