<figure class="image image_resized" style="width:100%;"><a href="https://www.wishket.com/crsr/?next=/w/EaN4AhXVQN/&referer_type=7110000705"><img src="https://yozm.wishket.com/media/news/1570/%EC%9C%84%EC%8B%9C%EC%BC%93_%EC%A0%84%ED%99%98_%EB%B0%B0%EB%84%88.png"></a></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">정보 통신에 대한 표준을 관리하는 비영리기구 Ecma 인터내셔널이 지난 6월 22일, <a href="https://www.ecma-international.org/news/ecma-international-approves-new-standards-6/">ECMAScript의 제13판을 최종 승인</a>했습니다. ECMAScript는 2015년의 제6판을 기점으로 매년 새롭게 발표가 되고 있는데요, 따라서 이번에 발표된 ECMAScript 제13판은 ECMAScript 2022 또는 ES2022이라는 이름으로도 불립니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">자바스크립트(JavaScript)는 ECMAScript를 준수하는 언어이기 때문에, 이번 발표는 JavaScript 사양의 변경으로도 해석할 수 있습니다. 사실 표준이 되기 전에도 이미 대부분의 브라우저가 이를 지원하고 있었기에 여러분도 이미 해당 제안(Proposal)을 써봤을 수도 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">저처럼 JavaScript를 주력 언어로 쓰시는 분들께는 이러한 ECMAScript의 발표가 굉장히 흥미로운(?) 이벤트인데요, 그래서 오늘은 ECMAScript 2022를 기점으로 새롭게 표준이 된 제안을 정리해보고자 합니다. 혹시라도 ECMAScript와 JavaScript가 정확히 어떤 것을 의미하는지 헷갈리시는 분들은 『<a href="https://wormwlrm.github.io/2018/10/03/What-is-the-difference-between-javascript-and-ecmascript.html">JavaScript와 ECMAScript는 무슨 차이점이 있을까?</a>』를 참고해주세요.</p><p style="text-align:justify;"> </p><figure class="image image_resized" style="width:100%;"><img src="https://yozm.wishket.com/media/news/1570/image001.png" alt="ecma 2022"><figcaption>따끈따끈한 새 표준</figcaption></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">이번 포스트를 통해 ECMAScript 2022가 궁금하신 분들께 도움이 되었으면 좋겠습니다.</p><div class="page-break" style="page-break-after:always;"><span style="display:none;"> </span></div><h3 style="text-align:justify;"><strong>클래스 필드</strong></h3><p style="text-align:justify;">첫 번째 항목은 바로 클래스 필드와 관련된 개선입니다. 클래스 필드는 <strong>접근 제어자가 부실한 JavaScript의 태생적 한계를 극복하고, 클래스 선언과 관련된 코드들의 가독성과 지역성(locality)을 높이기 위해</strong> 제안되었습니다. ES6부터 JavaScript에서도 객체 지향 프로그래밍의 클래스 개념을 적용할 수 있도록 class 키워드가 추가되었는데요. 애초에 JavaScript 자체가 객체 지향 프로그래밍을 위한 언어가 아니다 보니 최초 설계가 완벽하지는 않았습니다. 그래서 클래스 필드와 관련된 여러 부분의 개선이 필요했고 이들 중 일부가 표준으로 지정되었습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">클래스 필드라는 이름으로 뭉뚱그려 표기하긴 했지만, 여기에는 세 가지 세부 항목이 있습니다.</p><ul><li style="text-align:justify;">언어 자체에서 지원하는 프라이빗 접근 제어자 추가</li><li style="text-align:justify;">퍼블릭 필드 및 정적 필드 선언 방식 개선</li><li style="text-align:justify;">정적 초기화 블록 추가</li></ul><p style="text-align:justify;"> </p><p style="text-align:justify;">아무래도 글로 적는 것보다는 코드를 보는 게 나을 것 같네요. 기존 방식의 클래스 선언 코드를 봅시다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">class OldClass { constructor() { // 기존에는 생성자 내부에 this 를 사용하여 퍼블릭 필드를 설정했다. this.publicField = 0; // 기존에는 프라이빗 필드를 자체를 언어에서 지원하지 않았다. // 변수 명 앞에 언더스코어를 붙이는 관행이 있었지만 실제로는 퍼블릭하게 접근 가능하다. this._privateField = '비밀?'; } // 메서드는 클래스 바디에서 선언한다. publicMethod() {} // 프라이빗 메서드...로 선언한 것 같지만 실제로는 퍼블릭 메서드다. _privateMethod() {} } // 원래 정적 필드와 메서드는 이렇게 클래스 외부에서 설정해야 했다. OldClass.staticField = '정적 필드'; OldClass.staticMethod = function () {}</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">뭔가 중구난방이죠? 새 문법을 이용하면 더 깔끔하게 작성할 수 있습니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">class NewClass { // 새로 추가된 문법에서는 클래스 내부에 바로 퍼블릭 필드를 선언할 수 있다. publicField = 0; publicMethod() {} // 새로 추가된 문법에서는 언어 자체에서 프라이빗을 지원한다. // 변수, 메서드 이름 앞에 해시(#)를 붙이면 된다. #privateField = '비밀!'; #privateMethod() {} // getter와 setter도 프라이빗하게 만들 수 있다. get #privateGetter() {} set #privateSetter(value) {} // 정적 필드도 클래스 바디 안에 작성할 수 있다. static staticField = '정적 필드'; static staticMethod() {} // 위의 것들을 막 조합할 수도 있다. static #staticPrivateField = '정적 프라이빗 필드!'; static #staticPrivateMethod() {} static get #staticPrivateGetter() {} static set #staticPrivateSetter(value) {} // 정적 초기화 블록을 선언할 수 있다. static { console.log('정적 초기화 블록에서 딱 한 번만 실행됨'); window.getPrivateField = (myClass) => myClass.#privateField; } }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">우선 JavaScript 언어에서 지원하는 프라이빗 필드를 사용할 수 있게 됐습니다. 혹시라도 학부생 때 객체 지향 프로그래밍 수업을 들어보셨다면 <span style="color:#e52929;"><code>private</code></span>, <span style="color:#e52929;"><code>protected</code></span>, <span style="color:#e52929;"><code>public</code></span>과 같은 **접근 제어자(Access Modifier)**를 들어보셨을 텐데요, 이번 표준에서는 필드 이름 앞에 해시(#) 접두사를 붙여 프라이빗 필드를 만들 수 있습니다. 혹시라도 접근 제어자를 명시적으로 써본 기억이 있다면, 그건 아마 ‘<a href="https://www.typescripttutorial.net/typescript-tutorial/typescript-access-modifiers/">타입스크립트(TypeScript)</a>’에서 지원해주는 것일 겁니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">또한 클래스 바디에 직접 퍼블릭 필드와 정적 필드를 명시할 수 있습니다. 기존에는 퍼블릭 필드를 설정할 때 생성자 내에서 <span style="color:#e52929;"><code>this.</code></span>를 이용해 필드를 입력해야 했고, 정적 필드를 설정할 때는 클래스 밖에서 점 표기법으로 속성을 설정해야 했습니다. 새 표준에서는 두 방식의 필드 모두 클래스 바디 내에서 바로 선언이 가능합니다. 물론 정적 필드는 앞에 <span style="color:#e52929;"><code>static</code></span> 키워드를 붙여야 합니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_static_initialization_blocks">정적 초기화 블록</a>은 생성자와 비슷하면서도 약간 다릅니다. 생성자는 각 인스턴스가 생성될 때마다 실행되는 코드지만, 정적 초기화 블록은 클래스가 선언될 때 딱 한 번만 실행됩니다. 일반적으로 어떤 연산을 통해 정적 필드의 값을 초기화하는 용도로 주로 사용됩니다. 정적 초기화 블록은 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_static_initialization_blocks#multiple_blocks">선언된 순서에 따라 실행되며, 여러 개가 올 수도 있습니다.</a> 또한 상속된 클래스의 경우는 부모의 코드 블록이 먼저 실행되고, 자식의 코드 블록이 다음으로 실행됩니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">정적 초기화 블록 문법을 이용하면 클래스와 관련된 모든 코드를 클래스 바디 내부에 담을 수 있으며, 해당 블록 내에서는 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_static_initialization_blocks#access_to_private_fields">프라이빗 필드에 접근이 가능</a>하다는 장점이 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><span style="color:#e52929;"><strong>in</strong></span> <strong>연산자를 활용한 프라이빗 필드 체크</strong></h3><p style="text-align:justify;">클래스에서 프라이빗 필드를 <span style="color:#e52929;"><code>in</code></span>연산자로 체크하는 기능은 좀 더 가독성 좋은 코드를 위한 문법으로 확장된 경우입니다. 사실 클래스 필드와 연관된 것이긴 한데, 문서가 분리되어 있어 별도로 작성했습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">기존에도 <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/in">객체에서 특정 속성의 존재 여부를 검사하는 in 연산자</a>가 있었지만 이를 이용해 클래스의 프라이빗 필드의 존재 여부를 체크할 수 있게 된 점에서 의의가 있습니다. 기존 방식으로 코드를 작성하려면 <span style="color:#e52929;"><code>try</code></span>, <span style="color:#e52929;"><code>catch</code></span>문을 사용해야 하지만, in 연산자를 이용하면 보다 직관적인 문법으로 사용이 가능합니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">class OldClass { #field; // 프라이빗 필드에 접근하려 할 때 예외가 발생하는 것을 이용해야 한다. static isMyField(myClass) { try { myClass.#field; return true; } catch { return false; } } } class NewClass { #field; static isMyField(myClass) { // `in` 연산자를 쓰면 편-안. return #field in myClass; } }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">당연하게도 <span style="color:#e52929;"><code>in</code></span>연산자는 프라이빗 필드가 있는지의 여부만 반환하며, 그 값은 명시적인 메서드를 통해 반환하지 않는 한 외부에서 읽을 수 없습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><strong>정규표현식 플래그</strong> <span style="color:#e52929;"><strong>d</strong></span></h3><figure class="image image_resized" style="width:100%;"><img src="https://yozm.wishket.com/media/news/1570/image003.png" alt="정규표현식 플래그"><figcaption><span style="color:#e52929;"><code>d</code></span> 플래그를 추가하면 일치하는 인덱스 정보(<span style="color:#e52929;"><code>indices</code></span>)를 함께 반환해준다</figcaption></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">정규표현식 플래그로 새롭게 추가된 d 옵션은 매칭된 문자열의 인덱스 정보를 얻기 위해 추가된 속성입니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"><span style="color:#e52929;"><code>RegExp</code></span> 객체는 <span style="color:#e52929;"><code>exec</code></span>, <span style="color:#e52929;"><code>match</code></span> 등의 메서드를 호출할 때 매칭된 문자열의 정보를 제공하는데, 기존에는 시작 인덱스 속성(<span style="color:#e52929;"><code>index</code></span>)만 조회할 수 있었습니다. 매칭된 문자열의 종료 인덱스를 알 수가 없었기 때문에 일치하는 문자열을 인덱스 기반으로 조작하기가 어려웠습니다. 하지만 이번에 새로 표준이 된 플래그를 사용하면 시작 인덱스와 종료 인덱스가 함께 반환되기 때문에 더욱 쉬운 접근이 가능해졌습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"><span style="color:#e52929;"><code>RegExp</code></span> 객체에서 <span style="color:#e52929;"><code>d</code></span> 플래그를 추가한 채 <span style="color:#e52929;"><code>exec</code></span>, <span style="color:#e52929;"><code>match</code></span> 메서드를 호출하면, 매칭된 문자열의 시작, 종료 인덱스 배열을 포함하고 있는 2차원 배열 <span style="color:#e52929;"><code>indices</code></span>가 반환 값에 추가됩니다. 괄호를 이용한 캡처링 그룹이 있다면 해당 그룹의 인덱스 배열도 포함됩니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">/(a+)(b+)/d.exec('aaaabb'); // ['aaaabb', 'aaa', 'bb', indices: [[0, 6], [0, 4], [4, 6]]] // indices의 첫 번째 배열은 전체 문자열에 대한 시작, 종료 인덱스 // 두 번째와 세 번째 배열은 괄호로 묶여진 각 캡처링 그룹(괄호)의 시작, 종료 인덱스</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><strong>모듈에서 최상위 레벨의</strong> <span style="color:#e52929;"><strong>await</strong></span> <strong>호출 가능</strong></h3><p style="text-align:justify;">최상위 레벨 <span style="color:#e52929;"><code>await</code></span>호출은, JavaScript 모듈 파일 내에서 <span style="color:#e52929;"><code>async</code></span>함수를 선언하지 않고도 <span style="color:#e52929;"><code>await</code></span>키워드를 사용하여 비동기 호출을 가능하게 만든 문법의 기능 확장입니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">기존의 모듈 내에서 비동기 함수를 최상위 레벨에서 호출하기 위해서는 <span style="color:#e52929;"><code>async</code></span>, <span style="color:#e52929;"><code>await</code></span>함수를 만들거나 즉시 실행 함수(IIFE)를 만들어 호출해야 했는데요, 이러한 방법으로는 모듈을 호출하는 쪽에서 값이 초기화되기 전에 변수 접근이 가능하다는 문제가 있었습니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">// 우선 모듈 내에서만 동작하기 때문에 확장자는 `*.mjs` 형태여야 한다. let result; // 기존에는 비동기 함수를 쓰려면 이렇게 async 함수 자체를 선언해 호출하거나 async function getUser() { const response = await fetch('http://example.com/foo.json'); result = await response.json(); } getUser(); export { result }; // 즉시 실행 함수를 호출해야 했다. (async () => { await getUser(); })(); export { result }; // 하지만 위의 두 경우 모두 `result` 를 호출하는 쪽에서 값이 초기화 되기 전에 조회하면 // `undefined` 가 뜰 수도 있다. 즉, 기존 문법으로는 값의 초기화 전에 변수 접근이 가능하다.</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">이번에 추가된 모듈 내 최상위 레벨에서 <span style="color:#e52929;"><code>await</code></span>호출 문법은 비동기 호출 후의 로직을 <span style="color:#e52929;"><code>Promise.all()</code></span>로 감싸는 역할을 하기 때문에, 모듈의 비동기 작업이 완전히 완료되기 전에 작업 결과에 접근하지 않도록 합니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">// 새 문법으로 바꿔서 써보자. // module.mjs // 우선 async 함수로 감싸지 않아도 된다. // 비동기 로직 자체를 모듈 내에서 모두 처리하여 결과만 내보낸다고 생각하자. const response = await fetch('http://example.com/foo.json'); export const result = await response.json(); // another.mjs // 이 모듈을 불러오는 쪽에서는 이렇게 쓴다. import { result } from './module.mjs'; const messages = result; console.log(messages) // 위 코드는 실제로 아래와 동등하게 실행된다. // module.mjs export let result; export const promise = (async () => { const response = await fetch('http://example.com/foo.json'); result = await response.json(); })(); // another.mjs import { promise as promiseResult, result } from './module.mjs'; export const promise = (async () => { // 모듈을 호출하는 쪽에서 비동기 코드를 `Promise.all()` 로 묶음으로서 // 이후의 로직이 실행되지 않게 한다. await Promise.all([promiseResult]); const messages = result; console.log(messages); })(); </code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">참고로 최상위 레벨에서 <span style="color:#e52929;"><code>await</code></span>을 쓰거나, 또 다른 비동기 모듈을 <span style="color:#e52929;"><code>import</code></span>하면 해당 모듈은 비동기 모듈이 됩니다. 또한 비동기 모듈을 호출할 때 아래와 같은 방법으로 응용이 가능합니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">// 동적 호출 가능 const strings = await import(`/i18n/${navigator.language}`); // 의존성 폴백 관리 let jQuery; try { jQuery = await import('https://cdn-a.com/jQuery'); } catch { jQuery = await import('https://cdn-b.com/jQuery'); }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><span style="color:#e52929;"><strong>.at()</strong></span></h3><p style="text-align:justify;"><span style="color:#e52929;"><code>at()</code></span> 메서드는 문자열, 배열 등에서 <strong>음수 인덱싱(negative indexing)을 가능하게 해주는 메서드</strong>로 추가되었습니다. 이러한 인덱싱 기법은 Python에서 가장 흔히 사용되는데, JavaScript는 배열도 객체로 취급하는 JavaScript의 특성상 이것이 불가능합니다. 이를 지원하고자 이번 표준에 음수 인덱싱을 가능하게 하는 <span style="color:#e52929;"><code>.at()</code></span> 메서드가 추가되었습니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">const arr = [1, 2, 3]; // JavaScript에서 배열은 객체다. typeof arr === 'object'; // true // `-1` 이라는 키를 가진 값을 찾으려 하기 때문 arr[-1] // undefined // 기존 문법으로 마지막 인덱스의 값을 가져오려면 이렇게 해야 했지만 arr[arr.length - 1] // 3 // 이렇게 하면 훨씬 간편하다. arr.at(-1) // 3</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><span style="color:#e52929;"><strong>Object.hasOwn()</strong></span></h3><p style="text-align:justify;"><span style="color:#e52929;"><code>Object.hasOwn()</code></span>는 객체의 특정 속성이 프로토타입을 거치지 않은 객체 그 자신이 소유한 속성인지를 반환하는 메서드입니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">사실 기존의 <span style="color:#e52929;"><code>Object.hasOwnProperty()</code></span> 메서드와 거의 비슷한 역할을 하지만 이 함수가 새롭게 정의된 이유는 <span style="color:#e52929;"><code>.hasOwnProperty()</code></span> 자체가 <span style="color:#e52929;"><code>Object</code></span>의 프로토타입에 종속된 메서드이기 때문에, 프로토타입이 없거나 재정의된 객체에서는 <span style="color:#e52929;"><code>.hasOwnProperty()</code></span>의 기능을 사용할 수 없기 때문입니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">// 기존에는 이런 방법을 주로 활용했다. let hasOwnProperty = Object.prototype.hasOwnProperty hasOwnProperty.call(object, "foo") // 이렇게 굳이 메서드를 다시 꺼내고 변수에 저장해 쓴 이유는... // 프로토타입이 끊기면 메서드를 사용할 수 없었기 때문 Object.create(null).hasOwnProperty("someProp") // error // Uncaught TypeError: Object.create(...).hasOwnProperty is not a function</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">이번에 새로 추가된 Object.hasOwn()는 정적 메서드로 구현되었기 때문에 특정 인스턴스의 프로토타입 상속 관계에 구애받지 않고 사용 가능하다는 장점이 있습니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">const proto = { protoProp: '프로토타입 속성', }; const object = { __proto__: proto, objProp: '객체 속성', } // in 연산자는 객체의 속성에 해당 값이 있는지를 반환한다 console.log('protoProp' in object); // true // hasOwn 메서드는 프로토타입 상속이 아닌 객체 고유의 속성인지를 반환한다 console.log(Object.hasOwn(object, 'protoProp')); // false console.log(Object.hasOwn(proto, 'protoProp')); // true</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><span style="color:#e52929;"><strong>Error.prototype.cause</strong></span></h3><p style="text-align:justify;"><span style="color:#e52929;"><code>Error.prototype.cause</code></span>는 <strong>에러 체이닝을 위해 도입된 속성</strong>입니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">일반적으로 메서드 내에서 정의되지 않은 예외 동작이 발생한 경우, 발생한 에러를 근거로 코드를 수정할 수 있죠. 하지만 메서드가 깊게 중첩된 경우에는 단순한 에러 로그만으로는 원인 파악 및 대처가 어려울 수 있는데요, 이번에 추가된 <span style="color:#e52929;"><code>cause</code></span>를 활용하면 발생한 오류를 다시 한번 감싸서, 추가적인 컨텍스트 메시지를 참조하게 만든 새 에러를 <span style="color:#e52929;"><code>throw</code></span> 하는 방식으로 체이닝할 수 있습니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">function job1() { try { job2(); } catch (e) { console.log(e); // Error: job2 Error throw new Error('job1 Error', { cause: e }); } } function job2() { throw new Error('job2 Error'); } try { job1(); } catch (e) { console.log(e); // Error: job1 Error console.log(e.cause); // Error: job2 Error } </code></pre><p style="text-align:justify;"> </p><figure class="image image_resized" style="width:100%;"><img src="https://yozm.wishket.com/media/news/1570/image005.png" alt="원본 에러"><figcaption>원본 에러는 <span style="color:#e52929;"><code>e.cause</code></span> 를 추적해 따라가면 찾을 수 있다</figcaption></figure>