[자바스크립트 완벽가이드] - 웹 브라우저의 자바스크립트

Lee Jeong Min·2022년 8월 15일
0

자바스크립트

목록 보기
16/17
post-thumbnail

자바스크립트 완벽가이드 15장에 해당하는 부분이고, 읽으면서 자바스크립트에 대해 새롭게 알게된 부분만 정리한 내용입니다.

웹 프로그래밍 기본

script src 속성의 장점

  • JS코드를 HTML파일에서 제거해 단순화한다. 즉, '내용'과 '동작'을 분리한다.
  • 여러 개의 웹 페이지에서 같은 JS 코드를 공유시, src 속성을 쓰면 코드 하나만 관리해도 된다.
  • JS 코드를 여러 페이지에서 공유한다면 한 번만 내려받으면 된다.(다른 페이지는 브라우저 캐시에서 가져옴)
  • src 속성은 임의의 URL을 값으로 받으므로, 한 서버에 있는 JS 프로그램이나 웹 페이지가 다른 웹 서버에 있는 코드를 가져올 수 있다.

스크립트 타입 지정

웹 초기에는 JS가 아닌 다른 언어가 쓰일 것을 염두하여 <script> 태그에 language='javascript', type='application/javascript' 같은 속성을 추가했다.

그러나 이제는 불필요하며, JS는 웹의 기본 언어이자 유일한 언어이다.

<script> 태그에 type 속성을 사용하는 경우는 단 두 가지 뿐이다.

  • 스크립트가 모듈일 때(type=module)
  • 웹 페이지에 데이터를 가져오지만 표시하지는 않을 때(type=text/x-custom-data 같은 임의의 값을 지정하면 JS 코드로 실행되지 않는다. 문서의 트리에는 남아있고, text 프로퍼티로 그 데이터를 가져올 수 있다.)

document.write 메서드가 성능에 영향을 주는 이유

JS 코드가 문서 콘텐츠에 영향을 주는 방법으로 document.write() 메서드를 사용해 스크립트 위치에서 HTML 텍스트를 주입할 수 있었다. 그러나 이런식으로 동작한다는 것은 HTML 파서가 <script> 요소를 만날 때마다 반드시 그 스크립트를 실행해 HTML을 출력하지 않음을 확인한 후에야 문서 분석과 렌더링을 재개할 수 있다는 의미이다.

이런 방식은 웹 페이지의 분석과 렌더링 속도를 심하게 저해한다!

웹 브라우저의 전역 객체

전역 객체는 두 가지 임무를 수행한다.

  • 내장 타입과 함수 정의
  • 현재 웹 브라우저 창을 나타내기도 하고, 그 창의 브라우징 히스토리(history), 너비같은 프로퍼티(innerWidth)를 정의하기도 한다.

전역 객체와 관련된 기능을 사용할 때는 앞에 window.를 붙이는 편이 좋다.

클라이언트 사이드 자바스크립트 스레드 모델

JS는 싱글 스레드 언어이므로 프로그래밍이 훨씬 단순하다. 락, 교착상태, 경합 조건을 걱정할 필요가 없다. 그러나 웹 워커라는 사용자 인터페이스를 멈추지 않으면서 실행된느 백그라운드 스레드가 존재한다.

이를 통해 웹 플랫폼은 동시성을 구현한다.

웹 워커 스레드에서 실행되는 코드는 문서 콘텐츠에 접근할 수 없고, 오로지 다른 스레드와 비동기 메시지 이벤트를 통해서만 통신한다.

클라이언트 사이드 JS 타임라인

JS 프로그램은 스크립트 실행 단계에서 이벤트 처리 단계로 넘어간다. 그 과정을 한번 살펴보자.

  1. 웹 브라우저가 Document 객체를 생성하고 웹 페이지 분석을 시작한다. 이 단계에서 document.readyState 프로퍼티의 값은 loading이다.

  2. HTML 파서가 async, defer, type="module" 속성이 없는 <script> 태그를 만나면 스크립트 태그를 문서에 추가하고 스크립트를 실행한다. 이 스크립트는 동기적으로 실행되고 HTML 파서는 일시적으로 중지된다. 이런 스크립트는 document.write() 사용이 가능하다.

  3. 파서가 async 속성이 있는 <script> 요소를 만나면 스크립트 텍스트를 내려받기 시작하고, 모듈이라면 가져오는 모듈 역시 재귀적으로 내려받아 문서 분석을 계속한다. 이러한 비동기 스크립트는 document.write() 메서드를 사용해서는 안된다.

  4. 문서 분석이 완전히 끝나면 document.readState 프로퍼티가 interactive로 바뀐다.

  5. defer 속성이 있는 스크립트, async 속성이 없는 모듈 스크립트는 문서 순서대로 실행된다.

  6. 브라우저가 Document 객체에서 DOMContentLoaded 이벤트를 일으킨다. 이 이벤트는 스크립트 단계를 두 번째 단계로 전환한다.

  7. 이 시점에서 문서 분석은 완전히 끝났지만 브라우저는 여전히 이미지 같은 콘텐츠를 기다리고 있을 수 있다. 콘텐츠 로딩이 끝나고 async 스크립트 로딩과 실행도 끝나면 document.readyState 프로퍼티는 complete로 바뀌고 웹 브라우저는 윈도우 객체에서 load 이벤트를 일으킨다.

  8. 이제부터 사용자의 입력 이벤트, 네트워크 이벤트, 타이머 종료 등에 의해 이벤트 핸들러가 비동기적으로 호출된다.

프로그램 입출력

전역 navigator 프로퍼티를 통해 웹 브라우저, 운영 체제와 그 기능에 접근할 수 있다. 예를 들어 navigator.userAgent는 웹 브라우저를 식별하는 문자열이고 navigator.language는 사용자가 선호하는 언어이며 navigator.hardwareConcurrency는 웹 브라우저가 사용할 수 있는 논리적 CPU의 개수이다. 마찬가지로 전역 screen 프로퍼티의 screen.width, screen.height 프로퍼티를 통해 사용자의 디스플레이 크기에 접근할 수 있다.

이벤트

클라이언트 사이드 JS 프로그램은 비동기적인 이벤트 주도 프로그래밍 모델을 사용한다.

addEventListener의 3번째 인수

document.addEventListener("click", handleClick, {
  capture: true,
  once: true,
  passive: true
});

위 예제에서 3번째 인수안에 있는 객체들을 한번 살펴보자.

  • capture: 캡처 프로퍼티가 true이면 이벤트 핸들러는 캡처링 핸들러로 등록된다. 이 프로퍼티가 false이거나 생략됐다면 핸들러는 캡처링을 사용하지 않는다.
  • once: 프로퍼티가 true이면 이벤트 리스너는 한 번 호출된 뒤 자동으로 제거된다. 이 프로퍼티가 false이거나 생략됐다면 핸들러는 절대 자동으로 제거되지 않는다.
  • passive: 프로퍼티가 true이면 이벤트 핸들러는 절대 preventDefault()를 호출해서 기본 동작을 취소하지 않는다. 이 옵션은 모바일 장치의 터치 이벤트에서 특히 중요한데, touchmove 라는 이벤트의 이벤트 핸들러가 브라우저의 기본 스크롤을 방해하면 브라우저는 부드러운 스크롤 동작을 구현할 수 없기 때문이다.

브라우저의 기본 동작을 막는 방법

preventDefault() 메서드를 사용하는 것이 표준 방법이다. 핸들러에 return false;를 함으로 기본 동작을 막을 수 있지만, 표준 방법을 사용하는 것이 더 좋다.

커스텀 이벤트 전달

JS 객체에 addEventListener() 메서드가 있다면 그 객체는 '이벤트 대상'이므로 dispatchEvent() 메서드 또한 가지고 있다. CustomEvent() 생성자로 이벤트 객체를 생성하고 dispatchEvent()에 전달할 수 있다. CustomEvent()의 첫 번째 인자는 이벤트 타입을 나타내는 문자열이고 두 번째 인자는 이벤트 객체의 프로퍼티를 지정하는 객체이다.

예시

// 작업 중임을 UI에 알리는 커스텀 이벤트 전달
document.dispatchEvent(new CustomEvent('busy', { detail: true }));

// 네트워크 동작
fetch(url)
  .then(handleNetworkResponse)
  .catch(handleNetworkError)
  .finally(() => {
    document.dispatchEvent(new CustomEvent('busy', { detail: false }));
  });

document.addEventListener('busy', (e) => {
  if (e.detail) {
    showSpinner();
  } else {
    hideSpinner();
  }
});

문서 스크립트

미리 선택된 요소

역사적인 이유로 Docuemnt 클래스에는 특정 노드에 접근하는 단축 프로퍼티가 있다. 예를 들어 images, forms, links 프로퍼티로 문서에 존재하는 <img>, <form>, <a> 요소에 쉽게 접근할 수 있다. 단, <a> 태그에 href 속성이 있어야 한다. 이 프로퍼티들은 HTMLCollection 객체를 참조하고, document.forms 프로퍼티를 쓰면 <form id="address"> 태그에 다음과 같이 접근할 수 있다.

document.forms.address;

append() vs appendChild()

append()는 인자를 개수 제한 없이 받는다. 또한 Node 객체 또는 문자열을 받는다. 문자열 인자는 자동으로 Text 노드로 변환된다. 그러나 appendChild()는 오로지 node 객체만 자식 요소로 추가할 수 있고, 한번에 오직 하나의 노드만 추가할 수 있다.

출처: https://webruden.tistory.com/634

모던 자바스크립트 Deep Dive에선 appendChild() 메서드만 나와있어 이것을 주로 썻었는데 앞으로 append()라는 메서드를 알았으니 이를 사용해야겠다. 새롭고 유용한것을 배워가는 것 같아 기분이 좋다.

CSS 스크립트

이 장에서는 JS 코드로 CSS를 다루는 방법을 설명한다.

CSS 클래스

JS로 문서 콘텐츠의 스타일을 바꾸는 가장 단순한 방법은 HTML 태그의 class 속성에 CSS 클래스를 추가하거나 제거하는 것

classList 프로퍼티를 사용하여 add 또는 remove를 하면 쉽다.

인라인 스타일

클래스 스타일을 미리 만들어 두기 어렵다면 아래와 같이 인라인 스타일을 사용하는 방법이 있다.

function displayAt(tooltip, x, y) {
  tooltip.style.display = 'block';
  tooltip.style.position = 'absolute';
  tooltip.style.left = `${x}px`;
  tooltip.style.top = `${y}px`;
}

인라인 스타일은 style 프로퍼티를 이용하는 방법이며 style 프로퍼티는 문자열이 아닌 CSSStyleDeclaration 객체이다. 이 객체는 style 속성에 텍스트 형태로 존재하는 CSS 스타일을 파싱한 결과이다.

위에서 보았듯이, CSSStyleDeclaration 객체의 스타일 프로퍼티를 사용할 때는 값을 반드시 문자열로 바꾸어야한다.

CSSStyleDeclaration 객체를 사용하는 것보다 요소의 인라인 스타일을 사용하는 게 쉬울 때도 있다. 이런 경우, getAttribute(), setAttribute() 메서드를 사용하거나 CSSStyleDeclaration 객체의 cssText 프로퍼티를 사용하자.

// 요소 e의 인라인 스타일을 요소 f에 복사
f.setAttribute('style', e.getAttribute('style'));

// 위의 코드 결과와 동일한 cssText 사용예시
f.style.cssText = e.style.cssText;

계산된 스타일

계산된 스타일을 구할 때는 Window 객체의 getComputedStyle() 메서드를 사용하자.

첫 번째 인자는 계산된 스타일을 가져올 요소, 두 번째 인자는 선택사항으로 ::before, ::after 같은 CSS 가상 요소이다.

getComputedStyle()의 반환값은 지정된 요소에 적용된 스타일 전체를 나타내는 CSSStyleDeclaration 객체이다. 이는 인라인 스타일의 CSSStyleDeclaration 객체와 차이가 있다.

  • 읽기 전용이다.
  • 계산된 스타일 프로퍼티는 절댓값이다.(크기를 나타내는 프로퍼티는 모두 픽셀 값으로 바뀐다.)
  • 단축 프로퍼티는 계산되지 않고 베이스인 기본 프로퍼티만 계산된다.
  • 계산된 스타일에는 cssText가 존재하지 않는다.

CSS로 요소의 위치와 크기를 정확히 지정할 수 있지만 계산된 스타일을 통해 요소의 위치와 크기를 가져오는 것은 권하지 않는다.

15.5.2에 나오는 요소의 위치 검색 참고(getBoundingClientRect())

스타일시트 스크립트

JS로 스타일시트 자체도 조작할 수 있다. <style>, <link> 태그의 Element 객체에는 disabled 프로퍼티가 있으며 이를 통해 스타일시트 전체를 비활성화할 수 있다. 마찬가지로 DOM 조작 방법을 통해 새로운 스타일시트를 삽입할 수도 있다.

disabled를 사용해 비활성화가 가능하다는 점과 JS의 append 또는 replaceWith와 같은 메서드로 스타일시트를 추가 및 교체가 가능하다는 것을 알아두면 될듯

CSS 애니메이션과 이벤트

  • transitionrun: 트랜지션이 처음 시작하면 이 이벤트 발생. transition-delay와 같은 시각적인 변화가 시작되기 전 이벤트가 먼저 일어날 때를 말함
  • transitionstart: 시각적인 변화가 일어남과 동시에 이 이벤트 발생
  • transitionend: 애니메이션 완료되면 이 이벤트 발생

트랜지션과 마찬가지로 애니메이션 역시 JS 코드에서 주시할 수 있는 이벤트를 일으킨다.

  • animationstart: 애니메이션이 시작할 때
  • animationend: 애니메이션이 끝날 때
  • animationiteration: 애니메이션이 두 번 이상 반복되면 마지막을 제외하고 반복마다 이벤트 발생

문서 지오메트리와 스크롤

DOM 요소의 위치를 정확히 파악하기 위한 개념들에 대해 알아보자.

문서 좌표와 뷰포트 좌표

뷰포트(창기준): 문서 콘텐츠를 실제로 표시하는 부분으로 메뉴와 툴바, 탭 등을 제거한 부분이다.

요소의 위치를 언급할 땐 반드시 문서 좌표인지 뷰포트 좌표인지를 확실히 해야한다.

문서가 뷰포트보다 작거나 스크롤 되지 않았다면 문서 좌표계와 뷰포트 좌표계가 일치한다. 그러나 일반적으로 두 좌표계를 변환할 때는 반드시 스크롤 오프셋을 더하거나 빼야한다.

참고: https://ko.javascript.info/coordinates

위에서 보는 pageY, pageX가 문서좌표이고 clientY, clientX가 뷰포트 좌표이다.

일반적으로 문서 좌표엔 큰 의미가 없어서 뷰포트 좌표를 사용하는 경우가 많다.

뷰포트 좌표 관련 메서드

  • getBoundingClientRect()
  • elementFromPoint()
  • 마우스/포인터 이벤트 객체의 clientX, clientY

요소의 위치 검색

getBoundingClientRect() 메서드를 호출하면 left, right, top, bottom, width, height 프로퍼티가 있는 객체를 반환한다.

이 메서드를 이용하여 위치를 파악할 수 있음

지정된 위치에 있는 요소 파악

getBoundingClientRect() 메서드와 달리 특정 좌표에 있는 요소가 무엇인지 알기위해 elementFromPoint() 메서드를 이용할 수 있다.

해당 지점에서 가장 안쪽의 요소와 가장 위쪽의 요소를 반환한다.

뷰포트 크기, 콘텐츠 크기, 스크롤 위치

이 부분은 그림을 보면 쉽게 이해할 수 있을 것 같아 그림을 첨부한다.

clientWidth & clientHeight

offsetWidth & offsetHeight

scrollWidth & scrollHeight와 전체 비교

Element 객체의 다른 프로퍼티와 달리 scrollLeft, scrollTop은 쓰기 가능한 프로퍼티이며 이 값을 설정해 요소 안에서 콘텐츠를 스크롤할 수 있다.

웹 컴포넌트

책 안의 내용도 있지만 저번에 따로 정리했던 글이 있어서 같이 첨부.

웹 컴포넌트를 알아보자

SVG

SVG는 Scalable Vector Graphics 라는 용어의 약자로, 이름에 들어 있는 '벡터'라는 단어는 이 이미지가 픽셀 값 행렬로 이뤄진 비트맵 이미지와는 다름을 암시한다. SVG '이미지'는 원하는 그래픽을 표현하는 데 필요한 단계들을 해상도와 상관없이(따라서 확대/축소가 자유롭다) 정밀하게 표현하는 일종의 언어이다.

SVG 이미지는 XML 마크업 언어를 사용해 텍스트 파일로 작성한다.

웹 브라우저에서 SVG를 사용하는 방법 3가지

  1. <img> 태그 안에 .svg 이미지 파일을 사용
  2. HTML 문서에 SVG 태그를 직접 사용
  3. 필요에 따라 DOM API를 사용해 동적으로 SVG 요소를 생성

HTML 속 SVG

svg 태그의 자손 요소는 일반적인 HTML 태그가 아니며, 안에 <circle>, <line>, <text>등의 태그를 가지고 있다. 또한 이를 꾸미는데 사용되는 fill, stroke-width, text-anchor 같은 스타일은 일반적인 CSS 스타일 프로퍼티가 아니다. SVG 태그에서는 font 단축 프로퍼티가 동작하지 않으므로 font-family, font-size, font-weight를 따로따로 써야한다.

SVG 스크립트

<img> 태그를 사용하지 않고 HTML 파일에 SVG를 직접 넣는 건 DOM API를 통해 SVG를 조작할 수 있기 때문이다. 일반적인 DOM 조작과 비슷하게, SVG의 요소를 DOM 조작하여 속성을 동적으로 바꾸어 SVG를 조작할 수 있다.

document.querySelector API를 이용하여 DOM 요소를 가져와서 setAttribute 같은 것으로 조작한다.

자바스크립트로 SVG 이미지 생성

SVG 태그는 엄밀히 말해 HTML 태그가 아니라 XML 태그이기 때문에 createElementNS()를 사용하여 만들수 있다. 이 함수는 XML 네임스페이스 문자열(http://www.w3.org/2000/svg)을 첫 번째 인자로 받는다.

책의 예제에서는 실제 파이 조각을 그리는 path 부분까지 코드가 나와있는데, 너무 자세하기 때문에 간단한 코드만 보자.

  const svg = 'http://www.w3.org/2000/svg';

// svg 태그 생성
  const chart = document.createElementNS(svg, 'svg');
  chart.setAttribute('width', width);
  chart.setAttribute('height', height);
  chart.setAttribute('viewBox', `0 0 ${width} ${height}`);

이후 뒷부분에서 각도를 조절하고, 수학적 계산을 통해 path의 속성값과 데이터 키에 사용될 여러가지 태그를 만들어 svg를 동적으로 사용하는 예시를 다룬다.

<canvas>의 그래픽

캔버스 API와 SVG의 가장 큰 차이는 캔버스는 메서드를 호출하는 방식으로 그림을 그리는 반면, SVG는 XML 요소의 트리를 만드는 방식으로 그림을 그린다는 것이다. 둘다 장단점이 있는데 SVG 그래픽은 요소를 삭제하는 방식으로 쉽게 수정할 수 있지만 <canvas>에서 같은 그래픽의 요소 하나를 제거하더라도 처음부터 다시 그려야 할 때가 많다.

그러나 캔버스 API는 JS 기반이라 SVG 문법에 비해 비교적 간결하다.

캔버스 API는 getContext() 메서드를 호출해 얻는 컨텍스트 객체에 존재한다. 인자 2d를 넘겨서 getContext()를 호출하면 2차원 그래픽을 그릴 수 있는 CanvasRenderingContext2D 객체를 얻는다.

간단한 canvas 예제

let canvas = document.querySelector('#square');
let context = canvas.getContext('2d');
context.fillStyle = '#f00';
context.fillRect(0, 0, 10, 10);

canvas = document.querySelector('#circle');
context = canvas.getContext('2d');
context.beginPath();
context.arc(5, 5, 5, 0, 2 * Math.PI, true);
context.fillStyle = '#00f';
context.fill();

캔버스는 SVG와 달리 패스를 문자열로 만들지 않고 이전 예제의 beginPath(), arc() 같은 메서드를 이어서 만든다. 이후 패스를 완성한뒤, fill() 같은 다른 메서드를 적용한다.

패스와 다각형

패스는 하나 이상의 서브패스의 연속으로, 새로운 패스를 시작할 때 beginPath() 메서드를 사용한다. 이후 moveTo() 메서드로 시작할 지점을 지정하고 lineTo() 메서드를 이용하여 직선을 그을 수 있다. 그러나 이 직선들은 아무것도 그리지 않고 stroke() 메서드나 fill()을 사용하여 화면에 그릴 수 있다.

일반적으로 서브패스는 '열려'있기 때문에 모두 그리고 난 후에는 closePath() 메서드를 호출하여 닫아주어야 한다.

캔버스에서 서브패스가 겹치거나 교차할 때 어떤 영역이 내부에 있고 어떤 영역이 외부에 있는지 판단해야 한다. 캔버스는 '넌제로 와인딩 규칙'을 이용해 이를 판단한다.

육각형 안의 사각형이 있는 예제에서는 육각형과 사각형의 꼭짓점들이 서로 반대 방향으로 그러졌기에 이 규칙이 적용된다.

위키백과 - Nonzero Winding Rule

캔버스의 크기와 좌표

캔버스의 width, height 속성은 캔버스가 실제로 사용할 픽셀 숫자이다. 예를들어 각각 100이면 캔버스는 10,000 픽셀을 표현한다.

하지만 이러한 width, height 속성으로 캔버스의 화면 크기를 지정하지 말고, 대신 CSS의 width, height 스타일을 사용하여 window.devicePixelRatio를 곱한 값으로 설정해야 메모리 픽셀과 물리적 픽셀이 대응되게 할 수 있다.

그래픽 상태 저장과 복원

<canvas> 요소에는 컨텍스트 객체가 오직 하나만 존재하며, getContext()를 여러 번 호출하더라도 같은 CanvasRendering-Context2D 객체가 반환된다. 캔버스 API가 한 번에 한 가지 그래픽 속성 세트만 허용하긴 하지만, 현재 그래픽 사앹를 저장할 수 있으므로 수정했다가 쉽게 복원할 수 있다. (save() 메서드와 restore() 메서드 사용)

오디오 API

HTML <audio>, <video> 태그를 통해 웹 페이지에 사운드와 비디오를 넣을 수 있다.

Audio() 생성자

HTML 문서에 <audio> 태그를 삽입하지 않아도 웹 페이지에서 사운드 효과를 이용할 수 있다. <audio> 요소를 생성하여 문서에 추가할 필요없이 play() 메서드를 호출하면 된다.

예시

// 사용할 사운드 효과를 미리 불러온다.
const soundeffect = new Audio("soundeffect.mp3");

// 사용자가 마우스 버튼을 클릭할 때마다 사운드를 재생한다.
document.addEventListener('click', () => {
  soundeffect.cloneNode().play(); // 사운드를 불러와서 재생
})

audio 요소는 문서에 추가하지 않았으므로 재생을 마친 후 가비지 컬렉션 대상이 된다.

WebAudio API

이 API를 통해 파형의 소스, 변환, 대상을 나타내는 AudioNode 객체를 만들고 이 노드를 한데 묶어서 사운드를 만들 수 있다.

위치, 내비게이션, 히스토리

Window와 Document 객체의 location 프로퍼티는, 현재 창에 표시되는 문서의 URL을 나타내고 창에 새로운 문서를 불러오는 API를 제공하는 Location 객체를 참조한다.

이후 뒷 절에서는 내부 프로퍼티와 관련한 이야기를 다루며 히스토리와 관련한 hashChange, pushState() 등을 소개한다. 이와 관련하여 참고하면 좋은 자료가 있어 첨부.

SPA & Routing

네트워크

이 절에서는 세 가지 네트워크 API를 설명한다.

  • fetch()
  • SSE(서버 전송 이벤트) API
  • 웹소켓

fetch()

fetch()의 3단계 동작

  1. 콘텐츠를 가져올 URL을 전달하면서 fetch()를 호출
  2. HTTP 응답이 도착하기 시작하면서 1단계에서 비동기적으로 반환한 응답 객체를 가져오고 이 응답 객체의 메서드를 호출해 응답 바디를 가져온다.
  3. 2단계에서 비동기적으로 반환한 바디 객체를 사용해 필요한 일을 한다.

3단계 동작 예시코드

fetch('/api/users/current')
  .then((res) => res.json())
  .then((currentUser) => displayUserInfo(currentUser));

HTTP 상태 코드, 응답 헤더, 네트워크에러

fetch()는 서버의 응답이 도착하기 시작할 때, HTTP 상태와 응답 헤더를 받는 즉시 프라미스를 해석(resolve)한다. 일반적으로 이 시점은 응답 바디 전체가 도착하기 전인데, 이 단계에서 헤더는 확인할 수 있다.

응답 바디 분석

응답 객체에는 json(), text() 외에도 다음 세 가지 메서드가 존재한다.

  • arrayBuffer()
    이 메서드는 ArrayBuffer로 해석되는 프라미스를 반환한다. 이진 데이터를 포함하는 응답을 받을 때 유용하다.

  • blob()
    블롭은 거대한 이진 객체(Binary Large Object)의 약자로 대량의 이진 데이터를 받을 때 적합하다.

  • formData()
    FormData 객체로 해석되는 프라미스를 반환한다. 이 메서드는 응답 바디가 multipart/form-data 형식으로 인코드됐다고 확신할 때만 사용하며 자주 사용되지는 않는다.

응답 바디 스트리밍

응답 바디를 비동기적으로 완료해 반환하는 다섯 가지 메서드 외에 응답 바디를 스트리밍하는 방법도 있다.

응답 바디의 일부를 받을 때마다 처리하는 형태로 사용.

응답 객체의 body 프로퍼티는 ReadableStream 객체이다. text()json() 같은 응답 메서드를 이미 호출했다면 body 스트림이 이미 읽혔음을 뜻하는 bodyUsed가 true로 설정된다. 그러나 아직 false라면 response.body에서 getReader()를 호출해 스트림 리더 객체를 얻고, 이 리더 객체에 read() 메서드를 사용해 스트림에서 텍스트 덩어리를 비동기적으로 읽을 수 있다.

이를 사용한 예시를 살펴보자.
: 아주 큰 JSON 파일을 내려받을 때 사용자에게 진행 상태를 보고하는 코드

const streamBody = async (response, reportProgress, processChunk) => {
  // 받아야 되는 바이트 숫자로 헤더가 없으면 NaN이다.
  const expectedBytes = parseInt(response.headers.get('Content=Length'));
  let bytesRead = 0;
  const reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let body = '';

  while (true) {
    const { done, value } = await reader.read();

    if (value) {
      if (processChunk) {
        const processed = processChunk(value);
        if (processed) {
          body += processed;
        } else {
          body += decoder.decode(value, { stream: true });
        }

        if (reportProgress) {
          bytesRead += value.length;
          reportProgress(bytesRead, bytesRead / expectedBytes);
        }
      }

      if (done) {
        break;
      }
    }
  }

  return body;
};

두 번째 인자로 전달된 reportProgress 콜백은 덩어리를 받을 때마다 호출되고 processChunk 콜백은 데이터 덩어리를 Uint8Array 객체로 받는다.

요청 취소

fetch() 요청을 취소해야 하는 경우도 있다. 이러한 경우 AbortController와 AbortSignal 클래스를 사용해 요청을 취소할 수 있다.

// fetch와 비슷하지만 설정 객체에 timeout 프로퍼티를 지원
// 이 프로퍼티에 밀리초 단위로 지정한 시간 안에 요청이 완료되지 않으면 요청을 취소
const fetchWithTimeout = (url, options = {}) => {
  if (options.timeout) {
    const controller = new AbortController();
    options.signal = controller.signal;

    setTimeout(() => {
      controller.abort();
    }, options.timeout);
  }

  return fetch(url, options);
};

소소한 요청 옵션

fetch()Request() 생성자의 두 번째 인자로 설정 객체를 전달해 요청 메서드, 요청 헤더, 요청 바디를 지정할 수 있다.

  • cache: 브라우저의 기본 캐싱 동작을 덮어 쓴다.
    • default: 기본 캐싱 동작을 지정하는 값
    • no-store: 브라우저가 캐시를 무시하게 한다.
    • reload: 브라우저가 항상 캐시를 무시하고 네트워크 요청을 보내게 함
    • no-cache: 캐시에 저장된 값이 오래됐든 그렇지 않든 항상 확인 후에 전송한다.
    • force-cache: 항상 캐시에 있는 값 전송(저장된 값이 오래되어도)
  • redirect: 서버에서 보내는 리다이렉트 응답을 처리할 방법을 지정
    • follow: 기본 값이며 서버의 리다이렉트 응답을 따름
    • error: 리다이렉트 응답을 반환할 경우 프라미스를 거부
    • manual: 리다이렉트 응답을 직접 처리
  • referrer: HTTP Referer 헤더의 값을 지정하는 상대 URL을 포함하는 문자열. 이 프로퍼티를 빈 문자열로 설정하면 요청에서 Referer 헤더를 생략한다.

서버 전송 이벤트

HTTP 프로토콜의 기본적인 특징은 클라이언트가 요청을 보내고 서버가 이 요청에 응답한다는 것이다. 그러나 일부 웹 애플리케이션에서는 어떤 이벤트가 있을 때마다 서버가 알림을 보내는 게 적합할 때도 있다.

이를 클라이언트 사이드 자바스크립트에서는 EventSource API를 만들어 이 패턴을 지원한다.

const ticker = new EventSource('stockprices.php');

ticker.addEventListener('bid', (event) => {
  displayNewBid(event.data);
});

이 코드에서 메시지의 이벤트의 이벤트 객체에는 서버에서 응답으로 보낸 문자열이 data 프로퍼티로 담겨 있다.

네트워크를 통해 전송되는 이벤트는 다음과 같다.

event: bid  // 이벤트 객체의 타입
data: GOOG  // 데이터 프로퍼티 설정
data: 999   // 뉴라인 다음에 데이터를 추가한다.
            // 빈 줄은 이벤트가 끝났음을 의미한다.

책 안에 EventSource를 사용한 채팅 클라이언트와 서버 코드가 있는데 이를 사용할 일이 있다면 나중에 한번 보면 좋을 것 같다.

웹소켓

웹소켓은 서버 전송 이벤트와 달리 이진 메시지를 지원하며 서버에서 일방적으로 보내는 게 아니라 양방향 통신이 가능하다.

웹소켓 생성, 연결, 연결 끊기

웹소켓 서버와 통신하려면 서버와 서비스를 나타내는 wss://URL을 전달해 웹소켓 객체를 만든다.

let socket = new WebSocket("wss://example.com/stockticker");

연결 프로세스는 자동으로 시작되지만 새로 생성한 웹소켓이 즉시 연결되지는 않는다.

소켓의 readyState 프로퍼티는 연결 상태를 나타내며 값은 다음 중 하나이다.

  • WebSocket.CONNECTING: 웹소켓을 연결하는 중이다.
  • WebSocket.OPEN: 웹소켓이 연결됐으며 통신할 수 있다.(open 이벤트 발생)
  • WebSocket.CLOSING: 웹소켓 연결을 끊는 중이다.
  • WebSocket.CLOSED: 웹소켓 연결이 닫혔으며 더는 통신할 수 없다.(close 이벤트 발생)

웹소켓 연결에 프로토콜 에러나 기타 에러가 일어나면 웹소켓 객체에서 error 이벤트를 발생시킨다.

책에서는 이 내용 이외에도 웹소켓으로 메시지 전송, 웹소켓 메시지 수신, 프로토콜 교섭과 같은 내용을 다룬다.

스토리지

이 장에서는 클라이언트 사이드 스토리지에 대해 설명하는데 아래의 것들을 다룬다.

  • 웹 스토리지
  • 쿠키
  • IndexedDB

웹 스토리지와 쿠키는 많이 보았던 개념이어서 IndexedDB와 관련한 내용들을 정리할 예정이다.

IndexedDB

IndexedDB는 관계형 데이터베이스가 아니라 객체 데이터베이스이며 SQL 쿼리를 지원하는 데이터베이스에 비해 훨씬 단순하다. localStorage와 마찬가지로 IndexedDB 데이터베이스는 포함하는 문서의 출처에 종속된다.

IndexedDB API의 개념은 단순하지만, API가 비동기적이라는 사실 때문에 복잡해진다.
→ 웹 애플리케이션이 브라우저의 UI 스레드를 차단하지 않기 위해!

미국 우편 번호와 도시를 연결하는 데이터베이스를 만들고 검색하는 예제

// 이 함수는 비동기적으로 데이터베이스 객체를 가져와서 콜백에 전달
// 필요하다면 데이터베이스를 생성하고 초기화!
function withDB(callback) {
  const request = indexedDB.open('zipcodes', 1); // 데이터베이스 버전 1을 요청
  request.onerror = console.error; // 에러 기록
  request.onsuccess = () => {
    // 끝났으면 이 함수를 호출
    const db = request.result; // 요청 결과는 데이터베이스
    callback(db); // 데이터베이스를 전달해 콜백을 호출
  };

  // 데이터베이스 버전 1이 존재하지 않으면 이 이벤트 핸들러가 호출
  // 이 핸들러는 데이터베이스를 처음으로 생성할 때 객체 저장소와 인덱스를 생성하고
  // 초기화할 목적으로, 또는 DB 스키마를 다른 버전으로 전환할 목적으로 사용
  request.onupgradeneeded = () => {
    initdb(request.result, callback);
  };
}

// 데이터베이스가 아직 초기화되지 않았으면 withDB()에서 이 함수를 호출
// 데이터베이스를 생성하고 데이터를 채운 다음 콜백 함수에 전달
//
// 우편 번호 데이터베이스에는 다음과 같은 형태의 객체를 저장하는 객체 저장소가 하나 있다.
//
// {zipcode: "02134", city: "Allston", state: "MA",}
//
// zipcode 프로퍼티를 데이터베이스 키로 사용하고 도시 이름을 인덱스로 사용
function initdb(db, callback) {
  // 저장소 이름을 지정하고 이 저장소의 기본 키가 될 프로퍼티 이름을 지정하는
  // '키 경로'가 포함된 설정 객체를 넘겨 객체 저장소를 생성
  const store = db.createObjectStore(
    'zipcodes', // 저장소 이름
    { keyPath: 'zipcode' },
  );

  // 도시 이름과 우편 번호를 객체 저장소의 인덱스로 만든다.
  // 이 메서드에는 키 경로 문자열을 설정 객체가 아닌 필수 인자로 직접 전달
  store.createIndex('cities', 'city');

  // 데이터베이스 초기화에 사용할 데이터를 가져옴
  fetch('zipcodes.json')
    .then((res) => res.json())
    .then((zipcodes) => {
      const transaction = db.transaction(['zipcodes'], 'readwrite');
      transaction.onerror = console.error;

      const store = transaction.objectStore('zipcodes');

      for (const record of zipcodes) {
        store.put(record);
      }
      transaction.oncomplete = () => {
        callback(db);
      };
    });
}

// 우편 번호를 받아 indexedDB API를 써서 비동기적으로 도시를 검색한 다음
// 도시를 찾으면 지정된 콜백에 우편 번호를 전달하고, 찾은 도시가 없으면 null을 전달
function lookupCity(zip, callback) {
  withDB((db) => {
    const transaction = db.transaction(['zipcodes']);
    const zipcodes = transaction.objectStore('zipcodes');
    const request = zipcodes.get(zip);

    request.onerror = console.error;
    request.onsuccess = () => {
      const record = request.result;
      if (record) {
        callback(`${record.city}, ${record.state}`);
      } else {
        callback(null);
      }
    };
  });
}

// 도시 이름을 받아 IndexedDB API를 써서 비동기적으로 우편 번호를 모두 검색
// 도시 이름은 대소문자를 구분하며 모든 주에서 검색
function lookupZipcodes(city, callback) {
  withDB((db) => {
    const transaction = db.transaction(['zipcodes']);
    const store = transaction.objectStore('zipcodes');

    const index = store.index('cities');

    const request = index.getAll(city);
    request.onerror = console.error;
    request.onsuccess = () => {
      callback(request.result);
    };
  });
}

워커 스레드와 메시지

웹 브라우저는 Worker 클래스의 싱글 스레드 요건을 아주 조심스럽게 완화한다. Worker 클래스의 인스턴스는 메인 스레드, 이벤트 루프와 동시에 실행되는 스레드이다. 워커는 독립된 실행 환경에서 실행되며 완전히 독립적인 전역 객체를 갖고, Window나 Document 객체에 접근하지도 않는다. 워커는 비동기 메시지 전달을 통해서만 메인 스레드와 통신할 수 있다.

워커를 만드는 것은 가벼운 작업은 아니다. 그러나 워커는 애플리케이션에서 이미지 처리 같은 계산 집약적인 작업을 할 때 유용하다.

워커 API의 두 부분

  • Worker 객체(워커를 생성하는 스레드에서 바라보는 일종의 인터페이스)
  • WorkerGlobalScope(새로운 워커의 전역 객체이며 워커 스레드 내부에서 바라보는 자기 자신)

Worker 객체

// 워커가 실행할 자바스크립트 코드를 URL로 전달하면서 Worker() 생성자를 호출한다.
let dataCruncher = new Worker("utils/cruncher.js");

// postMessage()를 통한 데이터를 전송
dataCruncher.postMessage("/api/data/to/crunch");

// 메시지 수신
dataCruncher.onmessage = function(e) {
  let stats = e.data;
  console.log(`Average: ${stats.mean}`);
}

Worker 객체 역시 표준인 addEventListener(), removeEventListener() 메서드를 지원하며 onmessage도 사용할 수 있다. 또한 Worker 객체에는 위에서 사용한 postMessage(), 워커 스레드를 강제 종료하는 terminate() 메서드만 있다.

워커의 전역 객체

Worker() 생성자로 새로운 워커를 생성할 때는 JS 코드 파일의 URL을 전달하며 이 코드는 워커를 생성한 스크립트에서 분리된, 새롭고 깨끗한 JS 실행 환경에서 실행된다.

이 객체에는 Worker 객체와 마찬가지로 postMessage() 메서드와 onmessage 이벤트 핸들러 프로퍼티가 존재하지만 동작 방향은 반대이다.

WorkerGlobalScope는 워커의 전역 객체이므로 JSON 객체, isNaN() 함수, Date() 생성자 등 코어 자바스크립트 전역 객체의 프로퍼티는 모두 가지고 있다. 또한 클라이언트 사이드 Window 객체의 프로퍼티도 일부 포함한다.

워커로 코드 가져오기

WorkerGlobalScope 에는 모든 워커에서 접근할 수 있는 importScripts() 전역 함수가 있다.

// 작업을 시작하기 전에 필요한 클래스와 유틸리티를 불러온다.
importScripts("utils/Histogram.js", "utils/BitSet.js");

워커 실행 모델

워커 스레드는 코드, 가져온 스크립트나 모듈을 동기적으로 위쪽에서 아래쪽으로 실행하며, 이벤트와 타이머에 응답할 때는 비동기 단계에 들어간다. 워커에서 등록한 message 이벤트 핸들러는 메시지 이벤트를 받을 가능성이 있을 때는 절대 종료되지 않는다.

반면 워커가 메시지를 주시하지 않으면 fetch() 프라미스나 타이머 등 대기 중인 작업을 모두 처리하고 작업 관련 콜백을 모두 호출할 때까지 실행된다.

등록된 콜백이 모두 호출되면 워커가 새로운 작업을 시작할 방법이 없으므로 스레드를 종료해도 안전하고, 따라서 자동으로 종료된다.

만델브로트 세트

위에서 다룬 워커와 메시지를 사용하여 계싼 집약적인 작업을 병렬화하는 예제를 소개한다.

만델브로트 세트는 복소평면에 존재하는 점들의 집합이며, 각 포이트는 복잡한 곱셈과 덧셈을 반복하고 벡터가 결합된 값이다. 만델브로트 세트를 고품질 이미지로 표현하려면 2억 5천만 번 가량의 복잡한 연산을 수행해야 하므로 반드시 워커를 사용해야 한다.

예제코드 링크

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글