
지난 6월 ES2025가 출시되었고, 최근 TC39는 ES2026 후보(candidate)를 승인했습니다. 이번에 도입되는 기능 중 일부는 제가 매일 자바스크립트 코드를 작성하는 방식을 완전히 바꿔놓을 것입니다.
모든 기능이 그렇지는 않습니다. 하지만 이터레이터 헬퍼(iterator helpers), 새로운 Set 메서드, Map.getOrInsert, 그리고 Array.fromAsync는 자바스크립트라는 언어를 실질적으로 개선한 훌륭한 기능들입니다. 템포럴(Temporal) API(현재 Stage 4 단계로, ES2027 도입 예정), using 키워드, 그리고 import defer는 아쉽게도 ES2026 스펙에 포함되지 못했습니다. 하지만 폴리필(polyfill)이나 브라우저 구현체가 이미 성숙해서 지금 바로 사용할 수 있습니다.
이러한 신기능을 자세히 살펴보기 전에, 제가 개발을 처음 시작할 때 알았더라면 좋았을 몇 가지 배경지식을 먼저 소개해 드리고자 합니다.
모든 브라우저는 자체적인 자바스크립트 엔진을 탑재하고 있습니다. 크롬(Chrome)의 V8, 사파리(Safari)의 JavaScriptCore, 파이어폭스(Firefox)의 SpiderMonkey가 대표적입니다.
각 엔진은 서로 다른 팀이 개발하는 별개의 코드베이스입니다. 그렇다면 크롬이나 사파리 등 서로 다른 브라우저에서도 Array.prototype.map이 똑같이 동작하고, async/await가 동일하게 작동하는 이유는 무엇일까요?
답은 간단합니다. 모든 브라우저가 ECMAScript라는 동일한 명세(specification)를 구현하기 때문입니다.
자바스크립트라는 언어는 TC39 위원회(committee)가 관리하는 ECMAScript 명세를 따릅니다. 이 위원회는 C# 명세(ECMA-334)나 JSON 데이터 교환 표준(ECMA-404)을 발행하는 표준화 기구인 Ecma 인터내셔널(Ecma International) 산하 위원회입니다.
TC39 위원회는 구글, 애플, 모질라, 마이크로소프트 등 주요 브라우저 제조사의 대표단을 비롯하여 블룸버그, 이갈리아, 인텔과 같은 기업 관계자 및 개별 초빙 전문가들로 구성됩니다.
위원회는 대략 두 달에 한 번씩 회의를 열어 합의(consensus) 하에 의사 결정을 진행합니다. 이는 실질적으로 거부권을 행사할 만큼 강하게 반대하는 사람이 아무도 없음을 뜻합니다.
자바스크립트에 추가하려는 모든 제안은 일정한 단계를 거칩니다.
이를 깔때기에 비유할 수 있습니다. 누구나 아이디어를 제안할 수 있지만, 자바스크립트라는 언어에 최종적으로 탑재되는 기능은 극히 일부에 불과합니다.
제안이 Stage 4에 도달하면 살아있는 자바스크립트 명세(living specification)에 즉시 머지(merge)되며, 이듬해 출시되는 연간 스냅샷에 포함됩니다. 위원회는 매년 2월 1일에 명세 후보 초안을 작성하고, 3월에 명세를 분기(branch)하며, 7월에 개최되는 Ecma 총회에서 공식 비준을 받습니다.
따라서 매년 6월 무렵 새로 발표되는 기능들은 이미 수개월 전부터 브라우저에서 사용할 수 있었던 상태입니다. 공식 명세에 이름을 올릴 때쯤이면 프로덕션 환경에서도 무리 없이 사용할 수 있는 셈입니다.
2025년 6월 25일에 개최된 제129차 Ecma 총회에서 ECMAScript 2025가 비준되었습니다. 통산 16번째 버전입니다. 다음은 이번 명세에 추가된 기능들을 제가 체감하는 유용성 순서대로 정리한 내용입니다.
상태 ES2025 반영 완료. 크롬 122 이상, 노드(Node.js) 22 이상, 파이어폭스 131 이상, 사파리 18.4 이상 지원.
개인적으로 ES2025에서 가장 반가운 기능입니다.
이터레이터(iterator)는 필요할 때마다 값을 하나씩 차례로 꺼낼 수 있는 객체입니다. 객체 내부의 .next() 메서드를 호출할 때마다 다음 값을 반환하는 방식을 취합니다.
자바스크립트에서 이터레이터가 존재하는 이유는 순회해야 할 대상이 비단 배열(array)만이 아니기 때문입니다.
Map은 해시 테이블 구조로 키-값 쌍을 저장합니다.Set은 고유한 값을 내부 구조에 저장합니다.NodeList는 DOM 트리에 대한 실시간 뷰(view)를 제공합니다.이러한 요소들은 메모리에 펼쳐진 플랫한 배열이 아닙니다. 그럼에도 개발자는 for (const x of thing) 같은 방식으로 이들을 간편하게 순회하길 원합니다.
이터레이터는 이를 가능하게 만드는 통일된 프로토콜(protocol)입니다. 어떤 객체든 내부적으로 .next()를 구현하여 '원소를 순회하는 방법'을 명시하면, 자바스크립트의 문법 요소(for...of 문, 스프레드 연산자, 구조 분해 할당 등)가 이를 알아서 파악하고 동작합니다.
그래서 배열이 아닌 Set을 스프레드 연산자로 풀어 배열로 만들거나, Map 객체의 엔트리들을 구조 분해할 수 있고, DOM 쿼리 결과(NodeList)를 루프로 순회할 수 있는 것입니다.
다음과 같이 코드를 작성할 때도 마찬가지입니다.
for (const item of someArray) { ... }
for (const [key, value] of someMap) { ... }
for (const node of document.querySelectorAll('.card')) { ... }
const copy = [...someSet];
const merged = [...arr1, ...arr2];
자바스크립트는 내부적으로 알아서 이터레이터를 생성하고 거기서 값을 꺼내옵니다. for...of 루프나 스프레드 연산자는 '반복 가능한 객체(iterable)'라면 어디서든 작동하며, 내부에서는 이터레이터의 .next() 메서드를 반복해서 호출할 뿐입니다.
이터레이터가 중요한 또 다른 이유는 바로 지연 평가(lazy evaluation) 때문입니다.
배열은 그 즉시 모든 값을 메모리에 다 담고 있어야 하지만, 이터레이터는 값을 요청받는 바로 그 순간에 비로소 다음 값을 계산합니다.
데이터 크기가 작을 때는 별 차이가 없지만, 100만 행짜리 CSV 파일이나 페이지네이션이 적용된 API 스트림, 혹은 무한한 수열처럼 크기가 거대할 때는 애플리케이션이 터지거나 얼어붙지 않고 순회를 원활히 처리할 수 있는 핵심적인 역할을 합니다.
개발자는 function*로 선언하는 제너레이터(generator) 함수를 사용해 자신만의 이터레이터를 만들 수도 있습니다. 제너레이터는 yield 키워드를 만날 때마다 실행을 멈추고, 다음 값을 요구받을 때 다시 실행을 재개합니다.
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
naturalNumbers()를 호출하면 매 요청마다 1, 2, 3, ...을 무한히 생성하는 이터레이터가 반환됩니다.
일반적인 함수 안에 while (true) 문을 넣었다면 즉시 연산하면서 브라우저가 멈춰버렸을 것입니다. 하지만 제너레이터는 외부에서 값을 당겨올(pull) 때에만 실행되므로 문제가 생기지 않습니다.
이렇듯 이터레이터는 자바스크립트 전반에 활용되며 그 핵심은 지연 평가(laziness)에 있습니다. 문제는 이렇게 이터레이터를 확보한 뒤 할 수 있는 작업이 거의 없었다는 점입니다.
배열에는 .map(), .filter(), .reduce(), .flatMap() 같은 유용한 기능들이 가득하지만 이터레이터에는 오직 .next() 하나만 있을 뿐입니다.
그래서 이터레이터에서 데이터를 변환하고 싶으면 일단 배열로 변환하는 작업을 거쳐야만 했습니다.
const visibleCards = Array.from(document.querySelectorAll('.card'))
.filter(el => !el.classList.contains('hidden'))
.map(el => el.dataset.id);
이 방식은 작동은 하지만 두 가지 비용이 수반됩니다.
첫째, 단순한 메서드 체이닝을 위해 중간 과정에서 배열을 계속 새로 할당해야 합니다. DOM 노드가 100개뿐이라면 큰 문제 없겠지만, CSV 파서에서 나온 10만 행을 처리하는 수준이라면 단 한 줄을 필터링하기 위해 파일 전체를 메모리에 한꺼번에 띄우는 상황이 초래됩니다.
둘째, 무한 루프 수열이나 스트리밍 데이터의 경우에는 아예 작동하지 않습니다. Array.from은 이터레이터가 끝날 때까지 값을 다 가져오려 하므로, 여기에 naturalNumbers()를 전달하면 브라우저 탭이 영원히 먹통이 됩니다.
결국 무한 수열이나 스트리밍 상황에서는 배열 메서드를 쓰는 걸 포기하고 직접 루프문을 일일이 작성해야 했습니다.
기존 방식 (무한 수열에서 짝수인 제곱수 10개만 추출)
const firstTenEvenSquares = [];
for (const n of naturalNumbers()) {
if (n % 2 === 0) {
firstTenEvenSquares.push(n * n);
if (firstTenEvenSquares.length === 10) break;
}
}
ES2025부터는 이 메서드들을 이터레이터 프로토타입에서 곧바로 지원합니다.
개선된 방식
const firstTenEvenSquares = naturalNumbers()
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(10)
.toArray();
이 코드가 무한 수열 상황에서도 정상 동작하는 이유는 이터레이터 헬퍼가 지연 평가 방식으로 동작하기 때문입니다. .filter()는 naturalNumbers()의 모든 원소를 단번에 긁어오는 대신 요구할 때 한 번에 하나씩만 평가하는 이터레이터를 새로 반환합니다. 그리고 .take(10) 덕분에 10개를 확보하면 더는 위쪽 이터레이터에 요청하지 않고 멈춥니다. 전체 무한 수열을 전부 나열하려고 시도하지 않기 때문에 무한 루프 문제가 발생하지 않습니다.
Iterator.prototype에 정식으로 포함된 메서드는 .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .forEach(), .some(), .every(), .find(), .toArray() 입니다.
이미 이터레이터가 아닌 반복 가능한 객체(NodeList나 커스텀 이터러블 클래스 등 이터러블)는 새로 도입된 전역 클래스 Iterator와 이의 정적 메서드인 Iterator.from(x)으로 래핑하여 헬퍼 메서드를 사용할 수 있습니다. 앞서 보았던 DOM 노드 예제는 다음과 같이 바뀝니다.
const visibleCards = Iterator.from(document.querySelectorAll('.card'))
.filter(el => !el.classList.contains('hidden'))
.map(el => el.dataset.id)
.toArray();
이 기능이 진가를 발휘하는 부분은 청크(chunk) 단위로 끊어 읽는 로그 파일이나 CSV 데이터 같은 스트리밍 데이터를 가공할 때입니다.
// 대규모 로그 파일에서 처음으로 발견된 100개의 에러만 추려내고 읽기를 중단합니다.
const errors = logFileLines()
.filter(line => line.includes('ERROR'))
.take(100)
.toArray();
단 한 가지 주의할 점은, 이번 ES2025에 탑재된 것은 동기식(sync) 헬퍼뿐이라는 사실입니다. 비동기식 이터러블을 순회할 때 쓰는 비동기 이터레이터 헬퍼 및 동기 이터레이터를 비동기로 바꾸는 Iterator.prototype.toAsync() 등은 아직 Stage 2 단계에 머물러 있는 별도의 제안입니다.
따라서 스트리밍 fetch 호출, LLM 토큰 스트림, 비동기 제너레이터 등 비동기 작업을 다룰 때는 당분간 계속 for await...of 루프를 직접 작성해야 합니다.
상태 ES2025 반영 완료. 모든 주요 브라우저 및 Node.js 22 이상에서 즉시 사용 가능.
다른 언어들처럼 집합(set) 연산을 손쉽게 할 수 있는 메서드들이 추가되었습니다.
기존 방식 (교집합을 직접 구하는 방법)
const frontEnd = new Set(['HTML', 'CSS', 'JavaScript', 'React']);
const backEnd = new Set(['Node.js', 'JavaScript', 'SQL', 'React']);
// 직접 구현한 교집합 연산
const shared = new Set();
for (const tech of frontEnd) {
if (backEnd.has(tech)) shared.add(tech);
}
// 또는 lodash를 가져와서 사용: _.intersection([...frontEnd], [...backEnd])
개선된 방식
frontEnd.union(backEnd);
// Set(6) { 'HTML', 'CSS', 'JavaScript', 'React', 'Node.js', 'SQL' }
frontEnd.intersection(backEnd);
// Set(2) { 'JavaScript', 'React' }
frontEnd.difference(backEnd);
// Set(2) { 'HTML', 'CSS' }
frontEnd.symmetricDifference(backEnd);
// Set(4) { 'HTML', 'CSS', 'Node.js', 'SQL' }
frontEnd.isSubsetOf(backEnd); // false
frontEnd.isSupersetOf(backEnd); // false
frontEnd.isDisjointFrom(backEnd); // false
동작 방식에서 기억해 둘 점이 두 가지 있습니다. 먼저 이 메서드들은 기존의 Set 객체를 변형하지 않는 비파괴적(non-mutating) 메서드이며, 연산 결과로 새로운 Set 객체를 생성해 반환합니다.
또한 메서드의 인자로 주어지는 대상이 반드시 실제 Set 객체여야 하는 것은 아닙니다. 숫자로 된 size 프로퍼티, .has() 메서드, 그리고 이터레이터를 반환하는 .keys() 메서드를 가지고 있는 'Set 모양의 객체(set-like)'이기만 하면 어떤 것이든 전달할 수 있습니다.
Map 객체도 이 조건을 충족하며, 세 가지 요구 사항을 갖춘 직접 구현한 LRUCache 같은 커스텀 클래스도 마찬가지입니다. 단, 호출하는 대상(this)은 진짜 Set이어야 하고 인자로 받는 대상만 위 규칙에 따라 유연하게 대응합니다.
이 연산 지원 제안이 통과되기까지 수년이 걸린 것도 바로 어떤 스펙 프로토콜을 규정할 것인가를 두고 위원회 내부에서 논쟁이 치열했기 때문입니다.
상태 ES2025 반영 완료. 크롬 123 이상, Node.js 22 이상, 파이어폭스 133 이상, 사파리 17.4 이상 지원.
이제 자바스크립트 파일을 가져올 때와 마찬가지로 JSON 파일을 네이티브 구문으로 바로 임포트(import)하여 사용할 수 있습니다.
기존 방식
// 방법 A: 번들러의 자체 변환 기능에 의존하는 방식
import config from './config.json';
// 웹팩, 비트, 롤업 등 번들러에서는 작동하지만 비표준 방식입니다.
// 번들러 없이 웹 브라우저나 Node.js 환경에서 생으로 실행하면 에러가 납니다.
// 방법 B: 런타임에 직접 fetch하여 받아오는 방식
const config = await fetch('./config.json').then(r => r.json());
개선된 방식
import config from './config.json' with { type: 'json' };
// 또는 동적으로 가져올 수도 있습니다.
const translations = await import('./translations.json', {
with: { type: 'json' }
});
구문에 들어가는 with { type: 'json' } 문법은 필수 요소이며, 이를 임포트 속성(import attribute)이라고 부릅니다.
이 속성은 모듈 로더에 '가져올 파일이 JSON 파일'임을 미리 선언하는 것입니다. 서버가 반환한 MIME 타입이 JSON이 아닐 경우 로드를 거부하게 만들어 보안 사고를 예방합니다.
만약 with 속성 표기가 없었다면, 해킹당한 CDN 서버가 JSON으로 위장한 실행 가능한 악성 코드를 자바스크립트 코드 블록으로 그대로 다운로드해 실행해 버릴 위험이 생깁니다.
상태 ES2025 반영 완료. 크롬 128 이상, Node.js 22 이상, 파이어폭스 134 이상, 사파리 18.2 이상 지원.
여담이지만, 이 기능은 유명 비동기 라이브러리인 블루버드(Bluebird)에 10년도 더 전부터 들어있던 패턴입니다. 이것이 마침내 ES2025에 이르러 자바스크립트 표준 API가 되었습니다.
특정 함수를 호출하려는데 이 함수가 동기(sync) 방식일지 비동기(async) 방식일지 불확실하고, 또 연산 중에 즉각 예외를 던질지 아닐지 모르는 상황이 있습니다. 이때 반환 방식에 상관없이 모든 예외와 결과를 하나의 에러 핸들링 흐름으로 묶어 처리하고 싶을 때 유용합니다.
기존 방식
// thirdParty.doThing() 함수가 내부적으로 에러를 던질지, 일반 값을 줄지, 프로미스를 줄지 확실하지 않은 상황
try {
const result = thirdParty.doThing();
// 프로미스를 던졌다면 프로미스에 맞추어 처리합니다.
Promise.resolve(result)
.then(r => processResult(r))
.catch(err => handleAnyFailure(err));
} catch (err) {
// 동기 에러가 던져지면 프로미스 체인에 안 들어오고 예외가 발생하므로 catch 블록을 또 따로 준비합니다.
handleAnyFailure(err);
}
이렇게 되면 핸들러 코드가 파편화되어 두 곳을 모두 챙겨야 합니다. 이를 우회하려고 Promise.resolve().then(() => thirdParty.doThing()) 같은 꼼수를 쓰기도 했습니다. 전체 호출을 무조건 프로미스로 변환하므로 에러 핸들러는 통일할 수 있지만, 콜백 연산이 마이크로태스크(microtask) 큐로 넘어가 다음 '틱(tick)'으로 밀리는 단점이 있었습니다. 즉, 원래 동기식이었던 코드도 한 단계 늦게 처리되는 비효율이 발생합니다.
개선된 방식
Promise.try(() => thirdParty.doThing())
.then(result => processResult(result))
.catch(err => handleAnyFailure(err));
이제 동기적으로 던져진 예외, 프로미스가 실패(reject)하여 발생한 비동기 에러, 일반 값 등 어떤 반환값 형태든 단 하나의 .then 및 .catch 체인으로 자연스럽게 통일됩니다.
기존의 Promise.resolve().then(...) 우회법과 결정적으로 다르게, Promise.try는 가능하다면 일단 콜백을 즉시 동기적으로 실행시킵니다. 실제로 프로미스가 반환되었을 때 비로소 비동기 연산으로 흐름을 태웁니다.
개발 중에 마이크로태스크 실행 순서를 꼼꼼하게 따질 필요는 전혀 없으니 이것만 기억하시면 됩니다. 형식을 가리지 않는 미지의 함수를 호출할 때 일관된 프로미스로 뽑아 처리하는 가장 안전하고 깔끔한 표준 도구가 바로 Promise.try라는 점입니다.
상태 ES2025 반영 완료. 크롬 136 이상, Node.js 24 이상, 파이어폭스 134 이상, 사파리 18.2 이상 지원.
이 기능 역시 처음에 제안이 나온 것이 자그마치 15년 전입니다.
사용자가 폼에 입력한 텍스트로 정규식(RegExp)을 동적 빌드할 때, 정규식 특수 문자(., *, +, (, [, ? 등)들이 정규식 기호가 아닌 리터럴 문자 그대로 매치되게 하려면 이스케이프 처리가 필요합니다. 예를 들어 사용자가 "file.txt"를 검색할 때 마침표(.)를 이스케이프 하지 않으면 .이 '아무 글자 하나'를 뜻하므로 "fileAtxt"나 "file!txt"까지 검색되는 부작용이 발생합니다.
기존 방식 (다들 복사해서 쓰던 스택오버플로우 출신 이스케이프 함수)
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const userInput = 'file.txt';
const pattern = new RegExp(escapeRegex(userInput));
프로젝트마다 저마다의 헬퍼 함수를 선언해 썼는데, 누락된 매칭 규칙이 있거나 에지 케이스(edgecase)를 누락하는 등 자잘한 버그가 상존했습니다.
개선된 방식
const userInput = 'file.txt';
const pattern = new RegExp(RegExp.escape(userInput));
// "file.txt"라는 글자 그대로 안전하게 매칭됩니다.
여기서 자잘한 디테일이 있습니다. RegExp.escape("foo.bar")의 실행 결과가 우리가 예상하는 단순 백슬래시 처리된 "foo\\.bar"가 아닙니다.
실제로는 첫 글자를 항상 16진수로 이스케이프(hex-escaped) 처리하여 "\\x66oo\\.bar" 형태로 반환합니다.
이는 이스케이프된 문자열을 정규식 패턴 중간에 꽂아 넣었을 때 혹시나 다른 정규식 구문과 예기치 않게 얽혀 엉뚱하게 파싱되는 여지를 완전히 차단하기 위한 의도적인 설계입니다.
이러한 로직 구조를 직접 머리 싸매고 구현할 필요 없이 표준 API가 알아서 매끄럽게 처리해 주므로 마음 놓고 사용하시면 됩니다.
상태 ES2025 반영 완료. 크롬 135 이상, Node.js 24 이상, 파이어폭스 133 이상, 사파리 18.2 이상 지원.
16비트 반정밀도 부동소수점을 다루는 새로운 타입화 배열(TypedArray)입니다. 기존의 Float32Array에 비해 절반의 메모리만 점유합니다.
TensorFlow.js 같은 AI 라이브러리를 직접 작성하거나, WebGPU용 셰이더 프로그래밍을 다루거나, 혹은 대량의 데이터를 수송하는 HDF5/NetCDF 등의 포맷을 분석할 때 매우 중요한 타입입니다. 해당 도메인들은 이미 내부적으로 메모리 절약과 GPU 처리를 위해 float16을 표준 데이터 타입으로 널리 사용하고 있기 때문입니다.
물론 대다수의 전형적인 웹 프런트엔드 애플리케이션 개발에서는 일평생 직접 마주할 일은 거의 없습니다. (저도 그렇습니다.)
상태 두 제안 모두 ES2025 탑재 완료. 모든 메이저 브라우저 및 Node.js 지원.
Temporal.Duration 객체의 시간을 지역의 언어 습관에 맞게 출력해 주는 포매터입니다. (예: 영어권의 '2 hours, 15 minutes', 한국어권의 '2시간 15분') 앞으로 다가올 Temporal 스펙과 단짝처럼 쓰입니다.weekInfo, hourCycles, getCalendars 등)를 서드파티 조회용 라이브러리 없이 브라우저 내장 함수로 손쉽게 판단해 줍니다.다국어 인터내셔널라이제이션(i18n)을 무겁게 구현해야 하는 제품을 서비스 중이시라면 이 두 정적 포매터와 정보 접근자가 이번 명세 전체를 통틀어 가장 유용하게 와닿으실 겁니다.
TC39 위원회는 2026년 4월에 ES2026 후보 명세를 승인했습니다. 6월에 열릴 Ecma 총회의 최종 비준 단계만 남겨놓은 상태이며, 여기서 세부 규칙이 번복될 가능성은 사실상 제로에 가깝습니다. 총 7개의 자바스크립트 제안이 최종 승인을 받았으며, 상세 목록은 TC39의 finished-proposals.md에서 찾아보실 수 있습니다. 다만 기대를 많이 모았던 Temporal API나 using 키워드는 이번 ES2026에는 등재되지 못했습니다. 그 사유와 현황은 뒤이어 나오는 별도의 문단에서 따로 짚어보겠습니다.
상태 ES2026 탑재 예정. Stage 4 달성 완료. 크롬 137 이상 탑재 완료, 파이어폭스 지원 완료, 사파리 및 Node.js 버전에 따라 순차 배포 중.
자바스크립트에서 0.1 + 0.2가 정상적으로 더해지지 않는 소수점 오류는 너무나도 유명합니다.
더 끔찍한 점은, 소수를 많이 담고 있는 배열을 .reduce((a, b) => a + b)로 합산할 때 누적 연산 단계를 타고 소수 연산 오차가 눈덩이처럼 불어나기 쉽다는 것입니다.
기존 방식
// 쇼핑몰 장바구니 내역처럼 수많은 소수(예: 센트 단위 금액)를 순차적으로 합산하는 경우
const cents = Array(10000).fill(0.1);
cents.reduce((a, b) => a + b); // 1000.0000000001588 (오차가 약 1.6e-10 만큼 누적)
// 정밀도가 극단적으로 깨지는 사례
const values = [1e20, 1, -1e20];
values.reduce((a, b) => a + b); // 0 (중간에 1이 오차로 소실됨)
개선된 방식
Math.sumPrecise(cents); // 1000
Math.sumPrecise(values); // 1
Math.sumPrecise는 쉬츄크(Shewchuk) 알고리즘을 사용합니다. 합산 연산 중간과정에 발생한 오차값의 발생 추이를 역추적해 누계액의 최종 결과에 정확하게 보정하는 방식입니다.
첫 번째 사례가 바로 일반적인 개발자들이 가장 흔하게 맞닥뜨리는 실생활의 문제입니다. 소수점 아래 12번째 자리쯤에서 자꾸 자잘한 미세 오차가 삐져나오던 현상을 보정할 수 있습니다. 두 번째는 컴퓨터 부동소수점 명세(float64) 기준 1e20 + 1 === 1e20이라 합산 단계에서 1이 무시되었던 교과서적 정밀도 손실 문제를 원천 차단해 줍니다.
상태 ES2026 탑재 예정. Stage 4 달성 완료. 모든 메이저 브라우저에 이미 순차 배포 완료.
왜 이제야 자바스크립트 표준 구문에 들어왔는지 이해가 안 가던 기능 중 하나입니다.
기존 바이트 배열 데이터를 base64 인코딩 문자열로 교체할 때 쓰던 내장 API btoa는 텍스트 문자열에만 달라붙을 뿐 바이트 단위 배열(Uint8Array)을 곧바로 처리하지 못했습니다. 설상가상으로 라틴 문자 범위를 벗어나는 기호가 있으면 뻗어버리는 심각한 문제도 있었고, 16진수(hex)를 지원하는 표준 네이티브 API 역시 부재했습니다.
기존 방식
// Uint8Array를 base64로 바꾸기 위한 다소 억지스러운 방법
function toBase64(bytes) {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
}
// Uint8Array를 16진수(hex) 문자열로 변환하는 방법
function toHex(bytes) {
return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
}
프로젝트마다 이러한 자잘한 유틸리티 코드들이 어딘가에 꼭 박혀 있었거나 무겁게 서드파티 라이브러리를 의존해야 했습니다.
개선된 방식
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
bytes.toBase64(); // "SGVsbG8="
bytes.toHex(); // "48656c6c6f"
Uint8Array.fromBase64("SGVsbG8=");
Uint8Array.fromHex("48656c6c6f");
암호화 모듈, 파일 업로드 전처리, 웹 크립토(WebCrypto) 계열을 다루는 모든 프로젝트의 헬퍼 폴더 구석에 잠들어 있던 관련 변환 유틸리티 함수들이 이제 언어 수준에서 네이티브로 지원됩니다.
상태 ES2026 탑재 예정. Stage 4 달성 완료. 크롬 135 이상, 파이어폭스 134 이상, 사파리 18.4 이상, Node.js 24 이상 지원.
자바스크립트에서 기존의 instanceof Error 검사는 여러 렐름(realm)을 오고 갈 때 오작동할 위험이 큽니다.
렐름(realm)이란 분리된 자바스크립트의 실행 환경(context)을 뜻합니다. 브라우저의 아이프레임(iframe), 웹 워커(Web Worker), 서비스 워커(Service Worker) 및 Node.js의 vm 모듈 등이 각자의 독립 렐름을 구성하며, Error, Array, Object 등 고유한 빌트인(내장 객체)을 격리하여 따로 지닙니다.
따라서 한 아이프레임 렐름에서 뻗어 나온 에러 객체는 메인 웹 페이지의 렐름 입장에서는 instanceof Error 연산 시 false가 반환됩니다. 둘의 Error 생성자가 서로 다른 고유 인스턴스이기 때문입니다.
기존 방식
// 들어온 객체가 예외 에러인지 식별하려 할 때
function handleError(maybeError) {
if (maybeError instanceof Error) {
// 같은 렐름 환경의 일반 에러에서만 참이 되어 동작합니다.
logger.error(maybeError.message);
} else {
// 웹 워커나 다른 아이프레임에서 넘어온 진짜 Error 객체도 instanceof를 통과하지 못해 이리로 빠지는 참사가 발생합니다.
logger.error('Unknown value thrown:', maybeError);
}
}
이 때문에 서드파티 라이브러리 개발자들은 오래전부터 렐름 붕괴를 대비해 덕 타이핑(duck typing) 스타일의 임시방편 코드(typeof x.message === 'string' && typeof x.stack === 'string')를 주렁주렁 매달아 판별해야 했습니다.
개선된 방식
function handleError(maybeError) {
if (Error.isError(maybeError)) {
logger.error(maybeError.message);
} else {
logger.error('Unknown value thrown:', maybeError);
}
}
Error.isError(new Error('oops')); // true
Error.isError({ message: 'looks like an error' }); // false (진짜 Error 객체가 아님)
Error.isError(errorFromWorker); // true (렐름 경계를 넘어도 제대로 감지함)
라이브러리 단에서 예외 처리를 감싸 로깅을 넘겨주거나, 에러를 재발생(rethrow)시키거나, 래핑 객체를 만드는 기능을 한 번이라도 고안해 보셨다면 이 렐름 문제 때문에 머리가 지끈거리셨을 텐데 이제 원만하게 해결됩니다.
상태 ES2026 탑재 예정. Stage 4 달성 완료. 크롬 및 Node.js 탑재 완료, 기타 브라우저 순차 반영 중.
여러 개의 이터레이터를 하나의 스트림으로 이어서 조작할 수 있게 묶어 줍니다. 여러 제너레이터나 이터러블 데이터들을 하나의 단일 흐름으로 순차 처리하고 싶을 때 매우 요긴합니다.
기존 방식
function* first() { yield 1; yield 2; }
function* second() { yield 3; yield 4; }
// 이터레이터를 묶기 위해 별도의 제너레이터를 새로 정의해야 했습니다.
function* chained() {
yield* first();
yield* second();
}
for (const n of chained()) console.log(n); // 1, 2, 3, 4
개선된 방식
const all = Iterator.concat(first(), second());
for (const n of all) console.log(n); // 1, 2, 3, 4
배열에는 초기부터 .concat() 메서드가 있었던 것처럼, 이제 이터레이터 역시 번잡한 제너레이터 선언문이나 유틸리티 래퍼 구조를 생략하고 곧바로 연계 조작이 가능합니다.
상태 ES2026 탑재 예정. 2026년 1월 TC39 회의에서 Stage 4 통과. 크롬 및 Node.js 핵심 엔진 개발 부서에서 구현 작업 진행 중.
자바스크립트 코드 상에서 이 패턴을 개발할 때마다 '왜 이런 걸 기본 함수로 지원 안 할까' 늘 의아해하던 바로 그 기능입니다.
기존 방식
// 문자열 출현 횟수 계산기
const counts = new Map();
for (const word of words) {
if (!counts.has(word)) counts.set(word, 0);
counts.set(word, counts.get(word) + 1);
}
// 무거운 쿼리 로직의 캐싱 처리
function getUser(id) {
if (!cache.has(id)) {
cache.set(id, expensiveDatabaseLookup(id));
}
return cache.get(id);
}
개선된 방식
const counts = new Map();
for (const word of words) {
counts.set(word, counts.getOrInsert(word, 0) + 1);
}
// 팩토리 함수를 넘겨서 무거운 비동기/연산 지연 연산 캐싱 처리 시
function getUser(id) {
return cache.getOrInsertComputed(id, () => expensiveDatabaseLookup(id));
}
이 정적 메서드들은 일반 Map뿐만 아니라 WeakMap에서도 동등하게 활용할 수 있습니다.
이 제안은 처음엔 emplace나 upsert 같은 이름으로 통용되다 논의 끝에 결국 getOrInsert 및 getOrInsertComputed라는 직관적인 이름을 갖게 되었습니다.
상태 ES2026 탑재 예정. Stage 4 달성 완료. 모든 주요 브라우저 및 Node.js 버전에 반영 완료.
Array.from 메서드의 비동기 버전 파트너입니다. 비동기 이터러블(async iterable) 객체를 돌면서 원소들을 일괄 수거해 하나의 배열로 리턴합니다.
기존 방식
async function* fetchPages() {
let url = '/api/items?page=1';
while (url) {
const res = await fetch(url);
const data = await res.json();
yield* data.items;
url = data.nextPage;
}
}
// 비동기 루프를 직접 돌면서 배열에 적재
const allItems = [];
for await (const item of fetchPages()) {
allItems.push(item);
}
개선된 방식
const allItems = await Array.fromAsync(fetchPages());
상태 ES2026 탑재 예정. Stage 4 달성 완료. 크롬, Node.js, 파이어폭스 배포 완료.
기존 JSON.parse는 엄청나게 큰 정수를 만날 경우 정밀도가 손실되는 고질적인 한계가 있었습니다. JSON의 모든 정수값을 자바스크립트의 표준 숫자 타입(number, 즉 float64 부동소수점)으로 일괄 강제 파싱했기 때문입니다.
999999999999999999을 파싱하면 정밀도가 손실되어 1000000000000000000을 얻게 되고, 100경(quintillion)을 파싱해도 똑같이 잘못 계산된 값을 돌려받게 됩니다.
기존 방식
// 숫자 표현 오차 때문에 원본 보존 불가능
const big = JSON.parse('{"id": 999999999999999999}');
big.id; // 1000000000000000000 (!!)
소수점이나 정수부 표현 자릿수를 명확히 사수해야 하는 데이터 연산 업무에서는 기존 JSON.parse를 무력화하고 수작업으로 파싱을 흉내 내는 json-bigint 같은 별도 라이브러리를 강제 도입해야 했습니다.
개선된 방식
이제 JSON.parse의 리바이버(reviver) 콜백 함수에 세 번째 변수로 context라는 인자가 새로 추가되어 JSON 원본에 기재되었던 생 날것의 원형 문자열(source text)을 참조할 수 있게 되었습니다. 이를 통해 변환 시점을 안전하게 직접 제어할 수 있습니다.
const parsed = JSON.parse(text, (key, value, context) => {
if (typeof value === 'number' && !Number.isSafeInteger(value)) {
return BigInt(context.source); // JSON 파일 내에 기재되었던 텍스트 그 자체가 전달됩니다.
}
return value;
});
정밀도 사수를 위해 json-bigint 같은 무거운 라이브러리를 의존성에 욱여넣거나 JSON.parse 우회 래퍼를 억지로 작성했던 모든 프로젝트의 코드를 이 기능으로 단번에 대체할 수 있습니다.
가장 큰 관심을 한 몸에 받아온 3대 제안인 Temporal API, using 키워드, 그리고 import defer는 아쉽게도 이번 ES2026 최종 스냅샷 규격에서 제외되었습니다. 하지만 이미 브라우저 제조사나 Node.js 엔진 부서들의 실물 탑재 진척도가 매우 높고 성숙도가 상당하므로 짚고 넘어갈 가치가 있습니다. 공식 규격 합류는 아무리 빨라도 ES2027 버전 이후가 될 것입니다.
상태 Stage 4 단계 진입 완료(2026년 3월). ES2026이 아닌 ES2027 명세에 정식 반영될 예정. 파이어폭스는 이미 배포를 완료했고, 크롬 V8 엔진에도 조만간 병합되며, 사파리 개발진도 구현을 절반가량 완료했습니다. 프로덕션 등급에 준하는 유용한 폴리필(temporal-polyfill 및 @js-temporal/polyfill)들이 이미 배포되어 있습니다.
오랫동안 자바스크립트 개발자들의 애증을 자극하던 기존 Date 객체를 완벽히 대체하기 위해 출범한 Temporal API가 마침내 현실로 다가왔습니다.
자바스크립트에서 날짜나 시간 연산을 조작해 보았다면 기존 Date 객체가 가진 수많은 설계 결함을 뼈저리게 체감하셨을 것입니다.
객체 내용이 내부적으로 임의 변이되는 가변성(mutable) 문제, 기형적인 타임존 처리 구조, 월(month)을 의미하는 숫자는 0부터 시작하면서 일(day)을 가리키는 숫자는 1부터 세는 일관성 붕괴 현상, 실행 브라우저 엔진마다 문자열 파싱 결과가 달라 정해진 규칙이 없는 미정의 행위(undefined behavior) 속출 등이 있었습니다. 개발자 샘 로즈(Sam Rose)는 Date가 일으키는 이러한 엉뚱한 연산 행태를 퀴즈로 낸 jsdate.wtf 사이트를 개설하기도 했는데, 퀴즈의 상당수가 파이어폭스와 크롬 브라우저에서 각각 다른 실행 결과를 내뱉습니다.
예를 들어 제가 작년에 맞닥뜨렸던 난감한 사례가 있습니다. 저는 현재 런던에 있고, 다음 주 목요일 시드니 시간 기준으로 오전 9시에 시드니의 동료와 긴급 회의가 잡혔습니다. 제 개인 캘린더에는 이 시간이 런던 현지 시각 기준으로 언제로 기록되어야 할까요?
기존 방식 (정말 골치가 지끈거리던 연산 과정)
// 1단계: "다음 주 목요일" 날짜 계산
const today = new Date();
const daysUntilThursday = (4 - today.getDay() + 7) % 7 || 7;
const nextThursday = new Date(today);
nextThursday.setDate(today.getDate() + daysUntilThursday);
// 2단계: 시드니 시각 기준 오전 9시 매칭
// 하지만 순수 자바스크립트 Date 객체는 타임존 개념이 빈약합니다.
// 결국 moment-timezone이나 date-fns-tz, luxon 같은 무거운 라이브러리를 탑재하거나
// toLocaleString 메서드의 편법을 빌려 수동 오프셋 계산을 비틀어 연산해야 합니다.
보통의 정상적인 프로젝트라면 이쯤에서 고민을 멈추고 냉큼 Moment나 date-fns 라이브러리를 의존성에 새로 추가하며 도망치는 편을 택합니다.
개선된 방식 (Temporal API 사용)
// 회의 예약을 타임존 정보가 온전히 포함된 포맷으로 직접 파싱합니다.
const meeting = Temporal.ZonedDateTime.from(
'2026-04-23T09:00[Australia/Sydney]'
);
// 런던 타임존 기준 시각으로 원클릭 변환합니다.
const inLondon = meeting.withTimeZone('Europe/London');
inLondon.toString();
// "2026-04-23T00:00:00+01:00[Europe/London]"
Temporal API는 국제 규격 ISO 8601 문자열 포맷을 내장 지원하며, 여기에는 타임존 식별 지시어([Australia/Sydney])까지 명확히 포함되어 해석됩니다.
Temporal은 실무에서 날짜 데이터를 조작하는 대표적인 세 가지 유형에 매치되는 전용 타입을 지닙니다. 즉, 순수 날짜만 다루는 PlainDate, 순수 시각만 다루는 PlainTime, 특정 기준 권역과 시차가 결합한 절대적인 순간을 식별하는 ZonedDateTime 입니다.
따라서 값이 UTC 기준 시각인지 혹은 로컬 브라우저 타임존 시각인지 어림짐작할 필요가 전혀 없으며 데이터 타입 자체가 이를 증명합니다.
또한 에지 케이스 처리를 위한 추가적인 타입도 있습니다. 타임존 정보가 없는 PlainDateTime, 타임존 정보가 제거된 절대 순간을 뜻하는 Instant, 생일이나 월별 주기 일정 관리에 안성맞춤인 PlainYearMonth 및 PlainMonthDay 등이 이에 해당합니다.
날짜 간의 간격 연산 및 증감은 각각 전용 메서드인 .since(), .until(), .add(), .subtract()를 제공합니다.
const birthday = Temporal.PlainDate.from('1993-10-26');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthday, { largestUnit: 'years' });
age.toString(); // "P32Y5M24D" (32년 5개월 24일 경과함을 뜻하는 ISO 기간 포맷)
age.years; // 32 (그대로 연령 계산으로 취합 가능)
의존성 번들 크기 축소 메리트도 존재하나, 이는 각 프로젝트에서 현재 사용 중인 라이브러리의 형태에 따라 체감 규모가 다릅니다.
트리 셰이킹(tree shaking)을 일절 허용하지 않아 무거운 Moment.js를 걷어내고 브라우저 네이티브 Temporal로 전격 대체한다면 gzip 압축 기준 약 40KB의 대규모 용량 이득을 챙길 수 있습니다. 다만 이미 구조가 현대적이고 트리 셰이킹이 원활한 date-fns 기반 셋업과 비견할 경우에는 수 KB 수준의 미미한 세이브 효과에 그칩니다.
그보다 더 큰 성과는 플랫폼 단의 개선입니다. 브라우저가 엔진 수준에 Temporal 모듈을 한 번 내장해서 배포해 두면, 모든 웹 서비스들이 번들 비용 지불 없이 전 세계 어디서든 바로 혜택을 봅니다.
using 키워드상태 현재 Stage 3 상태로 ES2026 미탑재. 다만 크롬 134 이상, Node.js 24 이상, Deno 2.0 이상에서 즉시 실물 실행이 가능합니다. 파이어폭스 개발진도 적용 패치를 조율 중이며, 타입스크립트(TypeScript)에서는 이미 5.2 버전부터 구문을 정식 지원 중입니다.
파이썬(Python)의 with 구문, 혹은 C#의 using 구문을 다뤄보셨다면 이미 익숙하실 것입니다. 드디어 자바스크립트에도 스코프 자원 해제용 구문이 입성합니다.
자바스크립트 코딩 시 리소스 반환 처리가 요구되는 입출력 동작(파일 핸들 조작, 데이터베이스 접속 풀 관리 등)을 수행할 때 리소스를 정상적으로 잘 닫았는지 늘 점검해야 했습니다. 깜빡하고 수거하지 않으면 프로세스가 사망할 때까지 백그라운드 메모리가 무한정 누수되고 데이터베이스 접속 자원이 한계에 이릅니다.
기존 방식
// Node.js 데이터베이스 커밋 및 롤백 제어 상황
async function transferMoney(from, to, amount) {
const tx = await db.beginTransaction();
try {
await tx.debit(from, amount);
await tx.credit(to, amount);
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
} finally {
await tx.release(); // 무조건 커넥션을 수거 및 해제해야 합니다.
}
}
긴 함수를 짤 때는 생성 코드와 수거 코드가 문맥상 아주 동떨어진 영역에 분리되어 위치하므로 실수로 해제 구문을 빠뜨리기 쉽습니다. 함수 최상단에서 리소스를 변수에 바인딩하고, 드넓은 함수 본문을 지나 저 아래의 finally 수거 구문에 도달해야 비로소 마침표를 찍을 수 있어 전체적인 가독성을 해치는 원흉이었습니다.
개선된 방식
async function transferMoney(from, to, amount) {
await using tx = await db.beginTransaction();
// scope를 벗어나는 즉시 (함수가 return 혹은 throw를 던져 끝나든 상관없이)
// 자동으로 tx.release()가 비동기로 가동되어 안전하게 회수됩니다.
await tx.debit(from, amount);
await tx.credit(to, amount);
await tx.commit();
}
사후 처리 로직을 변수 선언 시점에 직관적으로 몰아넣었습니다. 함수 연산이 정상 종료되든, 혹은 에러가 터져 튕겨 나가든 스코프 영역을 완전히 이탈하는 그 즉시 할당 자원이 자동 반환되므로 무거운 finally 블록의 필요성이 싹 사라집니다.
동작 원리를 뜯어보면, using으로 선언하여 호출할 리소스 클래스 구조 내부상에 동기 처리를 끝낼 수 있는 [Symbol.dispose]() 메서드가 탑재되어 있거나, 비동기 처리가 수반되는 리소스 회수를 위한 [Symbol.asyncDispose]() 메서드가 구축되어 있어야 합니다(비동기 해제는 선언 시 await using 문법을 사용합니다).
심볼(Symbol)은 기존에 등록된 일반 객체 키 문자열과 충돌을 회피하기 위해 자바스크립트가 제공하는 특수 기호 프로퍼티 키입니다. 이 두 심볼은 이번 using 구문 지원을 위해 새로 등재된 예약어 성격의 웰노운(well-known) 심볼에 해당합니다.
이러한 프로토타입 설계는 라이브러리 제작자들이 미리 구축해 배포할 것이며, 우리는 그저 변수 선언문 앞에 using 수식어만 붙여 즐겁게 사용하면 그만입니다.
여기서 오해하지 말아야 할 것이 있습니다. using은 자바스크립트 언어 규격 그 자체에 편입되는 신기술이며 Node.js 전용 툴이 아닙니다. 자원의 반환이 요구되는 곳이라면 웹 브라우저에서도 정확히 가동되며, 대표적으로 비동기 태스크 취소 처리를 담는 AbortController나 브라우저의 Web Locks API 같은 영역에 즉시 결합할 수 있습니다.
리액트(React)를 구축하는 데도 도움이 될까요? 직접적인 도움은 안 됩니다. 리액트는 이미 이펙트 해제 처리 시 useEffect가 반환하는 클린업 콜백 구조를 잘 구축해 쓰고 있기 때문입니다.
하지만 프레임워크 밖의 전체 영역(API 엔드포인트 서버 핸들러, 프런트엔드 빌드 자동화 스크립트, CLI 유틸리티 등)에서 일관된 리소스 해제를 수행할 때는 using이 표준으로 통용될 것입니다.
또한 동일 스코프 내부에 여러 개의 using 선언문이 나열되어 있을 경우, 자원들의 소멸 시점은 스택(LIFO) 구조를 타며 역순으로 가동됩니다. 즉, A를 열고 이어서 B, C를 열었다면 자원이 해제될 때는 역방향인 C, B, A의 순서대로 콜백이 가동됩니다.
이는 우리가 리소스를 중첩해서 조작하기 위해 try/finally 블록을 안쪽으로 켜켜이 감쌌을 때의 해제 순서 물리 법칙과 자연스럽게 맞아떨어집니다.
상태 현재 Stage 3 상태로 ES2026 규격에서 제외. 타입스크립트는 5.9 버전부터 구문을 공식 지원하며, 바벨(Babel), 웹팩(Webpack), 에스빌드(esbuild) 등의 환경에서도 이미 플러그인을 달아 번역할 수 있습니다. V8 엔진 개발 부서 및 애플 JavaScriptCore 부서에서 실물 병합 작업을 한창 추진 중입니다.
또 하나의 강력한 성능 최적화 카드입니다. 자바스크립트에서 타겟 모듈을 import 지시어로 선언해 가져올 때, 해당 파일의 실제 함수를 사용하든 안 하든 상관없이 그 즉시 모듈 평가(evaluation) 단계를 밟으며 최상위(top-level) 전역 코드들을 모조리 실행해 버리는 특성이 있습니다.
만약 heavy.js라는 파일의 최상위 스코프에 단순 로그 출력문인 console.log('loading heavy') 혹은 무거운 전역 상태 초기화 연산 코드가 박혀 있었다면, 우리의 본문 앱 화면이 렌더링되기도 전에 임포트 선언문 해석 시점에 강제로 즉각 가동됩니다.
모듈 의존성 트리가 층층이 얽혀 거대해질수록 이 초기 전역 코드 해석 부담으로 인해 애플리케이션 초기 구동 성능(TBT, LCP 등)이 곤두박질치는 핵심 원인이었습니다. 사용처를 가리지 않고 초기에 비용을 강제로 지불해야 했기 때문입니다.
import defer는 타겟 파일의 네임스페이스 경로를 일단 확보하되, 실제로 그 내부 프로퍼티(property)를 조회하여 코드상에서 가동하는 바로 그 순간이 올 때까지 실제 파일의 연산 평가 실행을 영리하게 지연시켜 줍니다.
기존 방식 (임포트 즉시 무조건 다 실행)
// rarelyCalled() 함수가 끝내 호출되지 않더라도, 임포트하는 시점에 heavy.js 코드를 강제로 다 평가해 버립니다.
import * as heavyModule from './heavy.js';
function rarelyCalled() {
return heavyModule.doExpensiveThing();
}
개선된 방식
import defer * as heavyModule from './heavy.js';
// heavy.js 파일이 네트워크 등으로 다운로드 및 파싱은 완료되었으나, 아직 전역 코드들은 일절 작동하지 않은 안전한 상태입니다.
function rarelyCalled() {
// 실제 heavyModule.doExpensiveThing 구문을 물리적으로 터치하는 바로 이 타이밍에
// 비로소 heavy.js 본문 코드 및 그 하위 의존성 모듈들의 연산 평가가 즉시 순차 실행됩니다.
return heavyModule.doExpensiveThing();
}
여기에는 주의해야 할 두 가지 주요 제약사항이 동반됩니다.
첫째, 오직 네임스페이스 임포트 형태(import defer * as x)로만 선언할 수 있습니다.
특정 명칭을 골라 꺼내는 네임드 임포트(import defer { foo } from ...)나 디폴트 임포트는 불가능합니다. 전체를 통으로 쥐고 있는 네임스페이스 프록시 객체의 내부 속성을 가볍게 터치하는 시점을 기준으로 평가 가동 트리거가 촉발되는 원리이기 때문입니다.
평소 import { foo } from './thing' 스타일을 즐겨 쓰셨다면, 이를 지연 평가로 전환하기 위해서는 일단 import defer * as thing from './thing'으로 선언 방식을 고치고 호출처를 thing.foo 형태로 한 단계 우회하여 수정해주어야 합니다.
둘째, 모듈 최상위 스코프에 await 지시어가 포함된 모듈(top-level await)은 지연시킬 수 없습니다. 이 연산 지연 구조는 동기식 연산만을 전제로 돌아가기 때문에, 비동기 처리가 엮여 있다면 결국 기존의 동적 import() 구문 구조를 취해야만 합니다.
동적 임포트인 import() 구조와 혼동하지 마시기 바랍니다. 동적 임포트는 평가 결과물로 프로미스(Promise) 객체를 돌려주기 때문에 호출 체인의 부모 영역들을 강제로 async 비동기 처리로 리팩터링해야 하는 난감함이 있었습니다. 반면 import defer는 철저하게 동기식 흐름을 사수해 줍니다.
네임스페이스는 일종의 대리인(proxy) 역할을 수행합니다. 동기식 코드 진행 도중 임의의 프로퍼티에 접근하는 즉시 연산의 가동 스위치가 켜집니다.
블룸버그(Bloomberg) 터미널 개발에 기여하고 있는 TC39 공동 의장 롭 팔머(Rob Palmer)는 대규모 애플리케이션에서 한 번도 접근하지 않을 수도 있는 의존성 모듈들의 콜드 스타트(cold-start) 속도 저하 걱정 없이 임포트 문을 편안하게 꽂아 쓸 수 있도록 돕는 것이 이 명세의 핵심 가치라고 덧붙였습니다.
가장 큰 염원을 얻고 있는 제안 중 일부는 아쉽게도 아직 표준 진입 문턱을 넘지 못했습니다.
데코레이터(Decorators)는 2022년부터 줄곧 Stage 3에 주저앉아 있습니다. 타입스크립트 및 바벨 환경을 구축해 실무에서 널리 가져다 쓰고 있지만, 클래스 내부 멤버(fields)의 생성 순서 꼬임 문제나 데코레이터 메타데이터 가공 에지 케이스로 인해 조율이 지연되는 상황입니다. 타입스크립트 5 이상에서는 네이티브 컴파일러 사양으로 마음껏 사용이 가능하지만, 브라우저가 직접 네이티브 사양으로 데코레이터 구문을 가동하는 시점은 아직 불투명합니다.
레코드와 튜플(Records and Tuples)(객체나 배열처럼 생긴 불변 기본형 데이터 명세) 제안은 완전히 고사되어 폐기되는 수순을 밟고 있습니다. 이에 대한 실용 대안으로 규모를 한층 더 콤팩트하게 덜어낸 '컴포지트(Composites)'라는 제안이 현재 위원회 산하에서 우회 조율을 시도하고 있습니다.
파이프라인 연산자(Pipeline operator)(|>) 역시 수년째 'Stage 2 진입 직전'에 얼어붙어 있습니다. 인자를 꽂아 넣을 위치 예약어로 % 기호를 채택할 것인가, 혹은 토픽 스타일의 커스텀 바인딩을 적용할 것인가를 두고 벌어진 지루한 정치 논쟁 때문에 진척이 더딥니다.
패턴 매칭(Pattern matching)은 현재 기틀만 마련한 Stage 1 단계에 속해 있으며, 아무리 빨라도 ES2027 이전에는 구체적인 윤곽을 보기 힘듭니다.
비동기 이터레이터 헬퍼(Async iterator helpers)(비동기 이터러블의 .map(), .filter(), .take(), .toArray() 및 동기 이터레이터를 비동기 형상으로 풀어주는 Iterator.prototype.toAsync())는 현재 Stage 2 단계입니다. ES2025에 탑재된 동기 헬퍼와 완벽히 닮은꼴에 단지 호출 과정만 await 비동기를 고려해 고안된 설계 사양입니다. 비동기 헬퍼가 들어올 때까지는 비동기 리소스 연산(streaming fetch, LLM 토큰 스트림, 비동기 제너레이터 등)을 가공할 때 여전히 for await...of 구문으로 직접 루프문을 짜야만 합니다. 개인적으로 이 기능의 동향을 가장 유심히 추적하고 있는데, 바로 이 기능이 사수되어야 앞서 말씀드린 LLM 토큰 스트림 조작 유틸리티 코드를 완전 무결하게 한 줄로 줄여낼 수 있기 때문입니다.
Iterator.range(수작업 제너레이터 없이 수치 범위를 지연 순회하는 범위 순회 생성자로, Iterator.range(1, 100) 같은 간결한 표현식 작성 지원) 역시 Stage 2 단계에 지루하게 갇혀 있습니다. 다들 도입을 아우성치고 있지만 금방 실현될 조짐은 보이지 않으므로 당분간 기대를 내려놓으시는 편이 낫습니다.
AsyncContext(비동기 콜백 경계를 유연하게 관통하여 컨텍스트 값을 동시 전달하는 사양으로 Node.js의 AsyncLocalStorage와 동급 사양)는 현재 Stage 2 단계에 포진되어 있습니다. 다만 성능 모니터링 시스템이나 옵서버빌리티(observability) 추적 도구 벤더사들의 강력한 지지를 받으며 개발에 탄력을 얻고 있습니다. 앞으로 주목해 볼 가치가 충분합니다.
만약 개발 중에 클로드 코드(Claude Code), 코파일럿(Copilot), 커서(Cursor) 등의 AI 코딩 도구를 일상적으로 교환해 가며 쓰고 계신다면, 이 인공지능 모델들의 대량 학습 데이터가 이 최신 규격들이 도입되기 수년 전의 오래된 자바스크립트 소스 코드들에 깊게 쏠려 있다는 사실을 꼭 기억하셔야 합니다.
그래서 AI에 단순히 '소수의 총합을 구하는 코드'를 짜달라고 부탁하면 기계적으로 .reduce((a, b) => a + b) 스타일을 작성해 내밉니다. 날짜 연산 로직을 부탁하면 학습 데이터에 Temporal API가 전무했던 탓에 당연하다는 듯이 생 날것의 new Date() 조작 코드를 짜주거나 굳이 lodash 의존성을 무겁게 연결해 줍니다. NodeList를 다루면 일단 배열로 성급하게 리빌딩하려 들며, 스코프의 커넥션 회수는 고리타분하게 try/finally 구문으로 도배해 줍니다.
이러한 출력 코드들이 당장 오동작을 일으키는 틀린 답안은 아니지만, 이는 엄연히 2026년 기준의 현대적 솔루션에 대비하면 2022년도 시점에 묶인 고루한 코딩 스타일입니다.
저도 최근 클로드 코드를 조작해 작업하면서 이 한계를 수차례 체감했습니다. 자잘한 가공 유틸리티 코드를 부탁했더니 한참 긴 코드가 튀어나왔고, 코드를 가만히 들여다보니 '이거 getOrInsert 한 번이면 단 두 줄로 구현될 텐데'라거나 '이건 오래된 Moment의 중첩 포맷인데, Temporal을 쓰면 깔끔하게 해결되는 문제인데' 하는 아쉬움이 스쳤습니다. 인공지능 모델들의 학습 컷오프(cut-off) 시점이 이번 ES2025 표준들이 탑재되기 한참 전으로 고정되어 있어서, 이들이 평소 배운 가장 자연스러운(?) 코딩 형태란 결국 3~5년 전의 낡은 역사적 유산들뿐이기 때문입니다.
이 문제를 타개하기 위해 두 번의 명령어 입력만으로 AI의 코딩 습관을 최신으로 바로잡아 줄 수 있는 'ES2025/ES2026 선호도 셋업' 스킬을 패키지 형태로 만들어 보았습니다.
이는 개발자들이 널리 애용하는 react-tips-skill 플러그인의 일부입니다. 인공지능 모델에게 '만약 코드가 X라는 고루한 스타일을 지향하고 있다면, 이를 지우고 Y라는 현대 자바스크립트 네이티브 표준 API로 일괄 교정하라'는 대조표(lookup table) 역할을 해줍니다.
아래 명령어로 스킬 마켓플레이스를 추가하고 플러그인을 직접 다운로드해 설치해 보시기 바랍니다.
/plugin marketplace add Cst2989/react-tips-skill
/plugin install react-tips@neciudan.dev
설치를 끝내면, 클로드가 자바스크립트 파일을 파싱하거나 리뷰를 시작할 때마다 이 modern-js 최적화 룰셋 스킬이 상시 자동으로 가동됩니다. 원한다면 대화 도중 직접 명시적으로 /react-tips:modern-js 지시어를 날려 가동을 강제할 수도 있습니다.
이 플러그인 스킬은 최종 코드를 뽑아내기 직전, 후보 코드를 이 최신 대조군 맵과 비교해 필터링하는 규칙을 AI 엔진에 강제 부여합니다. 이제 클로드에 '배열 내의 단어 출현 횟수를 수집하라'고 요청하면 매번 기계처럼 만들던 map.has(word) ? map.set(word, map.get(word) + 1) : map.set(word, 1) 식의 귀찮은 삼항 연산문 대신 map.getOrInsert(word, 0) + 1이라는 우아한 현대식 구문으로 첫 답안을 가져옵니다.
클로드 코드가 아닌 일반 에디터를 활용 중이어도 방법은 동일합니다. 이 스킬의 내부 핵심 설계 원천은 단순 마크다운 지침 텍스트이므로, 해당 텍스트를 복사하여 에디터 내에 있는 .cursorrules, Copilot 지침 설정 파일 및 기타 시스템 프롬프트(system prompt)에 그냥 꽂아 넣으시면 됩니다. 아래는 핵심적인 설정 텍스트만 집약해 정리한 콤팩트 규격입니다.
# Modern JavaScript preferences (ES2025/ES2026)
When writing JavaScript, prefer the following newer APIs over their
older equivalents. Check every function you write against this list
before finalizing.
## Iterators and collections
- Iterating a large/infinite sequence?
→ Use Iterator.prototype methods (.map, .filter, .take, .drop,
.toArray) Instead of converting to an array first.
- Wrapping a NodeList, Set, or Map to use array methods?
→ Iterator.from(x).map(...) instead of [...x].map(...) or
Array.from(x).map(...).
- Set intersection, union, difference?
→ a.intersection(b), a.union(b), a.difference(b).
→ Never write a manual loop or reach for lodash.
- Concatenating iterators?
→ Iterator.concat(a, b) instead of a nested yield* generator.
- Counting occurrences in a Map, or caching expensive lookups?
→ map.getOrInsert(key, default) or
map.getOrInsertComputed(key, () => compute()).
→ Never write: if (!map.has(k)) map.set(k, v).
## Dates and times
- Any date/time operation more complex than Date.now()?
→ Use Temporal (Temporal.PlainDate, Temporal.ZonedDateTime, etc.).
→ Never reach for moment.js, date-fns, or luxon for new code.
- Parsing a date with timezone?
→ Temporal.ZonedDateTime.from('2026-06-15T09:00[America/New_York]').
- Computing age or duration?
→ someDate.since(otherDate, { largestUnit: 'years' }).
## Promises and async
- Calling a function that might be sync or async and might throw?
→ Promise.try(() => fn()) instead of new Promise(r => r(fn()))
or Promise.resolve().then(fn).
- Collecting an async iterable into an array?
→ await Array.fromAsync(asyncIter) instead of for-await-push loop.
## Resource cleanup
- Opening a resource that needs cleanup (transaction, file handle,
lock, subscription)?
→ using handle = openResource(); (for sync cleanup)
→ await using handle = await openResource(); (for async)
→ The resource must implement [Symbol.dispose] or
[Symbol.asyncDispose].
→ Never write try/finally for cleanup when using works.
## Errors
- Checking if a caught value is an Error?
→ Error.isError(x) instead of x instanceof Error.
→ instanceof is unreliable across realms (Workers, iframes, vm).
## Numbers
- Summing an array of floats?
→ Math.sumPrecise(values) instead of values.reduce((a, b) => a + b).
→ Especially for financial values or long arrays.
- Encoding/decoding bytes?
→ bytes.toBase64(), bytes.toHex(), Uint8Array.fromBase64(str).
→ Never use btoa/atob for byte arrays; they only work on strings.
## Regular expressions
- Building a regex from user-controlled input?
→ new RegExp(RegExp.escape(input)) instead of a custom escape fn.
## Modules
- Importing JSON?
→ import data from './data.json' with { type: 'json' }.
→ Never use fetch for bundle-time JSON.
- Importing a large module that's rarely used in the current path?
→ import defer * as heavy from './heavy.js'.
→ Works only with namespace imports, not named or default.
## Rules
- NEVER suggest moment.js for new code. Suggest Temporal.
- NEVER write instanceof Error in library code. Use Error.isError.
- NEVER write try/finally for cleanup when using works.
- NEVER write a manual for a for-await-of loop just to collect into an
array; use Array.fromAsync.
- ALWAYS check if the user's runtime supports these features before
suggesting them; if they don't, suggest a polyfill.
이 설정 구문을 심은 뒤, 인공지능 도구에 '배열에서 각 단어의 빈도수를 세는 알고리즘을 설계하라'거나 '생년월일을 기준으로 경과 날짜를 구하라' 같은 지시를 보내 룰셋이 정상 가동되는지 점검해 보시기 바랍니다. 룰셋이 없을 때는 당연히 낡은 레거시 패턴을 토해내지만, 해당 프롬프트 지시어 룰셋이 활성화된 상태에서는 높은 확률로 Map.getOrInsert 및 Temporal.PlainDate 구문을 선택하여 결과를 생성합니다.
이 룰은 프로젝트 실행 대상 환경에서 미지원하는 사양까지 무리하게 쓰도록 모델을 억압하려는 의도가 아닙니다. 단지 인공지능이 매번 낡은 과거 유산 코드만 최우선으로 검토하던 고질적인 학습 불균형을 해결하고, 매력적인 현대 자바스크립트의 표준 편의 기능들을 우선 검토 리스트 최상단에 올려두도록 뇌를 튜닝하는 것입니다.
Iterator.prototype에 새로 추가된 메서드 제안 표준 저장소using 키워드 제안 표준 저장소import defer 제안 표준 저장소🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!
매년 올려주는 사람들 많아서 참 좋아.