The JavaScript Standard Library

Sets and Maps

// 이터러블을 인자로 받아요
let s = new Set('banana');
console.log(...s);

// Chaining이 가능합니다
s.add('a').add('b').add(true);
console.log(...s);

// 불 값을 리턴해요
console.log(s.delete(true), s.delete('c'));

// 초기화
s.clear();

원소끼리 비교할 때 ===와 같은 strict equality check를 합니다:

let s = new Set();

s.add(1).add('1');
console.log(...s);

s.add({ a: 1 }).add({ a: 1 });
console.log(...s);

console.log(s.has({ a: 1 }));

삽입 순서를 기억합니다:

let s = new Set();
for (let i = 10; i; i--) s.add(i);
console.log(...s);

집합 순회하기:

let s = new Set('banana');
for (let e of s) console.log(e);
// 보통 두번째 인자가 인덱스이지만 집합은 마땅한게 없으므로 원소를 한번 더 전달합니다
s.forEach((...arr) => console.log(arr));

Map의 생성자에는 [key, value] 쌍의 배열을 전달합니다:

let m1 = new Map([
  [1, 'one'],
  [2, 'two'],
]);

// 객체가 이미 있으면 아래와 같은 초기화도 가능합니다.
let o = { 1: 'one', 2: 'two' };
let m2 = new Map(Object.entries(o));

// 객체의 키 값은 문자열만 가능하기에 현재 m2의 모든 키는 문자열입니다.
// 이것이 객체와 Map의 차이점 중 하나입니다.
console.log(m1.has(1), m2.has(1), m2.has('1'));

사용법은 set 메서드를 제외하고 집합 자료구조와 아주 유사합니다:

let m = new Map();
m.set(1, 'one').set(2, 'two');
console.log(m.get('1'), m.get(1));

Map도 set과 마찬가지로 삽입 순서대로 순회합니다:

let m = new Map([...Array(5).keys()].map((x) => [x, 2 ** x]));

console.log(...m.keys());
console.log(...m.values());
console.log(...m.entries());

// ⚠️ value 다음 key입니다
m.forEach((...arr) => console.log(arr));

WeakMapWeakSet은 객체를 약하게 참조하기에 Map과 Set에서 참조한다는 이유로 객체가 GC(garbage collect)되지 않는 경우를 방지합니다. WeakMap의 키로는 객체만 사용할 수 있고 WeakSet에도 객체만 추가될 수 있습니다. 순회를 지원하지 않고 size 프로퍼티를 가지지 않습니다. swift의 weak 키워드가 생각나네요.

WeakMap의 사용 예시:

stackoverflow.comWhat are the actual uses of ES6 WeakMap?What are the actual uses of the WeakMap data structure introduced in ECMAScript 6? Since a key of a weak map creates a strong reference to its corresponding value, ensuring that a value which has ...

Typed Arrays and Binary Data

Typed array는 엄밀히 Array는 아니지만 대부분의 배열 메서드들을 구현합니다.

let length = 10;
console.log(Array.isArray(new Int8Array(length)));

Typed array의 모든 요소는 같은 타입의 숫자고, 배열의 길이를 바꿀 수 없으며(push등의 메서드 없음), 배열 생성시 0으로 초기화됩니다.

NameDescription
Int8Arraysigned bytes
Uint8Arrayunsigned bytes
Uint8ClampedArrayunsigned bytes with rollover
Int16Arraysigned 16-bit short integers
Uint16Arrayunsigned 16-bit short integers
Int32Arraysigned 32-bit integers
Uint32Arrayunsigned 32-bit integers
BigInt64Arraysigned 64-bit BigInt values(ES2020)
BigUint64Arrayunsigned 64-bit BigInt values(ES2020)
Float32Array32-bit floating-point value
Float64Array64-bit floating-point value: a regular JS number
let printBytes = (x) => console.log(x.BYTES_PER_ELEMENT);
// ✍️ 다른 생성자들로도 해보세요
printBytes(Uint8Array);

이름에 clamp가 붙은 배열은 오버플로우를 예쁘게 처리합니다:

let a = new Uint8Array(1);
let b = new Uint8ClampedArray(1);

// -1 = 0b11111111
a[0] = -1;
b[0] = -1;

// 냅다 비트단위로 자릅니다(wrap around).
console.log(a[0]);
// 최소 혹은 최대 값으로 처리합니다
console.log(b[0]);

일반 배열과 비슷한 생성자를 지원합니다:

let white = Uint8ClampedArray.of(255, 255, 255, 0);
let ints = Uint32Array.from(white);

// 범위를 벗어나면 잘라요
console.log(...Uint8Array.of(1.23, 2.99, 0b1111111100000011));

ArrayBuffer은 메모리 청크에 대한 opaque한(구체적인 구현이 없는) 참조에요. Opaque에 대해서는 아래 글을 참고하세요:

en.wikipedia.orgOpaque data type - Wikipedia

따라서 ArrayBuffer의 메모리를 사용하려면 다른 타입을 통해야해요:

// 1MB의 메모리를 할당합니다.
let buffer = new ArrayBuffer(1024 * 1024);
console.log(buffer.byteLength);

let asBytes = new Uint8Array(buffer);
console.log(asBytes.length);

let asInts = new Int32Array(buffer);
console.log(asInts.length);

// 두번째 인자는 시작 바이트 오프셋입니다.
// 마지막 1KB를 바이트로 읽기
let lastK = new Uint8Array(buffer, 1023 * 1024);
console.log(lastK.length);

// 세번째 인자는 배열 요소의 개수입니다.
// 두번째 1KB를 int로 읽기
let ints2 = new Uint8Array(buffer, 1024, 256);
console.log(ints2.length);

모든 typed array는 기저에 ArrayBuffer가 있으며 buffer 프로퍼티로 접근할 수 있습니다. ArrayBuffer를 통해 하나의 버퍼를 여러 typed array로 바라볼 수 있어요.

set 메서드는 요소들을 다른 typed array로 복붙합니다:

let bytes = new Uint8Array(12);
let pattern = new Uint8Array([1, 2, 3, 4]);

bytes.set(pattern);
console.log(...bytes);

bytes.set(pattern, 4);
console.log(...bytes);

bytes.set([0, 1, 2, 3], 8);
console.log(...bytes);

slice는 새롭게 생성된 배열을 반환하지만 subarray는 기존 배열을 참조합니다:

let arr = new Int16Array([0, 1, 2, 3]);
let copy = arr.slice(2, 4);
let ref = arr.subarray(2, 4);

arr[2] = -9;

console.log(copy[0], ref[0]);
console.log(copy.buffer === arr.buffer, ref.buffer === arr.buffer);
console.log(copy.byteOffset, ref.byteOffset);
console.log(copy.byteLength, ref.byteLength);
console.log(copy.buffer.byteLength, ref.buffer.byteLength);

성능을 위해 typed array에서는 하드웨어의 엔디언을 따라갑니다.

ko.wikipedia.org엔디언 - 위키백과, 우리 모두의 백과사전

let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
console.log(littleEndian ? '리틀 엔디안 컴퓨터네요!' : '빅 엔디안 컴퓨터네요!');

네트워크등 외부에서 온 데이터는 엔디언이 다를 수 있습니다. 이때 DataView 객체를 활용합니다:

let bytes = new Uint8Array(16);
bytes[0] = 1;

let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);

// big-endian
console.log(view.getUint16(0, false));
// little-endian
console.log(view.getUint16(0, true));

// big-endian 형식으로 1을 write
view.setUint16(0, 1, false);
console.log(view.getUint16(0, false));
console.log(view.getUint16(0, true));

Typed array와 DataView를 활용해 압축 데이터나 이미지 데이터 등등 이진 데이터를 처리할 수 있어요.

Pattern Matching with Regular Expressions

재미없어서 다음에...

Dates and Times

Date 객체 만들기:

// 현재 시각
console.log(new Date());

// 1970년부터 지난 밀리초
console.log(new Date(0));

// 여러 숫자를 건네줄 수 있습니다.
// 인자 순서대로 fullyear, month, date, hours, minutes, seconds, milliseconds입니다.

// 플랫폼의 시간대를 사용합니다.
console.log(new Date(2100, 0, 1, 2, 3, 4, 5));
// UTC 기준으로 하고싶으면 UTC static method를 활용해 우선 ms로 변환합니다.
console.log(new Date(Date.UTC(2100, 0, 1, 2, 3, 4, 5)));

// 생성자에 문자열을 건네줄 수 있습니다.
// 적어도 toString, toUTCString, toISOString의 반환값은 지원합니다.
console.log(new Date('2100-01-01T00:00:00Z'));

여담으로 month가 zero-based인 이유:

stackoverflow.comWhy does the month argument range from 0 to 11 in JavaScript's Date constructor?When initializing a new Date object in JavaScript using the below call, I found out that the month argument counts starting from zero. new Date(2010, 3, 1); // that's the 1st April 2010! Why doe...

JS는 내부적으로 timestamp를 사용해 날짜를 표현합니다:

// date 객체에 대응하는 timestamp
console.log(new Date(10).getTime());

// 현재 시간의 timestamp
console.log(Date.now());

// 문자열에서 timestamp 파싱
console.log(Date.parse('1970-01-01T00:00:00Z'));

// 소숫점 단위까지 나타내며
// 특정 이벤트(아래 링크 참고) 이후 시간이 얼마나 지났는지를 나타냅니다.
console.log(performance.now());

developer.mozilla.orgPerformance: now() method - Web APIs | MDNThe performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and ServiceWorker contexts).

date 객체에 시간을 더하는 방법:

let d = new Date();
// overflow가 있더라도 잘 처리됩니다.
d.setMonth(d.getMonth() + 3, d.getDate() + 14);
console.log(d);

날짜를 문자열로 표현하는 메서드들:

let d = new Date(0);

// locale time zone / not user locale aware
console.log(d.toString());
console.log(d.toDateString());
console.log(d.toTimeString());

// locale time zone / user locale aware
console.log(d.toLocaleString());
console.log(d.toLocaleDateString());
console.log(d.toLocaleTimeString());

// UTC time zone / not user locale aware
console.log(d.toUTCString());

// UTC time zone / ISO-8601 standard
console.log(d.toISOString());

None of these date-to-string methods is ideal when formatting dates and times to be displayed to end users

열심히 봤는데 이렇게 말한다고?

Error Classes

JS에서는 아무 값이나 throw할 수 있습니다.

try {
  // ✍️ 문자열도 던져보세요
  throw 1;
} catch (e) {
  console.log(typeof e);
}

그래도 일반적으로는 Error를 상속받은 객체를 던집니다. Error는 생성될 시점의 스택 상태를 가지고있기에 디버깅에 용이합니다. 던져질 시점이 아닌 생성될 시점임을 조심하세요:

let e = new Error();

let f = () => {
  // ✍️ 주석을 해제하고 차이를 확인해보세요.
  // throw new Error();
  throw e;
};

let g = () => {
  f();
};

try {
  g();
} catch (e) {
  // 비표준 기능이지만 모든 주요 JS 엔진에 구현되어있습니다.
  // 스택 문자열의 정확한 내용에 의존할 수는 없지만 디버깅 목적으로는 사용할 수 있습니다.
  console.log(e.stack);
}

에러 객체는 message와 name 프로퍼티를 가집니다:

class MyError extends Error {
  name = 'MyError';
}

let e = new MyError('my message');
console.log(e.name, e.message);
console.log(e.toString());

Error 클래스를 상속받은 여러 빌트인 에러 클래스들이 있습니다:

developer.mozilla.orgError - JavaScript | MDNError objects are thrown when runtime errors occur. The Error object can also be used as a base object for user-defined exceptions. See below for standard built-in error types.

JSON Serialization and Parsing

자료 구조를 바이트/문자 스트림으로 바꾸는 과정을 serialization(marshaling, pickling)이라 합니다.

JSON.stringify의 세번째 인자로 indentation을 설정할 수 있습니다.

let o = { a: 1, b: 2 };
console.log(JSON.stringify(o, null, 2));
console.log(JSON.stringify(o, null, '    '));

stringify에서 지원하지 않는 값을 만나면 해당 값의 toJSON 메서드를 활용합니다.

let date = new Date();
let o = { date };

console.log(JSON.stringify(o));

date.toJSON = () => 'my implementation';
console.log(JSON.stringify(o));

위 예제처럼 date 객체는 문자열로 변환되기에 다시 JSON.parse를 하더라도 date 객체로 돌아오지 않습니다:

console.log(typeof JSON.parse(JSON.stringify(new Date())));

이럴 때 reviver라고 불리는 parse의 두 번째 인자를 활용합니다:

let date = new Date();

let reviver = function (key, value) {
  if (key[0] === '_') return undefined;
  if (key === 'date') return new Date(value);
  return value;
};

// ✍️ 언더스코어가 없는 프로퍼티를 추가해보세요
let o = { date, _unused: 1 };
let o2 = JSON.parse(JSON.stringify(o), reviver);

console.log(o2);
console.log(o2.date instanceof Date);

stringify의 두번째 인자에 문자열 배열을 전달하면 결과 문자열에 어떤 키를 어떤 순서로 넣을지를 결정합니다. 이런식으로 포맷을 고정시키는게 테스트 짤 때 유용하다고 하네요:

let o = { 1: 'one', 2: 'two', 3: 'three' };
console.log(JSON.stringify(o, [3, 1]));

배열 대신 함수를 전달하면 replacer 함수라고 부르며 reviver 함수의 정반대 역할을 합니다:

let replacer = function (key, value) {
  if (key === '2') return undefined;
  if (key === '1') return value + '!';
  return value;
};

let o = { 1: 'one', 2: 'two', 3: 'three' };
console.log(JSON.stringify(o, replacer));

The Internalization API

숫자 포맷팅하기:

// ✍️ 옵션들을 바꿔보세요
let format = Intl.NumberFormat('ko', {
  style: 'currency', // 'decimal' | 'currency' | 'percent'

  // 스타일이 currency일 때 유효
  currency: 'KRW',
  currencyDisplay: 'name', // 'symbol' | 'code' | 'name'

  // 숫자 사이 separator(쉼표 등) 관련
  useGrouping: true,

  // 최대/최소 숫자 개수 관련
  minimumIntegerDigits: 5,
  minimumFractionDigits: 1,
  maximumFractionDigits: 4,

  // scientific notation 관련.
  // 위에 있는 세 프로퍼티를 덮어씁니다.
  minimumSignificantDigits: undefined,
  maximumSignificantDigits: undefined,
}).format;

console.log(format(1234.56789));

locale을 override하는 방법이 있다는데 자세한건 아래 문서를 읽어보세요:

developer.mozilla.orgIntl - JavaScript | MDNThe Intl namespace object contains several constructors as well as functionality common to the internationalization constructors and other language sensitive functions. Collectively, they comprise the ECMAScript Internationalization API, which provides language sensitive string comparison, number formatting, date and time formatting, and more.

let hindi1 = Intl.NumberFormat('hi-IN').format;
let hindi2 = Intl.NumberFormat('hi-IN-u-nu-deva').format;
console.log(hindi1(1234567890));
console.log(hindi2(1234567890));

Intl.DateTimeFormat으로 날짜와 시간을 포맷팅할 수 있어요. 연도를 제외하는 등 Date 클래스의 toLocaleDateString()이나 toLocaleTimeString()보다 세밀하게 조절할 수 있습니다.

let d = new Date();

// ✍️ en-US로 바꿔보세요
let locale = 'ko-KR';

// ✍️ 옵션을 바꿔보세요
let options = {
  // 아래 세 개는 'numeric'과 '2-digit'을 지원합니다
  year: 'numeric',
  month: 'numeric', // 추가로 'long' | 'short' | 'narrow'
  day: 'numeric',

  // 'long' | 'short' | 'narrow' 를 지원합니다.
  weekday: 'long',

  // 아래 세 개는 'numeric'과 '2-digit'을 지원합니다
  hour: 'numeric',
  minute: '2-digit',
  second: 'numeric',

  timeZone: 'Asia/Seoul',
  timeZoneName: 'short', // 'short' | 'long'

  hour12: true,
};

console.log(Intl.DateTimeFormat(locale, options).format(d));

사용할 달력 체계를 명시할 수도 있어요:

let date = new Date();
// era는 아주 옛날 날짜나 일본 달력등을 사용할 떄 활용됩니다.
let options = { year: 'numeric', era: 'short' };

console.log(Intl.DateTimeFormat('ko-KR', options).format(date));
console.log(Intl.DateTimeFormat('ko-KR-u-ca-buddhist', options).format(date));
console.log(Intl.DateTimeFormat('ko-KR-u-ca-japanese', options).format(date));

영어가 아닌 언어의 문자열 정렬을 Intl.Collator로 할 수 있습니다:

// ✍️ numeric을 바꿔보세요
let compareFileName = new Intl.Collator('ko-KR', {
  // 숫자를 포함한다면 이를 고려합니다.
  numeric: true,
}).compare;

console.log(['page10', 'page9'].sort(compareFileName));

// ✍️ sensitivity를 바꿔보세요
let matcher = new Intl.Collator('ko-KR', {
  // 'base'    case 무시 accents 무시
  // 'accent'  case 무시 accents 고려
  // 'case'    case 고려 accents 무시
  // 'variant' case 고려 accents 고려
  sensitivity: 'base',

  // 공백과 구두점 무시 여부
  ignorePunctuation: true,
}).compare;

console.log(matcher('à', 'a') === 0);
console.log(matcher('e', 'E') === 0);
console.log(matcher('i', '  i  ') === 0);

The Console API

블로그 컴포넌트에 Console API가 구현되어있지 않으니(ㅠ) 브라우저 콘솔탭에 붙여넣기해서 실행해보세요.

console.log('log');

console.debug('debug');
console.info('info');
console.warn('warn');
console.error('error');

console.trace('trace');

여러 로그를 묶을 수 있어요:

console.group('group');
console.log('1');
console.log('2');
console.groupEnd();

console.groupCollapsed('groupCollapsed');
console.log('1');
console.log('2');
console.groupEnd();

브라우저에서도 assert가 가능합니다:

console.assert(1 === 1, 'assert');
console.assert(1 === 2, 'assert');

표도 그릴수 있네요!!:

console.table({ x: 1, y: 2 });
console.table({ x: 1, y: { z: 2 } });
console.table(
  [
    { x: 1, y: 3 },
    { x: 2, y: 2 },
    { x: 3, y: 2 },
  ],
  // ✍️ 주석을 해제해보세요
  // 'x',
);

호출 횟수를 기록할 수도 있어요. 이벤트 핸들러 디버깅할 때 유용하다고 하네요:

console.count('count');
console.count('count');
console.countReset('count');
console.count('count');

실행 시간을 기록할 수 있어요:

console.time('time');
console.timeLog('time');
console.timeEnd('time');

콘솔을 초기화할 수 있습니다:

console.clear();

C언어처럼 포맷을 정할 수도 있어요:

console.log('%s %d %f', 'str', 123.45, 123.45);
console.log(
  '%c red %c blue',
  'color: red; font-size: 16px',
  'color: blue; font-size: 32px',
);

URL APIs

let url = new URL('https://example.com:3000/path/file?query=content#fragment');

console.log(url.href);
console.log(url.origin);
console.log(url.protocol);
console.log(url.host);
console.log(url.hostname);
console.log(url.port);
console.log(url.pathname);
console.log(url.search);
console.log(url.hash);

origin을 제외하고는 쓰기가 가능합니다. 결과는 hreftoString()으로 볼 수 있어요:

let url = new URL('http://a.xyz');

url.protocol = 'https';
url.hostname = 'example.com';
url.port = 3000;
url.pathname = '/path/file';
url.search = '?query=content';
url.hash = '#fragment';

console.log(url.href);
console.log(url.toString());

search 프로퍼티는 단순 문자열이라 수정하기 어려운데, 수정을 돕는 읽기 전용 searchParams 프로퍼티가 있습니다:

let url = new URL('https://example.com');

url.searchParams.append('key1', 'val1');

// 같은 키를 여러번 추가할 수 있어요
url.searchParams.append('key2', 'val2');
url.searchParams.append('key2', 'val3');

// 해당 키가 여러개면 모두 교체됩니다.
url.searchParams.set('key2', 'val4');
url.searchParams.append('key2', 'val5');

url.searchParams.append('key3', 'val6');
url.searchParams.delete('key3');

console.log([...url.searchParams]);
console.log(url.toString());

console.log(url.searchParams.has('key1'));
console.log(url.searchParams.get('key2'));
console.log(url.searchParams.getAll('key2'));

정렬도 시켜주네요 이렇게 친절할수가:

let url = new URL('https://example.com');

url.searchParams.append('z', 'zz');
url.searchParams.append('a', 'zz');
url.searchParams.append('aa', 'zz');
console.log(url.href);

url.searchParams.sort();
console.log(url.href);

searchParams를 통하지 않고 아래처럼 적용할 수도 있어요:

let url = new URL('http://example.com');

let params = new URLSearchParams();
params.append('key1', 'val1');

url.search = params;
console.log(url.toString());

URL 문자열을 인코딩하기 위해 아래 함수들을 사용할 수 있어요.

let str = 'https://example.com:3000/path/file?query=content#fragment';

// /, ?, #와 같은 URL separator 문자를 escape하지 않습니다.
console.log(encodeURI(str));

// URL separator 문자를 escape합니다.
console.log(encodeURIComponent(str));

두 함수에 관련해서 읽어볼만한 아티클:

unixpapa.comJavascript Madness: Query String Parsing

Timers

// id의 타입은 브라우저와 노드에서 다릅니다.
// opaque value로 취급하세요.
let id = setTimeout(() => console.log('boom'), 1000);

Microtask, macrotask 등등 타이머 관련해서 다룰 내용이 많지만 이후 챕터에서 더 살펴볼게요.