브라우저는 웹 페이지를 불러올 때마다 HTTP나 HTTPS 프로토콜을 사용해 네트워크 요청을 보내 HTML 파일과 이미지, 폰트, 스크립트, 스타일시트 등을 가져온다.
기본적인 HTTP 요청에서
fetch()
는 3단계로 동작한다.
fetch()
를 호출한다.fetch()
API는 완전히 프라미스 기반이고 비동기 단계가 두 단계 있다.fetch('/api/users/current')
.then((res) => res.json())
.then((currentUser) => {
displayUserInfo(currentUser);
});
// or
async function asyncFetch() {
const res = await fetch('/api/users/current');
const currentUser = await res.json();
displayUserInfo(currentUser);
}
fetch()
API는 XML과는 아무 상관도 없으면서 이상한 이름이 붙은, 오래된 API인 XMLHttpRequest
를 대체한다.
fetch()
가 반환하는 프라미스는 응답 객체로 해석된다.status
프로퍼티는 HTTP 상태 코드이다.ok
프로퍼티는 status
가 200~299 사이의 숫자일때만 true이고 나머지는 false이므로 편리하게 사용할 수 있다.fetch()
는 HTTP 상태와 응답 헤더를 받는 즉시 프라미스를 해석(resolve
)한다.요청을 보내며
URL
과 함께 매개변수를 전달해야 한다면URL
마지막?
뒤에 이름 값 쌍을 추가하면 된다.
async function search(term) {
const url = new URL('/api/search');
url.searchParams.set('q', term);
const res = await fecth(url);
if (!res.ok) throw new Error(res.statusText);
const resultsArray = await res.json();
return resultArray;
}
const authHeaders = new Headers();
// HTTPS 연결이 아니라면 기본 인증을 쓰면 안 된다.
authHeaders.set('Authorization', `Basic ${btoa(`${username}:${password}`)}`);
fetch('/api/users/', { headers: authHeaders })
.then((res) => res.json())
.then((usersList) => displayAllUsers(usersList));
fetch()
의 두번째 단계는 응답 객체의json()
이나text()
메서드를 호출해서 그 메서드가 반환하는 프라미스 객체를 반환하는 것이다.
이후 세번째 단계는 프라미스가 JSON 객체 또는 문자열로 파싱되며 시작한다.
json()
, text()
외에도 세 가지 메서드가 더 있다.arrayBuffer()
이 메서드는
ArrayBuffer
로 해석되는 프라미스를 반환한다.
이진 데이터를 포함하는 응답을 받을 때 유용하다.
blob()
이 메서드는
Blob
객체로 해석되는 프라미스를 반환한다.
Blob
은 거대한 이진 객체(Binary Large Object
)의 약자이다.
Blob
은 대량의 이진 데이터를 받을 때 적합하다.
formData()
이 메서드는
FormData
객체로 해석되는 프라미스를 반환한다.
이 메서드는 응답 바디가multipart/form-data
형식으로 인코드됐다고 확신할때만 사용해야한다.
응답 바디를 비동기적으로 완료해 반환하는 다섯가지 메서드 외에 응답 바디를 스트리밍하는 방법도 있다.
이 방법은 응답 바디의 일부를 받을때마다 처리하는 형태로 사용할 수 있으며, 다운로드 진행 상태를 알리는 인터페이스를 제공할 때도 유용하다.
ReadableStream
객체이다.text()
나 json()
같은 응답 메서드를 이미 호출했다면 body
스트림이 이미 읽혔음을 뜻하는 bodyUsed
가 true
로 설정된다.bodyUsed
가 false
라면 아직 스트림을 읽지 않았다는 뜻이다.지금까지 설명한
fetch()
예제에서는HTTP
나HTTPS GET
요청을 사용했다.
POST
,PUT
,DELETE
같은 요청 메서드를 사용하려면fetch()
두번째 인자로 설정 객체를 전달하면서 그 객체에method
프로퍼티를 넣으면 된다.
fetch(url, {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringfy(reqBody),
})
.then((res) => res.json())
.then(handleRes);
사용자의 컴퓨터에서 웹 서버로 파일을 업로드할 때는
FormData
객체를 요청 바디로 사용한다.
파일 객체를 얻을 때는 웹 페이지에<imput type="file">
요소를 사용하고 이 요소에change
이벤트 리스너를 등록하는 방법을 주로 사용한다.
사용자가 취소 버튼을 클릭하거나 요청이 너무 오랫동안 수행될 경우
fetch()
요청을 취소해야 할 수도 있다.
fetch API
는AbortController
와AbortSignal
클래스를 사용해 요청을 취소할 수 있다.
// 이 함수는 fetch()와 비슷하지만 설정 객체에 timeout 프로퍼티를 지원한다.
// 이 프로퍼티에 밀리초 단위로 지정한 시간 안에 요청이 완료되지 않으면 요청을 취소한다.
function fetchWithTimeout(url, options = {}) {
if (options.timeout) {
const controller = new AbortController();
options.signal = controller.signal;
setTimeout(() => {
controller.abort();
}, options.timeout);
}
return fetch(url, options);
}
cache
브라우저의 기본 캐싱 동작을 덮어 쓴다.
유효한 값은 아래와 같다.
redirect
서버에서 보내는 리다이렉트 응답을 처리할 방법을 지정한다.
fetch()
에서 가져오는 응답 객체에 300-399 범위의 status
값이 있어서는 안된다.referer
이 프로퍼티 값은
HTTP Referer
헤더의 값을 지정하는 상대URL
을 포함하는 문자열이다.
이 프로퍼티를 빈 문자열로 설정하면 요청에서Referer
헤더는 생략한다.
HTTP 프로토콜의 기본적인 특징은 클라이언트가 요청을 보내고 서버가 이 요청에 응답한다는 것이다.
웹은 이 특징을 기초로 만들어졌다.
하지만 일부 웹 애플리케이션에서는 어떤 이벤트가 있을 때마다 서버가 알림을 보내는게 적합할 때도 있다.
HTTP에는 이를 지원하는 기능이 없지만, 클라이언트가 서버에 요청을 보내면 클라이언트와 서버 어느쪽에서도 연결을 끊지 않는다는 점을 이용한 일종의 편법이다.
EventSource()
생성자에 URL을 전달하기만 하면 된다.EventSource
객체는 그 데이터를 이벤트로 일으킨다.const ticker = new EventSource('stockprices.php');
ticker.addEventListener('bid', (event) => {
displayNewBid(event.data);
});
EventSource
객체를 생ㅇ성해 서버에 연결을 보내면 서버는 이 연결을 열어둔다.fetch()
를 통해 채팅방에 메시지를 보낸다.EventSource
객체를 통해 스트림을 구독한다.// UI 세부 사항 일부
const nick = prompt('Enter your nickname');
const input = document.getElementById('input');
input.focus();
// EventSource를 써서 새로운 메시지 알림을 등록
const chat = new EventSource('/chat');
chat.addEventListener('chat', (event) => {
const div = document.createElement('div');
div.append(event.data);
input.before(div);
input.scrollIntoView();
});
// fetch()를 써서 사용자의 메시지를 서버에 보냅니다.
input.addEventListener('change', () => {
fetch('/chat', {
method: 'POST',
body: `${nick}: ${input.value}`,
}).catch((e) => console.error);
input.value = '';
});
// 이 코드는 노드에서 실행하도록 만든 서버 사이드 프로젝트이며
// 아주 단순하고 완전히 익명인 채팅방을 만듭니다.
// /chat에 새로운 메시지를 POST로 보내거나 GET으로 text/event-stream 메시지를 받습니다.
// /에 GET 요청을 보내면 클라이언트 사이드 채팅 UI가 포함된 단순한 HTML 파일을 반환합니다.
const http = require('http');
const fs = require('fs');
const url = require('url');
// 아래에서 사용할 채팅 클라이언트 HTML 파일
const clientHTML = fs.readFileSync('chatClient.html');
// 이벤트를 보낼 ServerResponse 객체 배열
const clients = [];
// 새로운 서버를 생성하고 포트 8080을 사용합니다.
// http://localhost:8080/
const server = new http.Server();
server.listen(8080);
// 서버는 새로운 요청을 받으면 이 함수를 실행합니다.
server.on('request', (req, res) => {
// 요청받은 URL을 분석합니다.
const pathname = url.parse(req.url).pathname;
// 요청이 /라면 클라이언트 사이드 채팅 UI를 전송합니다.
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' }).end(clientHTML);
}
// /chat 경로가 아니거나 메서드가 GET, POST가 아니라면 404에러를 전송합니다.
else if (
pathname !== '/chat' ||
(req.method !== 'GET' && req.method !== 'POST')
) {
res.writeHead(404).end();
}
// /chat에 GET 요청이 들어왔다면 클라이언트가 연결하는 겁니다.
else if (req.method === 'GET') {
acceptNewClient(req, res);
}
// 아니라면 /chat 요청은 새로운 메시지를 보내는 POST 요청입니다.
else {
broadcastNewMessage(req, res);
}
});
// 클라이언트가 새로운 EventSource 객체를 생성할 때, 또는 EventSource가 자동으로 다시 연결될 때 생성되는 /chat 엔드포인트의 GET 요청을 처리합니다.
function acceptNewClient(req, res) {
// 나중에 메시지를 보낼 수 있도록 응답 객체를 기억합니다.
clients.push(res);
// 클라이언트가 연결을 끊으면 클라이언트 배열에서 해당 클아이언트의 응답 객체를 제거합니다.
req.connection.on('end', () => {
clients.splice(clients.indexOf(res), 1);
res.end();
});
// 헤더를 설정하고 이 클라이언트에 초기 채팅 이벤트를 전송합니다.
res.writeHead(200, {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
});
res.write('event: chat/ndata: Connected\n\n');
// 여기서는 의도적으로 res.end()를 호출하지 않았습니다.
// 서버 전송 이벤트는 연결을 계속 유지해야 합니다.
}
// 이 함수는 사용자가 새로운 메시지를 보내는 /chat 엔드포인트에 대한
// POST 요청의 응답으로 호출됩니다.
async function broadcastNewMessage(req, res) {
// 먼저 요청 바디를 읽어 사용자의 메시지를 가져옵니다.
req.setEncoding('utf8');
let body = '';
for await (let chunk of req) {
body += chunk;
}
// 바디를 읽으면 빈 응답을 보내고 연결을 닫습니다.
res.writeHead(200).end();
// 메시지를 text/event-stream 형식으로 바꾸고 각 행 앞에 "data: "를 붙입니다.
let message = 'data: ' + body.replace('\n', '\ndata: ');
// 메시지 데이터 앞에 채팅 이벤트임을 뜻하는 전치사를 붙이고 뉴라인 두개를 이어붙여
// 이벤트가 끝났음을 알립니다.
let event = `event: chat\n${message}\n\n`;
// 연결된 클라이언트 전체에 이 이벤트를 전송합니다.
clients.forEach((client) => client.write(event));
}
웹 소켓 API는 복잡하고 강력한 네트워크 프로토콜의 단순한 인터페이스이다.
브라우저는 웹 소켓을 통해 텍스트나 이진 메시지를 서버와 쉽게 주고받을 수 있다.
서버 전송 이벤트와 마찬가지로 클라이언트가 연결을 시작해야 하지만
일단 연결이 되면 서버 전송 이벤트와 달리 이진 메시지를 지원하고 양방향 통신이 가능하다.
https://
대신 wss://
로 시작한다.웹 소켓 서버와 통신하려면 서버와 서비스를 나타내는
wss://
URL을 전달해 웹 소켓 객체를 만든다.
let socket = new WebSocket('wss://example.com/stockticker');
웹 소켓 객체를 만들면 연결 프로세스는 자동으로 시작되지만 새로 생성한 웹소켓이 즉시 연결되지는 않는다.
웹 소켓으로 연결된 서버에 메시지를 보낼 때는 웹소켓 객체의
send()
메서드를 호출하기만 하면 된다.
send()
는 문자열, 블롭, ArrayBuffer, 형식화 배열, DataView 중 하나를 메시지 인자로 받는다.
웹 소켓을 통해 서버 메시지를 수신할 때는
message
이벤트에 이벤트 핸들러를 등록한다.
message
이벤트의 이벤트 객체는MessageEvent
인스턴스이며 이 인스턴스의data
프로퍼티에 서버의 메시지가 포함되어 있다.
서버가UTF-8
로 인코드된 텍스트를 보낸다면event.data
에 이 텍스트가 포함된다.
웹 소켓 프로토콜은 텍스트와 이진 메시지를 주고받지만 그 메시지의 구조나 의미에 대해서는 아무것도 주고받지 않는다.
애플리케이션에서 웹 소켓을 사용하려면 반드시 웹 소켓 기반 통신 프로토콜을 만들어야 한다.
웹 애플리케이션은 브라우저 API를 사용해 사용자의 컴퓨터에 데이터를 저장할 수 있다.
웹 스토리지 API는
localStorage
,sessionStorage
객체로 구성된다.
이들은 기본적으로 문자열 키와 문자열 값을 연결한 지속성 있는 객체이다.
쿠키는 원래 서버 사이드 스크립트에서 사용하도록 설계된 메커니즘이다.
클라이언트 사이드에서 쿠키를 다룰 수 없는건 아니지만, 쉽지는 않으며 텍스트 데이터를 소량 저장하는 정도로만 쓸 수 있다.
또한 쿠키에 저장된 데이터는 서버에서 쓸모가 없더라도 HTTP 요청이 있을 때마다 항상 서버로 전송된다.
indexDB
는 인덱스를 지원하는 객체 데이터베이스의 비동기 API이다.
Window 객체의
localStorage
,sessionStorage
프로퍼티는 스토리지 객체를 참조한다.
스토리지 객체는 일반적인 JS 객체와 거의 비슷하지만 다음과 같은 차이가 있다.
localStorage와 sessionStorage는 문서 출처에 종속된다.
localStorage는 브라우저에도 종속된다.
sessionStorage에 저장된 데이터는 최상위 창이나 브라우저 탭이 닫힐 때 사라진다.
하지만 최신 브라우저에는 최근에 닫은 탭을 다시 열고 마지막 세션을 복원하는 기능이 있으므로 이런 탭에 저장된 데이터는 더 오래 지속될 수 있다.
쿠키는 특정 웹 페이지 또는 웹 사이트와 연관된 소량의 이름 붙은 데이터이며 웹 브라우저에 저장된다.
쿠키
또는매직 쿠키
라는 용어는 컴퓨팅 분야에서 오래 전부터 접근을 식별하거나 허용하는 데이터의 작은 덩어리, 특히 개인적이거나 비밀스러운 데이터를 가리키는 용도를 쓰였다.
JS의 가장 기본적인 특징 중 하나는 싱글 스레드라는 점이다.
웹 브라우저는 Worker
클래스의 싱글 스레드 요건을 아주 조심스럽게 완화한다.
Worker
클래스의 인스턴스는 메인 스레드, 이벤트 루프와 동시에 실행되는 스레드이다.Worker
객체새로운 워커를 생성할 때는 워커가 실행할 JS 코드를 URL로 전달하면서
Worder()
생성자를 호출합니다.
const dataCruncher = new Worker('utils/cruncher.js');
dataCruncher.postMessage('/api/data/to/crunch');
dataCruncher.onmessage = function (e) {
const stats = e.data;
console.log(`Average: ${stats.mean}`);
};
Worder()
생성자로 새로운 워커를 생성할 때는 JS 코드 파일의 URL을 전달한다.
이 코드는 워커를 생성한 스크립트에서 분리된, 새롭고 깨끗한 JS 실행 환경에서 실행된다.
이 새로운 실행 환경의 전역 객체가Worder-GlobalScope
객체이다.
워커는 JS에서 모듈 시스템을 수용하기 전에 정의됐으므로 워커만의 가져오기 시스템이 있다.
WorkerGlobalScope
에는 모든 워커에서 접근할 수 있는importScripts()
전역 함수가 있다.
importScripts('utils/Histogram.js', 'utils/BitSet.js');
워커에서 모듈을 사용하려면 반드시
Worder()
생성자에 두번째 인자를 전달해야 한다.
두번째 인자는 반드시type
프로퍼티가 문자열module
인 객체여야 한다.
Worder()
생성자에서type:"module"
옵션은 HTML<script>
태그의type="module"
속성과 마찬가지로 이 코드를 모듈로 해석하며import
선언을 허용하라는 의미이다.