자바스크립트 ES2022의 새로운 기능 살펴보기

ES2022의 새로운 기능들

TC39 위원회와 스펙

ECMA에 있는 TC39라는 위원회는 자바스크립트를 더 낫게 만들기 위한 공통된 목표 아래 다양한 배경을 지닌 사람들이 모인 놀랍고 헌신적인 그룹으로, “웹을 망치지 않는다“는 하나의 규칙을 갖고 있다고 합니다.

ECMAScript의 스펙에 추가되기 위해서는 모든 기능들이 마치 임상 실험과 같이 다음의 5단계를 거쳐야 하는데, 각각의 다음 단계로 진행하기 위해서는 각 단계의 특정 기준을 만족해야 하고, 최소 4단계 이상을 만족해야 됩니다.

Stage 0(제안)
새로운 기능이 위원회에 제기되기 위한 계획 단계이거나, 아직 다음 단계를 위한 합격 기준을 만족하지 못한 제안들로, 현재 0단계에 위치한 제안들은 이 곳에서 확인할 수 있습니다.
Stage 1(제안)
아직 기초 단계지만, 위원회가 정의된 API의 기본 구조를 점검할 여력이 있는 단계로, 현재 1단계에 위치한 제안들은 이 곳에서 확인할 수 있습니다.
Stage 2(초안)
제안의 핵심 부분과 문제들이 해결된 상태로, 이 기능들은 스펙의 다음 버전에 존재할 수 있는 기능들이고, 현재 2단계에 위치한 제안들은 이 곳에서 확인할 수 있습니다.
Stage 3(후보)
제안이 상세하게 검토되었으며, 반려될 여지가 더 이상 없는 상태로, 스펙에 추가되기 전 마지막 단계이며, 현재 3단계에 위치한 제안들은 이 곳에서 확인할 수 있습니다.
Stage 4(완료)
스펙에 추가된 상태로, 완료된 제안들은 이 곳에서 확인할 수 있습니다.

이전 스펙에 추가된 기능들 살펴보기

자바스크립트는 계속 진화하는 언어로 우리가 현재 사용하는 몇몇 기능들은 비교적 최근에 추가되었는데, 2015년 이후 부터 TC39는 매년 새로운 기능들을 추가하기로 결정했습니다. 다음은 2015년 이후 새롭게 추가된 기능들입니다.

ES2016
Array.protytype.includes()
Exponentation operator (**, **=): 거듭제곱 연산자
ES2017
Object.values / Object.entries
Trailing commas in function parameter lists and calls: 함수 파라미터에 후행 comma(,)를 붙이는 것
Async : async / await
Object.getOwnPropertyDescriptors()
String.prototype.padStart() / String.prototype.padEnd()
ES2018
Promise.prototype.finally
Rest and spread operators(…)
Asynchronous iteration
Improvements on Regular Expressions
ES2019
Array.prototype.flat()
Array.prototype.flatMap()
Object.fromEntries()
String.prototype.trimStart()
String.prototype.trimEnd()
Symbol.prototype.description
Optional catch binding
기존 기능들의 수정: JSON.stringify(), Function.prototype.toString(), Array.sort()
ES2020
String.prototype.matchAll()
dynamic imports
BigInt
Promise.allSettled()
globalThis
Optional Chaining Operator(?.)
Nullish coalescing operator(??)
ES2021
String.prototype.replaceAll()
Promise.any()
Underscore as a numeric separator
Logical assignment operators(&&=, ||=, ??=)
WeakRefs and Finalizers

ES2022에 추가된 기능 살펴보기

2022년 6월 22일, 제123회 ECMA 총회에서 ECMAScript 2022 언어의 Spec을 승인했는데, 이는 이제 공식적으로 표준이 되었음을 의미합니다.

1. Class Fields

Class Public Instance Fields & Private Instance Fields

ES2015 이후 우리는 생성자를 통해 필드를 정의할 수 있었습니다. 일반적으로 클래스 메서드의 외부에서 액세스하면 안 되는 필드에는 밑줄_이 붙지만, 이는 클래스를 사용하는 사람들을 막을 수 있는 방법이 아니었죠.

class ColorButton extends HTMLElement {
  constructor() {
    this.color = "red"
    this._clicked = false
  }
}

const button = new ColorButton()

// 공개 필드는 누구나 액세스하고 변경할 수 있음
button.color = "blue"

console.log(button._clicked) // false, 인스턴스에서 액세스할 수 있음
button._clicked = true // 오류가 발생하지 않고, 인스턴스에서 읽을 수 있음

Class Fields 기능은 클래스 내의 필드를 좀 더 명확하게 정의할 수 있게 해주는데, 생성자 내에 정의하는 대신 클래스의 최상단 레벨에 정의할 수 있습니다.

class ColorButton extends HTMLElement {
  color = "red"
  _clicked = false
}

또 Class Fields 기능을 사용하여 private 필드를 좀 더 안전하게 숨길 수 있는데, 밑줄을 붙이는 기존의 방식과는 달리 필드 이름 앞에 '#'을 붙여 외부의 액세스를 방지할 수 있습니다.

class ColorButton extends HTMLElement {
  // 모든 필드는 기본적으로 공개됨
  color = "red"

  // private 필드는 #으로 시작하며 클래스 내부에서만 변경 가능
  #clicked = false
}

const button = new ColorButton()

// 공개 필드는 누구나 액세스하고 변경할 수 있음
button.color = "blue"

// 구문 오류
console.log(button.#clicked) // 외부에서 읽을 수 없음
button.#clicked = true // 외부에서 값을 할당할 수 없음
Private instance methods and accessors

클래스의 몇몇 메소드나 변수는 클래스 내부적으로 기존에 의도했던 기능들을 수행해야 하는 중요도를 가지면서 외부에서는 접근할 수 없어야 하는데, 이를 위해 메소드나 접근자 앞에 '#'을 붙일 수 있습니다.

class Banner extends HTMLElement {
  // 외부에서 직접 접근할 수 없지만 내부 메서드로 수정할 수 있는 private 변수
  #slogan = "Hello there!"
  #counter = 0

  // private getter 및 setter(접근자)
  get #slogan() {return #slogan.toUpperCase()}
  set #slogan(text) {this.#slogan = text.trim()}

  get #counter() {return #counter}
  set #counter(value) {this.#counter = value}

  constructor() {
    super();
    this.onmouseover = this.#mouseover.bind(this);
  }

  // private method
  #mouseover() {
    this.#counter = this.#counter++;
    this.#slogan = `Hello there! You've been here ${this.#counter} times.`
  }
}
Static class fields and private static methods

정적 필드나 메소드는 프로토타입 내에서만 존재하도록 하는 데 있어 유용하지만, 주어진 클래스의 모든 인스턴스에 대해서는 그렇지 않기 때문에 필드와 메소드들이 클래스 내에서만 액세스할 수 있도록 허용할 수 있습니다.

ES2015 이후 정적 필드를 정의하기 위해서는 필드를 클래스 자체에 정의하는 방법을 사용해야 했습니다.

class Circle {}
Circle.PI = 3.14

하지만 이제는 static 키워드를 통해 정적 필드를 클래스 내부에 정의할 수 있습니다.

class Circle {
  static PI = 3.14
}

정적 필드 역시 클래스 필드와 메소드 처럼 '#'을 붙여 private하게 만들 수 있는데, 이 경우 오직 클래스 내부에서만 액세스할 수 있도록 만들어 주기 때문에 외부에서의 액세스를 방지할 수 있습니다.

class Circle {
  static #PI = 3.14

  static #calculateArea(radius) {
    return #PI * radius * radius
  }

  static calculateProperties(radius) {
    return {
      radius: radius,
      area: #calculateArea(radius)
    }
  }

}

// Public static method
console.log(Circle.calculateProperties(10)) // {radius: 10, area: 314}

// 구문 오류 - Private static field
console.log(Circle.PI)

// 구문 오류 - Private static method
console.log(Circle.calculateArea(5))

Ergonomic brand checks for Private Fields

public 필드의 경우 클래스에 존재하지 않는 필드에 접근을 시도하면 undefined가 반환되는데, 반면 private 필드는 undefined 대신 예외를 발생시키게 됩니다.

private 필드가 존재하는지 체크하는 방법은 클래스 내에서 접근했을 때 예외를 발생시키는지 여부를 확인하는 겁니다. 하지만 이 방법은 큰 단점을 가지는데, 존재하는 필드에 대한 잘못된 접근자로 인한 것과 같이 다른 이유로 인한 예외가 발생할 수도 있기 때문입니다.

이를 방지하기 위해서는 in 키워드를 사용해 private 속성과 메소드를 체크하는 방법을 사용할 수 있습니다.

class VeryPrivate {
  constructor() {
    super()
  }

  #variable
  #method() {}
  get #getter() {}
  set #setter(text) {
    this.#variable = text
  }

  static isPrivate(obj) {
    return (
      #variable in obj && #method in obj && #getter in obj && #setter in obj
    )
  }
}

RegExp Match Indices

정규식을 사용하면 문자열의 패턴을 찾을 수 있는데, Regexp.execString.matchAll 모두 match 된 리스트를 결과로 반환합니다.

Regexp.exec의 경우에는 결과를 하나하나 반환하기 때문에, null이 반환될 때까지 여러 번 실행해야 하는 반면, String.matchAll의 경우에는 모든 match에 대해 순회할 수 있도록 iterator를 반환하게 되는데, 이 때 결과에는 일치하는 문자의 전체 문자열과 괄호화된 하위 문자열, 입력 문자열 및 일치 항목의 0 기반 인덱스가 모두 포함됩니다.

const str = 'Ingredients: cocoa powder, cocoa butter, other stuff'
const regex = /(cocoa) ([a-z]+)/g
const matches = [...str.matchAll(regex)]

// 0: "cocoa powder", 1: "cocoa", 2: "powder"
// index: 13
// input: "Ingredients: cocoa powder, cocoa butter, other stuff"
console.log(matches[0])

// 0: "cocoa butter", 1: "cocoa", 2: "butter"
// index: 27
// input: "Ingredients: cocoa powder, cocoa butter, other stuff"
console.log(matches[1])

이런 결과는 원본 입력에서 전체 일치의 위치에 대해 많은 정보를 제공해 주지만 하위 문자열과 일치하는 인덱스에 대한 정보는 부족한데, 이때 새로운 /d를 사용하면, 일치한 그룹에 대한 시작과 끝 위치를 얻을 수 있습니다.

const str = 'Ingredients: cocoa powder, cocoa butter, other stuff'
const regex = /(cocoa) ([a-z]+)/gd
const matches = [...str.matchAll(regex)]

// 0: "cocoa powder", 1: "cocoa", 2: "powder"
// index: 13
// input: "Ingredients: cocoa powder, cocoa butter, other stuff"
// indices: [[13,25],[13,18],[19,25]]
console.log(matches[0])

// 0: "cocoa butter", 1: "cocoa", 2: "butter"
// index: 27
// input: "Ingredients: cocoa powder, cocoa butter, other stuff"
// indices: [[27,39],[27,32],[33,39]]
console.log(matches[1])

Top-level await

이전에 await는 오직 async 함수 내에서만 사용할 수 있었는데, 이 때문에 모듈의 최상단에서는 await를 쓸 수 없다는 문제가 존재했습니다.

하지만 이제 await는 모듈 최상단에서도 사용할 수 있기 때문에 import, fallback 등을 만들 때 매우 유용하게 활용될 수 있습니다.

// 원래는 구문 오류가 발생하지만, ES2022에서는 발생하지 않음
await Promise.resolve(console.log("????"))

awaited promise가 해결되기까지, 현재 모듈과 현재 모듈을 import하는 상위 모듈의 실행은 지원하지 않았지만, 형제 모듈은 동일한 순서로 실행될 수 있습니다.

error.cause

Error와 그 하위 클래스를 통해 현재 에러의 원인이 된 에러를 지정할 수 있습니다.

try {
  // Do something
} catch (otherError) {
  throw new Error("Something went wrong", { cause: otherError });
}

오류의 원인 err은 스택 추적에 표시되고, err.cause를 통해 접근할 수 있습니다. error.cause에 대한 추가 정보는 다음 페이지를 참고하세요.

인덱싱 가능한 값의 .at() 메서드

인덱싱 가능한 값에 .at() 메서드를 사용하면, 대괄호 연산자[]와 같이 지정된 인덱스의 요소를 읽을 수 있고, 대괄호 연산자와는 다르게 음수 인덱스를 지원할 수 있습니다.

> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'

또 다음과 같이 “인덱싱 가능한” 유형에서 .at() 메서드를 지원합니다.

  • string
  • Array
  • 모든 Typed Array 클래스: Uint8Array 등

인덱싱 가능한 값의 .at() 메서드에 대한 추가 정보는 다음 페이지를 참고하세요.

Object.hasOwn(obj, propKey)

Object.hasOwn(obj, propKey)는 객체 objpropKey 키가 있는 고유(비상속) 속성이 있는지 확인하는 안전한 방법을 제공합니다.

const proto = {
  protoProp: "protoProp",
};
const obj = {
  __proto__: proto,
  objProp: "objProp",
};

assert.equal("protoProp" in obj, true); // (A)
assert.equal(Object.hasOwn(obj, "protoProp"), false); // (B)
assert.equal(Object.hasOwn(proto, "protoProp"), true); // (C)

in은 상속된 속성(행A)을 검출하지만 Object.hasOwn()은 자체 속성(행B 및 C)만 알아내는 것에 주의해야 합니다.

Object.hasOwn()에 대한 추가 정보는 다음 페이지를 참고하세요.

답글 남기기