JavaScript 표준화 단계별 주요 기능 심층 분석

ES2016부터 ES2021까지: 현대 JavaScript의 핵심 문법과 API

JavaScript 언어 사양은 매년 새로운 기능들을 추가하며 진화하고 있습니다. 이 글에서는 ES2016(ES7)부터 ES2021(ES12)에 이르는 각 버전에서 도입된 중요한 문법적 변화와 내장 객체 메서드들의 동작 원리 및 실무 적용 사례를 기술적으로 분석합니다.

ES2016 (ES7): 배열 검색과 연산자 개선

Array.prototype.includes()

includes() 메서드는 특정 값이 배열에 존재하는지 여부를 부울(Boolean) 값으로 반환합니다. 기존 indexOf()와의 가장 큰 차이점은 NaN을 감지할 수 있다는 점이며, 비교 시 타입 변환 없이 엄격한 동등성(strict equality)을 사용합니다.

const data = [10, 20, NaN, 'test'];

// indexOf는 NaN을 찾을 수 없음 (-1 반환)
console.log(data.indexOf(NaN)); // -1

// includes는 NaN을 정확히 식별함
console.log(data.includes(NaN)); // true

// 두 번째 인자로 검색 시작 인덱스를 지정 가능 (음수 허용)
console.log(data.includes('test', 2)); // false (인덱스 2부터 시작하므로 'test'는 인덱스 3에 있어 찾지 못함? 아니, 인덱스 3이므로 true가 나와야 함. 수정: 인덱스 2부터 시작하면 인덱스 3의 'test'를 찾음 -> true)
// 주의: fromIndex가 음수일 경우 배열 끝에서부터 역순으로 계산 후 그 위치부터 순방향으로 탐색합니다.
console.log(data.includes('test', -1)); // true (마지막 요소부터 탐색)

주의사항: 문자열 비교는 대소문자를 구분합니다. 또한, 객체나 중첩된 배열과 같은 참조 타입의 경우 내부 구조가 동일하더라도 참조 주소가 다르다면 false를 반환합니다.

멱승 연산자 (**)

거듭제곱 계산을 위해 Math.pow() 함수 대신 더 간결한 기호 연산자가 도입되었습니다.

// 전통적인 방식
let result1 = Math.pow(2, 10); // 1024

// ES7 멱승 연산자
let result2 = 2 ** 10; // 1024

// 소수점 지수 및 음수 지수 지원
console.log(2 ** -1);   // 0.5
console.log(9 ** 0.5);  // 3

연산자 우선순위 주의: 단항 연산자(-)보다 멱승 연산자(**)의 우선순위가 높습니다. 즉, -2 ** 2는 -(2**2)로 해석되어 -4가 되며, (-2) ** 2로 명시해야 4가 나옵니다.

ES2017 (ES8): 객체 조작과 비동기 처리의 간소화

Object.values() & Object.entries()

객체의 키(Key)만 반환하던 Object.keys()에 이어, 값(Value)과 키-값 쌍을 배열로 반환하는 메서드가 추가되었습니다.

const user = { name: 'Alice', age: 30, city: 'Seoul' };

// 값 배열 반환
console.log(Object.values(user)); 
// ['Alice', 30, 'Seoul']

// 키-값 쌍 배열 반환
console.log(Object.entries(user));
// [['name', 'Alice'], ['age', 30], ['city', 'Seoul']]

String.prototype.padStart() & padEnd()

문자열의 길이를 특정 타겟 길이로 맞추기 위해 앞이나 뒤에 지정된 문자열을 반복하여 채워 넣는 기능입니다.

const id = '123';

// 왼쪽 패딩: 총 길이를 6으로 만들고 부족분은 '0'으로 채움
console.log(id.padStart(6, '0')); // "000123"

// 오른쪽 패딩: 총 길이를 6으로 만들고 부족분은 '.'으로 채움
console.log(id.padEnd(6, '.'));   // "123..."

// 실제 활용 예시: 날짜 포맷팅
const month = 5;
console.log(String(month).padStart(2, '0')); // "05"

async / await

Promise 기반의 비동기 코드를 동기식 코드처럼 작성할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다. 코드의 가독성을 높이고 에러 핸들링을 try-catch 블록으로 통합할 수 있습니다.

function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === 1) resolve({ id: 1, name: 'Alice' });
            else reject(new Error('User not found'));
        }, 1000);
    });
}

async function displayUser() {
    try {
        const user = await fetchUserData(1); // 프로미스가 해결될 때까지 대기
        console.log(user.name); // Alice
        
        // 병렬 실행이 필요한 경우 Promise.all 사용 권장
        // const [u1, u2] = await Promise.all([fetchUserData(1), fetchUserData(2)]);
    } catch (error) {
        console.error(error.message);
    }
}

displayUser();

성능 고려사항: await는 직렬 실행을 의미합니다. 독립적인 여러 요청을 보낼 때 각각을 await하면 전체 시간이 누적됩니다. 이런 경우 Promise.all을 사용하여 병렬 처리 후 결과를 기다리는 것이 효율적입니다.

ES2018 (ES9): 객체 분해와 비동기 반복

Object Rest & Spread

배열에서 사용되던 전개 문법(...)과 나머지 문법이 객체에도 적용되었습니다.

const source = { a: 1, b: 2, c: 3 };

// Spread: 객체 복사 또는 병합
const copy = { ...source, d: 4 }; 
// { a: 1, b: 2, c: 3, d: 4 }

// Rest: 일부 속성을 제외하고 나머지를 변수에 할당
const { a, ...remainingProps } = source;
console.log(a);           // 1
console.log(remainingProps); // { b: 2, c: 3 }

참고: 스프레드 연산자는 얕은 복사(Shallow Copy)를 수행하므로, 중첩된 객체의 참조는 공유됩니다.

for-await-of 루프

비동기 이터레이터(Async Iterator)를 순회하기 위한 구문입니다. 각 항목이 Promise로 반환되는 경우, 해당 Promise가 해결(resolve)될 때까지 다음 반복으로 진행하지 않습니다.

async function processFiles() {
    const filePromises = [
        readFileAsync('file1.txt'),
        readFileAsync('file2.txt')
    ];

    for await (const content of filePromises) {
        console.log(content); // 각 파일 내용이 순차적으로 출력됨
    }
}

Promise.prototype.finally()

프로미스의 성공/실패 여부와 관계없이 반드시 실행되어야 하는 클린업 로직(예: 로딩 상태 해제)을 정의할 때 유용합니다.

showLoadingSpinner();

fetchData()
    .then(result => handleResult(result))
    .catch(err => handleError(err))
    .finally(() => hideLoadingSpinner()); // 항상 실행

ES2019 (ES10): 데이터 변환 유틸리티 확장

Object.fromEntries()

Object.entries()의 역연산으로, 키-값 쌍의 배열을 객체로 변환합니다. Map을 객체로 변환하거나 URLSearchParams를 파싱할 때 유용합니다.

const entries = [['key1', 'value1'], ['key2', 'value2']];
const obj = Object.fromEntries(entries);
// { key1: 'value1', key2: 'value2' }

// Map to Object
const map = new Map([['a', 1], ['b', 2]]);
const mappedObj = Object.fromEntries(map);
// { a: 1, b: 2 }

Array.prototype.flat() & flatMap()

중첩된 배열을 원하는 깊이만큼 평탄화(Flatten)합니다. flatMap()은 map()과 flat(depth=1)을 결합한 메서드로 성능상 유리합니다.

const nested = [1, [2, [3, [4]]]];

// 기본 깊이는 1
console.log(nested.flat());     // [1, 2, [3, [4]]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4]

// flatMap 예시
const sentences = ["Hello world", "Foo bar"];
const words = sentences.flatMap(s => s.split(" "));
// ["Hello", "world", "Foo", "bar"]

Optional Catch Binding

try-catch 블록에서 오류 객체를 사용하지 않을 경우 파라미터 선언을 생략할 수 있습니다.

try {
    JSON.parse(invalidJsonString);
} catch { // err 변수 생략 가능
    console.log("Parsing failed");
}

ES2020 (ES11): 안전 접근과 대규모 정수 지원

Nullish Coalescing Operator (??)

좌측 피연산자가 null 또는 undefined일 때만 우측 값을 반환합니다. 논리적 OR(||)과 달리 0, '', false 등의 페이시(Falsy) 값은 유효한 값으로 취급합니다.

const input = 0;
const defaultVal = 42;

console.log(input || defaultVal); // 42 (0은 falsy이므로 대체됨)
console.log(input ?? defaultVal); // 0  (0은 null/undefined가 아님)

Optional Chaining (?.)

깊게 중첩된 객체 구조에서 중간 단계가 null이나 undefined일 경우 발생하던 TypeError를 방지합니다.

const config = {
    db: {
        host: 'localhost'
    }
};

// 이전 방식
// const port = config.db && config.db.port ? config.db.port : undefined;

// ES2020 방식
const port = config.db?.port; // undefined (안전하게 평가됨)
const host = config.db?.host; // 'localhost'

// 메서드 호출에도 사용 가능
const userName = user?.profile?.getName?.();

BigInt

Number 타입의 최대 안전 정수($2^{53}-1$)를 초과하는 큰 정수를 정확하게 표현하기 위한 새로운 원시 타입입니다.

const maxSafe = Number.MAX_SAFE_INTEGER; // 9007199254740991
const bigNum = BigInt(maxSafe) + 2n;     // 9007199254740993n

// 주의: BigInt와 Number는 직접 혼합 연산이 불가합니다.
// console.log(bigNum + 1); // TypeError

Dynamic Import

모듈을 런타임 시점에 조건부 또는 지연적으로 로드할 수 있게 합니다. 번들 크기를 줄이고 초기 렌더링 속도를 높이는 데 기여합니다.

button.addEventListener('click', async () => {
    const module = await import('./heavyModule.js');
    module.doSomething();
});

ES2021 (ES12): 연산자 축약과 문자열 처리 향상

Logical Assignment Operators (&&=, ||=, ??=)

논리 연산자와 대입 연산자를 결합한 형태입니다.

let x = 1;
x &&= 2; // x가 truthy이면 x에 2를 대입. 결과: x = 2

let y = 0;
y ||= 5; // y가 falsy이면 y에 5를 대입. 결과: y = 5

let z = null;
z ??= 10; // z가 null/undefined이면 z에 10을 대입. 결과: z = 10

String.prototype.replaceAll()

정규식 없이 모든 일치하는 부분을 교체할 수 있습니다. 정규식을 사용할 경우 반드시 /g 플래그가 포함되어야 합니다.

const text = "foo bar foo baz";

// 문자열 패턴으로 모두 교체
console.log(text.replaceAll("foo", "qux")); 
// "qux bar qux baz"

// 정규식 사용 시 global flag 필수
// console.log(text.replaceAll(/foo/, "qux")); // TypeError
console.log(text.replaceAll(/foo/g, "qux"));  // 정상 작동

Numeric Separators (_)

긴 숫자 리터럴의 가독성을 높이기 위해 언더스코어를 구분자로 사용할 수 있습니다.

const budget = 1_000_000_000; // 10억
const pi = 3.141_592_653;     // 파이 근사치

// 실제 값에는 영향 없음
console.log(budget === 1000000000); // true

Promise.any()

여러 프로미스 중 하나라도 성공(fulfilled)하면 즉시 그 결과로 해결됩니다. 모두 실패(rejected)할 때만 AggregateError로 거부됩니다. Promise.race()가 첫 번째 완료 시점(성공/실패 무관)을 반환하는 것과 대비됩니다.

const p1 = new Promise((_, reject) => setTimeout(() => reject('fail1'), 100));
const p2 = new Promise((resolve) => setTimeout(() => resolve('success2'), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('success3'), 300));

Promise.any([p1, p2, p3])
    .then(value => console.log(value)) // 'success2' (첫 번째로 성공한 것)
    .catch(err => console.error(err));

태그: JavaScript ES2016 ES2017 ES2018 ES2019

10월 10일 04:13에 게시됨