State of JavaScript 2021/2022

dante Yoon·2022년 1월 17일
1
post-thumbnail

State of JavaScript 2021/2022

언어

Proxy

(TMI) 예전 모 기업 면접에서 Proxy 를 이용해 런타임에서 타입 에러를 반환하는 모듈을 만들어보라는 요청을 받은 적이 있었다.

Proxy는 객체를 래핑해서 프로퍼티를 읽고 쓰는 작업을 할때 해당 작업을 가로채는 객체다. 프록시 객체는 [[ProxyHandler]] 라는 internal slot을 가지는데, 이 슬롯에는 proxy handler 함수 혹은 null이 올 수 있다.

proxy handler는 다음과 같이 생겼다.

let alphabets = Object.freeze(["a","b","c"]);

const proxy = new Proxy(alphabets, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return "unknown";
    }
  }
});

console.log(alphabets["zz"]) // unknown

프록시 객체는 proxy handler에 정의된 로직에 따라서만 target 객체를 변경 시켜야 한다. 프록시 객체 생성 이후 타겟 객체를 마음대로 수정한다면 런타임에서 예상치 못한 사이드 이펙트가 발생할 수 있다.

함께 읽기 좋은 글

Promise.allSelttled

MDN 문서에 잘 설명되어있는데,

Promise.allSettled() 메서드는 주어진 모든 프로미스를 이행하거나 거부한 후, 각 프로미스에 대한 결과를 나타내는 객체 배열을 반환합니다.

일반적으로 서로의 성공 여부에 관련 없는 여러 비동기 작업을 수행해야 하거나, 항상 각 프로미스의 실행 결과를 알고 싶을 때 사용합니다.

그에 비해, Promise.all()이 반환한 프로미스는 서로 연관된 작업을 수행하거나, 하나라도 거부 당했을 때 즉시 거부하고 싶을 때 적합합니다.

Promise.all의 경우 여러 개의 비동기 호출에 대한 응답 값을 가장 늦게 settled 된 promise의 타임에 맞춰 값을 가져온다. 모든 요청이 정상 값으로 응답되는 경우 개별 요청의 응답 값을 구조분해를 이용해 받을 수 있으나, 10개의 요청 중 1개의 요청에서 에러를 발생한다면 다른 9개의 값 또한 받아올 수 없다.

Promise.all은 resolve된 Promise 값들만 기대할 때,
Promise.allSettled는 resolve, reject 의 Promise 상태 값을 기대할 때 사용한다.

Private Fields

자바스크립트에서 클래스 필드들은 다른 언어들과 다르게 기본적으로 class modifier가 public한 성격을 띄며 인스턴스 생성 이후 클래스 인스턴스를 이용해 참조 및 값의 변경이 가능하다.

ES20에서 부터 # prefix를 붙여 private한 필드를 정의할 수 있다.

Numeric Separators

큰 금액의 돈을 표기해야 하는 경우, 우측 부터 3n의 자리 이후 흔히 ,를 이용해 가독성을 높여주게 표기하는 경우가 많은데, underscore 기호를 이용해 숫자를 표기하는 것을 말한다.

const value = 100000000;
const speratedValue = 100_000_000;


const octalValue  = 0o32_12;
const hexValue    = 0xff_55_00;
const binaryValue = 0b1010_1011_1111;

String.prototype.replaceAll()

MDN 문서에 잘 설명이 되어있는데, ECMA-262 12th(2021) 문서에서 자세히 살펴볼 수 있다.

자매품으로 String.prototype.replace 함수가 있는데, replace 함수는 target string의 값을 변경하지 않고 regex를 통해 변경된 문장을 새로운 값으로 반환한다.

let test = "dog dog dog horse dog dog horse dog dog dog  horse dog horse dog horse"
test.replace(/(dog)/g, "donkey");
// 'donkey donkey donkey horse donkey donkey horse donkey donkey donkey  horse donkey horse donkey horse'
test
// 'dog dog dog horse dog dog horse dog dog dog  horse dog horse dog horse'

근데 보다시피 /g 플래그를 사용하면 replaceAll을 사용할 필요가 없어서, 대단히 긴 문자열이나 동적인 값을 변수에 넣고 모든 부분을 변경하지 않는다면 필요할까 싶다.. (동적인 값도new RegExp(str1, "g") 이런 방법으로 해결된다.)

String.prototype.matchAll()

ECMA-262 12th(2021) 문서에서 자세히 살펴볼 수 있다.

replace, replaceAll의 상관 관계랑 약간 다르다.
replace, replaceAll은 타겟 값을 변경하지 않고 둘다 새로운 값을 반환하는 부분에서 동일하며 동일하게 문자열을 반환한다.

match는 주어진 regex에 만족하는 값을 배열로 반환하고 만족하는 값이 없다면 null을 반환한다. ReturnType<typeof match>string[] | null인 것이다.

matchAll은 RegExpStringIterator 을 반환하는데, 이 타입은 Iterator이다.

예제 코드를 보자. 예제 코드에 사용되는 regex는 Javascript Reference에서 가져와 일부분 수정했다.

var regexp = new RegExp("ta[a-z]*", "g");
var str = "talk tax tos";

// match를 사용해 변경 
str.match(regexp)
// ['talk', 'tax']
str.match(regexp).map(match => `일치 문자열 ${match}`)
// ['일치 문자열 talk', '일치 문자열 tax']

// matchAll을 사용해 변경
var result =Array.from(str.matchAll(regexp), match => `일치 문자열 ${match[0]}`);
// ['일치 문자열 talk', '일치 문자열 tax']

Logical Assignment

ES2021 proposal에서 확인해볼 수 있다. 루비 문법에서 영향을 받았다.

깃허브 문서의 예제코드를 보면, 인자의 키값에 baz가 없으면 반환 값의 키 값으로도 넣지 않길 원한다. 다음의 접근 방법에서는 opts.foo의 존재 여부와 상관없이 internal operation인 Setter가 호출된다.

function example(opts) {
  // Ok, but could trigger setter.
  opts.foo = opts.foo ?? 'bar'

  // No setter, but 'feels wrong' to write.
  opts.baz ?? (opts.baz = 'qux');
}

example({ foo: 'foo' })

logical assignment를 이용해 좀 더 간편하게 사용할 수 있다.
게다가 opts.foo가 존재하지 않는한 [[Setter]]를 불필요하게 호출하지 않는다.

function example(opts) {
  // Setters are not needlessly called.
  opts.foo ??= 'bar'

  // No repetition of `opts.baz`.
  opts.baz ??= 'qux';
}

example({ foo: 'foo' })

Promise.any()

MDN 문서에 잘 설명되어 있다.

Also, unlike Promise.race(), which returns the first settled value, this method returns the first resolved value. This method will ignore all rejected promises up until the first promise that resolves.

Promise는 두 가지의 상태 값을 가지고 있다. pending, settled 이다.
settled는 두 가지의 상태 값을 포함한다. fulfillment, rejected.

Promise.race는 첫번째 settled된 Promise의 상태 값이 resolved, rejected 된 값 중 어느 상태인지 상관 없이 처음 settled된 값을 반환하는 반면,

Promise.any는 첫번째 settled된 promise의 상태가 rejected여도, 두 번째로 settled된 값이 resolve 값이라면 두번째 resolved 값을 반환한다.

Array.prototype.at()

파이썬이었던 것 같은데, 특정 언어는 마이너스 인덱스를 참조할 경우 배열의 끝에서부터 역순으로 n번째 인덱스를 참조하여 해당 값을 반환해준다. 자바스크립트에서는 해당 기능이 아직 존재하지 않아 indexOf와 같은 다른 Array.prototype 값을 통해 일단 인덱스가 전체 배열 length보다 작고 0보다 큰지 유효성 검사를 확인 후 조회가 가능했는데, at을 통해 직관적으로 인덱스의 배열 값을 조회할 수 있다.

마지막 인덱스 값을 조회하기 위해서 다음의 선택권들이 주어지는 것이다.

// length - 1 을 이용한 조회 
const lastIndex= arr.length -1;
return lastIndex >= 0 ? arr[lastIndex] : undefined
// at을 이용한 조회
return arr.at(-1);

Top Level Await

크롬 89, Node v14.8.0 버전 부터 ES 모듈에서만 사용 가능하다.

따로 지면을 할애해서 설명해보고 싶은 주제이다.

과거 top level에서 async await문법을 사용해 비동기 동작을 처리하고 싶어하는 수요가 있었는데, 이를 Immediately Invoked Async Function Expression (IIAFE) 이용하거나 async await 문법 표기만을 위한 함수를 설정해 사용했었다.

let module;
(async () => {
	module = await import("https://personnel_cdn_module");
})();

export module;
let module;
async function main() {
  module = await import("https://personnel_cdn_module");
}
main();
export module;

하지만 위의 해결방안은 문법적인 규칙만 충족 시켜줄 뿐, promise가 settled되기 전에 해당 모듈을 참조할 경우 사용하는 쪽에서 undefined 를 참조하게 되어 Reference Error 가 발생할 것이다.

대안으로는 promise를 export하여 모듈을 사용하는 쪽에서 await, then 키워드를 사용하는 것이다. 근본적인 원인인 Reference Error는 해결 했으나 사용하는 사람의 입장에서 module의 사용을 항상 promise가 settled된 이후에 참조하는 것을 지켜야 한다는 것을 준수해야 하고 이 방법을 학습해야 한다.

// customModule.mjs
let module;
export default (async () => {
  const dynamic = await import("https://personnel_cdn_module");
  module = process(dynamic.default);
})

export { module};
// usage.mjs
import promise, {module} from "./customModule";
export function processWithModule(value) {
	return module(process);
}

let refinedModule;

promise.then(() => {
  refinedModule = processWithModule("123");
  console.log({refinedModule});
});

이에 대한 대안으로 module 또한 promise의 resolved 값으로 반환하는 것이다.

// sample.mjs
export default (async function () {
    const dynamic = await import("https://personnel_cdn_module");
	return { module: process(dynamic.default)};
})()
// usage.mjs
import promise from "./sample";

promise.then(({module}) => {
  ...
})

이렇게 promise based 의 해결책들이 있지만 Top Level Await을 사용하여 기존에 사용하던 await 문법을 적용할 수 있다.

// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);

브라우저 API

작성중..

Intl

웹 오디오 API

웹 애니메이션 API

Web Speech API

Page Visibility API

Broadcast Channel API

File System Access API

Web Share API

프레임워크

Solid

테스팅

Vitest

빌드도구

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글