본문으로 건너뛰기
Advertisement

ES2021~ES2024 주요 기능

ES2021부터 ES2024까지 추가된 최신 기능들을 살펴봅니다. WeakRef, 논리 할당 연산자, 배열 비변경 메서드, Promise.withResolvers 등 실용적인 기능들이 포함됩니다.


ES2021 (ES12)

String.replaceAll()

const text = 'foo bar foo baz foo';

// 기존 방식: 전역 정규식 필요
text.replace(/foo/g, 'qux'); // 'qux bar qux baz qux'

// ES2021: replaceAll
text.replaceAll('foo', 'qux'); // 'qux bar qux baz qux'

// 특수문자 이스케이프 불필요
const html = '<div class="error">Error</div>';
html.replaceAll('<div', '<section').replaceAll('div>', 'section>');

// replace와 차이
'aaa'.replace('a', 'b'); // 'baa' (첫 번째만)
'aaa'.replaceAll('a', 'b'); // 'bbb' (모두)

// 정규표현식도 사용 가능 (g 플래그 필수)
'Hello World'.replaceAll(/[aeiou]/gi, '*'); // 'H*ll* W*rld'

Promise.any()

// 하나라도 fulfilled되면 그 결과 반환
// 모두 rejected되면 AggregateError

// 가장 빠른 서버 응답 사용
try {
const data = await Promise.any([
fetch('https://server1.example.com/api').then(r => r.json()),
fetch('https://server2.example.com/api').then(r => r.json()),
fetch('https://server3.example.com/api').then(r => r.json()),
]);
console.log('가장 빠른 응답:', data);
} catch (err) {
// err instanceof AggregateError
console.error('모든 서버 실패:', err.errors);
}

// Promise.any vs Promise.race
// race: 첫 번째 settled (실패도 포함)
// any: 첫 번째 fulfilled (성공만)

Promise.race([
Promise.reject('실패'),
Promise.resolve('성공'),
]).catch(err => console.log(err)); // '실패' (실패가 먼저 settled)

Promise.any([
Promise.reject('실패'),
Promise.resolve('성공'),
]).then(val => console.log(val)); // '성공' (성공한 것)

WeakRef

// 객체에 대한 약한 참조 (GC를 방해하지 않음)
class Cache {
#cache = new Map();

set(key, value) {
this.#cache.set(key, new WeakRef(value));
}

get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;

const value = ref.deref();
if (!value) {
// GC에 의해 수거됨
this.#cache.delete(key);
return undefined;
}
return value;
}
}

// 실전: 이미지 캐시
const imageCache = new Map();

function getCachedImage(src) {
const ref = imageCache.get(src);
const cached = ref?.deref();

if (cached) return cached;

const img = new Image();
img.src = src;
imageCache.set(src, new WeakRef(img));
return img;
}

// WeakRef.deref()는 GC 이전에는 객체 반환, 이후에는 undefined 반환

FinalizationRegistry

// 객체가 GC될 때 콜백 실행
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue}가 GC에 의해 수거되었습니다`);
});

let obj = { name: 'Resource' };
registry.register(obj, 'Resource 객체');

obj = null; // 참조 제거
// 나중에 GC가 실행되면 콜백 호출

// 실전: 리소스 정리
class ManagedResource {
static #registry = new FinalizationRegistry((cleanup) => {
cleanup(); // 정리 함수 실행
});

constructor(resource) {
this.resource = resource;
ManagedResource.#registry.register(
this,
() => resource.dispose() // GC 시 자동 정리
);
}
}

논리 할당 연산자

// &&= (논리 AND 할당)
// x &&= y → x && (x = y)
let config = { debug: true, verbose: false };

config.debug &&= false; // debug가 truthy면 false로 설정
config.verbose &&= true; // verbose가 falsy면 변경 안 함

// ||= (논리 OR 할당)
// x ||= y → x || (x = y)
let settings = { theme: '', language: null };

settings.theme ||= 'dark'; // theme이 falsy면 'dark'로 설정
settings.language ||= 'ko'; // language가 null이면 'ko'로 설정

// ??= (Nullish 할당)
// x ??= y → x ?? (x = y)
let options = { timeout: 0, name: '' };

options.timeout ??= 5000; // 0은 null/undefined가 아니므로 변경 안 함
options.name ??= 'default'; // ''은 null/undefined가 아니므로 변경 안 함
options.cache ??= true; // undefined이므로 true로 설정

// 실전: 옵션 객체 기본값 설정
function createServer(options = {}) {
options.port ??= 3000;
options.host ??= 'localhost';
options.timeout ??= 30000;
options.debug &&= process.env.NODE_ENV === 'development';

return new Server(options);
}

숫자 구분자

// 큰 숫자 가독성 향상
const million = 1_000_000;
const billion = 1_000_000_000;
const hex = 0xFF_FF_FF;
const binary = 0b1010_0001_1000_0101;
const bytes = 0xFF_EC_D5_12;
const bigint = 9_007_199_254_740_991n;

// 소수점도 가능
const pi = 3.141_592_653_589_793;
const e = 2.718_281_828;

console.log(million); // 1000000 (숫자 값은 동일)

ES2022 (ES13)

Array.at()

const arr = [1, 2, 3, 4, 5];

// 기존: 마지막 요소 접근
arr[arr.length - 1]; // 5 (번거로움)

// at(): 인덱스 접근 (음수 가능)
arr.at(0); // 1
arr.at(-1); // 5 (마지막)
arr.at(-2); // 4 (끝에서 두 번째)

// 문자열과 TypedArray에도 적용
'hello'.at(-1); // 'o'
new Int32Array([1, 2, 3]).at(-1); // 3

// 실전
function getLast(arr) {
return arr.at(-1);
}

const history = ['page1', 'page2', 'page3'];
const currentPage = history.at(-1); // 'page3'
const previousPage = history.at(-2); // 'page2'

Object.hasOwn()

// Object.prototype.hasOwnProperty의 안전한 대체
const obj = { name: 'Alice' };

// 기존 방식 (안전하지 않을 수 있음)
obj.hasOwnProperty('name'); // true

// Object.create(null)로 만든 객체는 hasOwnProperty 없음
const bare = Object.create(null);
bare.name = 'Alice';
// bare.hasOwnProperty('name'); // TypeError!

// Object.hasOwn은 항상 안전
Object.hasOwn(bare, 'name'); // true
Object.hasOwn(obj, 'name'); // true
Object.hasOwn(obj, 'toString'); // false (프로토타입 속성)

// 실전
function processConfig(config) {
if (Object.hasOwn(config, 'timeout')) {
setupTimeout(config.timeout);
}
}

Error.cause

// 에러 체인 - 원인 에러 추적
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (originalError) {
throw new Error(`사용자 ${userId} 데이터 로드 실패`, {
cause: originalError // 원인 에러 연결
});
}
}

// 에러 체인 탐색
try {
await fetchUserData(1);
} catch (err) {
console.error(err.message); // 사용자 1 데이터 로드 실패
console.error(err.cause.message); // 원인 에러 메시지

// 에러 체인 전체 출력
let current = err;
while (current) {
console.error(current.message);
current = current.cause;
}
}

// 커스텀 에러 클래스에서 활용
class DatabaseError extends Error {
constructor(message, options) {
super(message, options);
this.name = 'DatabaseError';
}
}

async function queryDB(sql) {
try {
return await db.query(sql);
} catch (err) {
throw new DatabaseError(`쿼리 실패: ${sql}`, { cause: err });
}
}

Top-level await (모듈에서)

// config.mjs - 모듈 최상위에서 await 사용
const response = await fetch('/api/config');
export const config = await response.json();

// db.mjs
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
export { client };

// 이를 import하는 쪽은 자동으로 완료 대기
// app.mjs
import { config } from './config.mjs';
import { client } from './db.mjs';
// config와 client가 준비된 후 실행됨

클래스 private 필드/메서드

class BankAccount {
// private 필드: # 접두사
#balance = 0;
#owner;
#transactions = [];

constructor(owner, initialBalance = 0) {
this.#owner = owner;
this.#balance = initialBalance;
}

// private 메서드
#validateAmount(amount) {
if (amount <= 0) throw new Error('금액은 0보다 커야 합니다');
if (typeof amount !== 'number') throw new Error('금액은 숫자여야 합니다');
}

#recordTransaction(type, amount) {
this.#transactions.push({
type, amount,
balance: this.#balance,
timestamp: new Date()
});
}

// public 메서드
deposit(amount) {
this.#validateAmount(amount);
this.#balance += amount;
this.#recordTransaction('deposit', amount);
return this;
}

withdraw(amount) {
this.#validateAmount(amount);
if (amount > this.#balance) throw new Error('잔액 부족');
this.#balance -= amount;
this.#recordTransaction('withdrawal', amount);
return this;
}

// getter
get balance() { return this.#balance; }
get owner() { return this.#owner; }

// static private
static #instanceCount = 0;
static getInstanceCount() { return BankAccount.#instanceCount; }
}

const account = new BankAccount('Alice', 10000);
account.deposit(5000).withdraw(3000);
console.log(account.balance); // 12000
// account.#balance; // SyntaxError! private 접근 불가

클래스 static 블록

class Config {
static #instance = null;
static debug;
static apiUrl;

// static 초기화 블록
static {
const env = process.env.NODE_ENV ?? 'development';
Config.debug = env !== 'production';
Config.apiUrl = env === 'production'
? 'https://api.example.com'
: 'http://localhost:3000';
}
}

console.log(Config.debug); // true (개발 환경)
console.log(Config.apiUrl); // 'http://localhost:3000'

ES2023 (ES14)

Array.findLast() / findLastIndex()

const arr = [1, 2, 3, 4, 5, 4, 3];

// findLast: 뒤에서부터 조건에 맞는 첫 번째 요소
arr.findLast(x => x > 3); // 4 (뒤에서 첫 번째)
arr.findLast(x => x > 10); // undefined

// findLastIndex: 뒤에서부터 조건에 맞는 인덱스
arr.findLastIndex(x => x > 3); // 5 (index 5의 값이 4)

// 실전: 최근 이벤트 찾기
const events = [
{ type: 'click', time: 100 },
{ type: 'scroll', time: 200 },
{ type: 'click', time: 300 },
{ type: 'keypress', time: 400 }
];

const lastClick = events.findLast(e => e.type === 'click');
// { type: 'click', time: 300 }

배열 비변경 메서드 (Immutable Array Methods)

const arr = [3, 1, 4, 1, 5, 9, 2];
const original = [...arr];

// toSorted: sort()의 비변경 버전
const sorted = arr.toSorted((a, b) => a - b);
// sorted: [1, 1, 2, 3, 4, 5, 9]
// arr: [3, 1, 4, 1, 5, 9, 2] (변경 안 됨)

// toReversed: reverse()의 비변경 버전
const reversed = arr.toReversed();
// reversed: [2, 9, 5, 1, 4, 1, 3]
// arr: 변경 안 됨

// toSpliced: splice()의 비변경 버전
const spliced = arr.toSpliced(2, 1, 99, 100);
// 인덱스 2에서 1개 제거하고 99, 100 삽입
// spliced: [3, 1, 99, 100, 1, 5, 9, 2]
// arr: 변경 안 됨

// with: 특정 인덱스 값 교체 (비변경)
const updated = arr.with(0, 999); // 인덱스 0을 999로
// updated: [999, 1, 4, 1, 5, 9, 2]
// arr: 변경 안 됨

// 실전: React/Vue 같은 프레임워크에서 불변 상태 업데이트
const [items, setItems] = useState([3, 1, 4]);

// 이전 방식: 복사 후 조작
setItems(prev => {
const next = [...prev];
next.sort();
return next;
});

// ES2023 방식: 더 간결
setItems(prev => prev.toSorted());
setItems(prev => prev.with(0, 99));

Hashbang Grammar

#!/usr/bin/env node
// 파일 맨 첫 줄에 hashbang(shebang) 허용
// Node.js, Deno 등 CLI 스크립트에서 사용

console.log('Hello from CLI script!');

ES2024 (ES15)

Promise.withResolvers()

// 기존: Promise 생성자 밖에서 resolve/reject 접근이 번거로움
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});

// ES2024: Promise.withResolvers()로 간결하게
const { promise, resolve, reject } = Promise.withResolvers();

// 실전: 이벤트를 Promise로 변환
function waitForEvent(element, eventName) {
const { promise, resolve, reject } = Promise.withResolvers();

function handler(event) {
element.removeEventListener(eventName, handler);
resolve(event);
}

element.addEventListener(eventName, handler);

// 타임아웃 지원
const timeout = setTimeout(() => {
element.removeEventListener(eventName, handler);
reject(new Error(`${eventName} 이벤트 타임아웃`));
}, 5000);

return promise.finally(() => clearTimeout(timeout));
}

// 사용
const clickEvent = await waitForEvent(document.getElementById('btn'), 'click');
console.log('클릭됨:', clickEvent.target);

// 비동기 큐 구현
function createQueue() {
const pending = [];
let { promise, resolve } = Promise.withResolvers();

return {
push(item) {
pending.push(item);
const prev = resolve;
({ promise, resolve } = Promise.withResolvers());
prev();
},
async *[Symbol.asyncIterator]() {
while (true) {
await promise;
while (pending.length) yield pending.shift();
}
}
};
}

ArrayBuffer resize/transfer

// 크기 조절 가능한 ArrayBuffer
const resizable = new ArrayBuffer(1024, { maxByteLength: 4096 });
console.log(resizable.byteLength); // 1024
console.log(resizable.resizable); // true
console.log(resizable.maxByteLength); // 4096

// 크기 조절
resizable.resize(2048);
console.log(resizable.byteLength); // 2048

// transfer: ArrayBuffer 소유권 이전 (복사 없이)
const source = new ArrayBuffer(1024);
const view = new Uint8Array(source);
view[0] = 42;

// transfer: 새 ArrayBuffer로 소유권 이전, source는 무효화됨
const transferred = source.transfer(512); // 크기 변경도 가능
console.log(source.byteLength); // 0 (무효화됨)
console.log(transferred.byteLength); // 512

// 실전: WebSocket에서 수신 버퍼 관리
const buffer = new ArrayBuffer(4096, { maxByteLength: 16384 });

Object.groupBy() / Map.groupBy()

const products = [
{ name: 'Apple', category: 'fruits', price: 1000 },
{ name: 'Banana', category: 'fruits', price: 500 },
{ name: 'Carrot', category: 'veggies', price: 800 },
{ name: 'Potato', category: 'veggies', price: 600 },
{ name: 'Milk', category: 'dairy', price: 1500 },
];

// Object.groupBy: 키가 문자열인 일반 객체로 그룹화
const byCategory = Object.groupBy(products, item => item.category);
// {
// fruits: [{ name: 'Apple', ... }, { name: 'Banana', ... }],
// veggies: [{ name: 'Carrot', ... }, { name: 'Potato', ... }],
// dairy: [{ name: 'Milk', ... }]
// }

// 가격대로 그룹화
const byPriceRange = Object.groupBy(products, item => {
if (item.price < 700) return 'cheap';
if (item.price < 1200) return 'medium';
return 'expensive';
});

// Map.groupBy: 키가 어떤 타입이든 가능 (객체도 키)
const byPriceObj = Map.groupBy(products, item => item.price > 900 ? 'high' : 'low');
byPriceObj.get('high'); // 고가 상품 배열
byPriceObj.get('low'); // 저가 상품 배열

// 이전 방식 (reduce)
const grouped = products.reduce((acc, item) => {
const key = item.category;
(acc[key] ??= []).push(item);
return acc;
}, {});

Well-formed Unicode Strings

// String.prototype.isWellFormed(): 유효한 유니코드 문자열인지 확인
'hello'.isWellFormed(); // true
'\uD800'.isWellFormed(); // false (lone surrogate)
'\uD800\uDC00'.isWellFormed(); // true (valid surrogate pair)

// String.prototype.toWellFormed(): 유효하지 않은 부분을 U+FFFD로 대체
'\uD800hello'.toWellFormed(); // '\uFFFDhello'

// 실전: URL 인코딩 전 안전 처리
function safeEncodeURIComponent(str) {
return encodeURIComponent(str.toWellFormed());
}

버전별 요약

기능버전설명
String.replaceAllES2021전체 문자열 치환
Promise.anyES2021첫 성공 Promise
WeakRefES2021약한 참조
논리 할당 연산자 `&&=,=, ??=`
Array.atES2022음수 인덱스 접근
Object.hasOwnES2022안전한 hasOwnProperty
Error.causeES2022에러 체인
Top-level awaitES2022모듈 최상위 await
Class private #ES2022진짜 private
Array.findLast/findLastIndexES2023뒤에서 탐색
toSorted/toReversed/toSpliced/withES2023비변경 배열 메서드
Promise.withResolversES2024Promise 외부 제어
Object.groupBy/Map.groupByES2024그룹화
ArrayBuffer resize/transferES2024버퍼 관리

고수 팁

1. 비변경 메서드를 함수형 프로그래밍에 활용

// 파이프라인 스타일
const result = data
.toSorted((a, b) => b.score - a.score)
.toSpliced(3) // 상위 3개만
.map(formatUser); // 원본은 그대로

2. Object.groupBy로 다차원 집계

// 중첩 그룹화
const byYearAndMonth = Object.groupBy(
transactions,
tx => `${tx.date.getFullYear()}-${String(tx.date.getMonth() + 1).padStart(2, '0')}`
);

3. WeakRef와 FinalizationRegistry 조합으로 캐시 구현

class SmartCache {
#cache = new Map();
#registry = new FinalizationRegistry(key => {
this.#cache.delete(key);
});

set(key, value) {
const ref = new WeakRef(value);
this.#cache.set(key, ref);
this.#registry.register(value, key);
}

get(key) {
return this.#cache.get(key)?.deref();
}
}
Advertisement