Skip to main content
Advertisement

ES2017~ES2020 주요 기능

ES2017부터 ES2020까지 도입된 주요 기능들을 살펴봅니다. 이 시기에 async/await, Optional Chaining, Nullish Coalescing 등 현대 JavaScript의 핵심 기능들이 추가되었습니다.


ES2017 (ES8)

async/await

비동기 코드를 동기식으로 작성할 수 있게 해주는 문법입니다. (6장에서 상세 설명)

// Promise 방식
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.catch(err => console.error(err));
}

// async/await 방식
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (err) {
console.error(err);
}
}

Object.entries() / Object.values()

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

// Object.entries: [키, 값] 쌍의 배열
const entries = Object.entries(user);
// [['name', 'Alice'], ['age', 30], ['city', 'Seoul']]

// 객체를 Map으로 변환
const map = new Map(Object.entries(user));

// 객체 순회
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}

// 필터링 후 새 객체
const filtered = Object.fromEntries(
Object.entries(user).filter(([_, v]) => typeof v === 'string')
);
// { name: 'Alice', city: 'Seoul' }

// Object.values: 값만 배열로
const values = Object.values(user); // ['Alice', 30, 'Seoul']

// 합계 계산
const scores = { math: 90, english: 85, science: 92 };
const avg = Object.values(scores).reduce((a, b) => a + b) / Object.values(scores).length;

String.padStart() / padEnd()

// padStart(목표길이, 채울문자)
'5'.padStart(3, '0'); // '005'
'42'.padStart(5, '0'); // '00042'
'hello'.padStart(10); // ' hello' (기본값: 공백)

// padEnd
'hello'.padEnd(10, '.'); // 'hello.....'
'100'.padEnd(8, '%'); // '100%%%%%'

// 실전: 포맷팅
function formatTime(hours, minutes, seconds) {
return [hours, minutes, seconds]
.map(n => String(n).padStart(2, '0'))
.join(':');
}
formatTime(9, 5, 3); // '09:05:03'
formatTime(14, 30, 0); // '14:30:00'

// 숫자 정렬을 위한 패딩
const items = [
{ id: 1, name: 'Apple' },
{ id: 42, name: 'Banana' },
{ id: 100, name: 'Cherry' }
];
items.forEach(({ id, name }) => {
console.log(`${String(id).padStart(3)}: ${name}`);
});
// 1: Apple
// 42: Banana
// 100: Cherry

Object.getOwnPropertyDescriptors()

const source = {
get name() { return 'Alice'; },
set name(value) { this._name = value; }
};

// 기존 Object.assign은 getter/setter를 복사하지 못함
const copy1 = Object.assign({}, source);
// getter/setter가 호출되어 값만 복사됨

// getOwnPropertyDescriptors로 완전한 복사
const copy2 = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
// getter/setter 포함 완전 복사

// 믹스인 패턴에서 유용
const mixin = (target, ...sources) => {
sources.forEach(source => {
Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
});
return target;
};

ES2018 (ES9)

객체 스프레드/나머지 연산자

// 객체 스프레드 (이전에는 배열만 가능)
const defaults = { theme: 'light', language: 'ko', fontSize: 16 };
const userPrefs = { theme: 'dark', fontSize: 18 };

// 합치기 (뒤에 오는 것이 우선)
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', language: 'ko', fontSize: 18 }

// 객체 나머지 (비구조화에서)
const { theme, ...rest } = settings;
// theme: 'dark', rest: { language: 'ko', fontSize: 18 }

// 실전: Redux 스타일 불변 업데이트
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.id
? { ...user, ...action.updates }
: user
)
};
default:
return state;
}
}

Promise.finally()

// 성공/실패 상관없이 항상 실행
async function uploadFile(file) {
showProgressBar();

try {
const result = await fetch('/api/upload', {
method: 'POST',
body: file
});
return await result.json();
} catch (err) {
showErrorMessage(err.message);
throw err;
} finally {
hideProgressBar(); // 항상 실행
cleanupTempFiles(); // 항상 실행
}
}

// 체이닝
fetch('/api/data')
.then(r => r.json())
.then(processData)
.catch(handleError)
.finally(() => {
isLoading = false;
updateUI();
});

for-await-of

비동기 이터러블 순회 (6장에서 상세 설명)

// 비동기 API 페이지네이션
async function* getPages(url) {
let nextUrl = url;
while (nextUrl) {
const { data, next } = await fetch(nextUrl).then(r => r.json());
yield* data;
nextUrl = next;
}
}

for await (const item of getPages('/api/items')) {
process(item);
}

정규표현식 개선

// Named Capture Groups (ES2018)
const dateStr = '2024-03-15';
const { groups: { year, month, day } } =
dateStr.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);

console.log(year, month, day); // 2024 03 15

// Lookbehind Assertion
// ?<= : 양의 후방탐색
// ?<! : 음의 후방탐색
const prices = ['$100', '€200', '£300'];
const usd = prices.filter(p => /(?<=\$)\d+/.test(p)); // ['$100']

// dotAll 플래그 (s)
const multiline = 'Hello\nWorld';
/Hello.World/.test(multiline); // false (기본 .은 줄바꿈 매칭 안 함)
/Hello.World/s.test(multiline); // true (s 플래그로 해결)

ES2019 (ES10)

Array.flat() / flatMap()

// flat(): 중첩 배열 평탄화
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]] - 1단계만
nested.flat(2); // [1, 2, 3, 4, 5, 6] - 2단계
nested.flat(Infinity); // [1, 2, 3, 4, 5, 6] - 완전 평탄화

// flatMap(): map + flat(1) 결합
const sentences = ['Hello World', 'Foo Bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'World', 'Foo', 'Bar']

// 기존 방식 (비효율적)
const words2 = sentences.map(s => s.split(' ')).flat();

// 실전: API 응답 중첩 배열 처리
const categories = [
{ name: 'Fruits', items: ['Apple', 'Banana'] },
{ name: 'Veggies', items: ['Carrot', 'Potato'] }
];

const allItems = categories.flatMap(cat => cat.items);
// ['Apple', 'Banana', 'Carrot', 'Potato']

// 빈 배열로 필터링 효과
const numbers = [1, 2, -3, 4, -5];
const positiveDoubled = numbers.flatMap(n => n > 0 ? [n * 2] : []);
// [2, 4, 8]

Object.fromEntries()

// Map → 객체
const map = new Map([['name', 'Alice'], ['age', 30]]);
const obj = Object.fromEntries(map);
// { name: 'Alice', age: 30 }

// [키, 값] 배열 → 객체
const entries = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(entries); // { a: 1, b: 2, c: 3 }

// 객체 변환 파이프라인
const prices = { apple: 1000, banana: 500, cherry: 2000 };

// 모든 가격에 10% 할인
const discounted = Object.fromEntries(
Object.entries(prices).map(([key, value]) => [key, value * 0.9])
);
// { apple: 900, banana: 450, cherry: 1800 }

// URLSearchParams → 객체
const params = new URLSearchParams('name=Alice&age=30&city=Seoul');
const paramsObj = Object.fromEntries(params);
// { name: 'Alice', age: '30', city: 'Seoul' }

String.trimStart() / trimEnd()

const str = '   Hello World   ';

str.trim(); // 'Hello World' (양쪽)
str.trimStart(); // 'Hello World ' (앞만)
str.trimEnd(); // ' Hello World' (뒤만)

// 별칭: trimLeft/trimRight (비표준이었던 것 표준화)
str.trimLeft(); // trimStart와 동일
str.trimRight(); // trimEnd와 동일

// 실전: 사용자 입력 처리
function parseCSV(line) {
return line.split(',').map(cell => cell.trim());
}
parseCSV('Alice , 30 , Seoul '); // ['Alice', '30', 'Seoul']

Optional catch binding

// 이전: catch 변수가 필요 없어도 선언해야 했음
try {
JSON.parse(invalidJson);
} catch (e) { // e를 사용하지 않아도 선언 필요
isValidJson = false;
}

// ES2019: 변수 생략 가능
try {
JSON.parse(invalidJson);
} catch { // e 없이 사용 가능
isValidJson = false;
}

// 실전 예시
function isJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}

Array.prototype.sort 안정성 보장

// ES2019부터 모든 JS 엔진에서 안정적 정렬 보장
const users = [
{ name: 'Charlie', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];

// age로 정렬할 때 같은 age이면 원래 순서 유지 (안정 정렬)
users.sort((a, b) => a.age - b.age);
// Charlie와 Bob은 age가 같으므로 원래 순서(Charlie → Bob) 유지

ES2020 (ES11)

Optional Chaining (?.)

중첩 객체 접근 시 null/undefined 에러를 방지합니다.

const user = {
profile: {
address: {
city: 'Seoul'
}
}
};

// 기존 방식
const city1 = user && user.profile && user.profile.address && user.profile.address.city;

// Optional Chaining
const city2 = user?.profile?.address?.city; // 'Seoul'
const zip = user?.profile?.address?.zip; // undefined (에러 없음)

// 메서드 호출
user?.profile?.getFullAddress?.(); // 메서드가 없어도 안전

// 배열 접근
const firstTag = user?.tags?.[0]; // 배열이 없어도 안전

// 동적 프로퍼티
const key = 'name';
const value = user?.profile?.[key];

// Optional Chaining + Nullish Coalescing 조합
const displayName = user?.profile?.name ?? '익명';
const userCity = user?.address?.city ?? '위치 미설정';

// 함수 호출에서
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response?.json(); // response가 null이면 undefined 반환
return data?.user ?? null;
}

Nullish Coalescing (??)

// ?? : null 또는 undefined일 때만 기본값 사용
// || : falsy 값(0, '', false, null, undefined)일 때 기본값 사용

const count = 0;
const name = '';

// || 문제
count || 10; // 10 (count가 0이라 falsy → 원하지 않는 결과)
name || '기본'; // '기본' (빈 문자열도 falsy)

// ?? 해결
count ?? 10; // 0 (null/undefined가 아니므로 원래 값 유지)
name ?? '기본'; // '' (빈 문자열도 유효한 값)

// 실전
const userSettings = {
volume: 0, // 0이 유효한 값
username: '', // 빈 문자열이 유효한 값
timeout: null, // null은 기본값 사용
debug: undefined // undefined는 기본값 사용
};

const volume = userSettings.volume ?? 50; // 0
const username = userSettings.username ?? 'Guest'; // ''
const timeout = userSettings.timeout ?? 5000; // 5000
const debug = userSettings.debug ?? false; // false

// ??= (Nullish Assignment) - ES2021
userSettings.timeout ??= 5000; // null이므로 5000으로 설정
userSettings.volume ??= 50; // 0이므로 변경 안 함

BigInt

// Number.MAX_SAFE_INTEGER보다 큰 정수 처리
Number.MAX_SAFE_INTEGER; // 9007199254740991

// BigInt 리터럴: 숫자 뒤에 n
const bigNum = 9007199254740992n;
const trillion = 1_000_000_000_000n;

// BigInt 생성자
BigInt(9007199254740992);
BigInt('9007199254740992');

// 연산 (BigInt끼리만 가능)
const a = 100n;
const b = 200n;
a + b; // 300n
a * b; // 20000n
b / a; // 2n (소수점 버림)
b % a; // 0n

// Number와 혼용 불가
// 100n + 100; // TypeError!

// 비교는 가능
100n == 100; // true (동등 비교)
100n === 100; // false (타입 다름)
100n > 99; // true

// 실전: 암호화, 대용량 ID
const id = 9007199254740993n;
const timestamp = BigInt(Date.now());

function factorial(n) {
if (n <= 1n) return 1n;
return n * factorial(n - 1n);
}
factorial(100n); // 엄청난 큰 수

Promise.allSettled()

// 모든 Promise가 완료될 때까지 기다림 (성공/실패 무관)
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/invalid').then(r => r.json()), // 실패
fetch('/api/products').then(r => r.json()),
]);

// 각 결과에 status 포함
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('성공:', result.value);
} else {
console.log('실패:', result.reason);
}
});

// 성공한 것만 필터링
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);

globalThis

// 환경에 상관없이 전역 객체 접근
// 브라우저: window
// Node.js: global
// Web Worker: self

// 이전 방식 (환경마다 다름)
const global = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: this;

// ES2020: globalThis로 통일
globalThis.setTimeout(() => {}, 0);
globalThis.fetch('/api/data');
console.log(globalThis === window); // 브라우저에서 true

// 폴리필 환경 감지
if (typeof globalThis.Proxy === 'undefined') {
// Proxy 폴리필 추가
}

String.matchAll()

// 정규표현식의 모든 매칭 결과 반환 (이터레이터)
const text = '2024-01-15, 2024-06-20, 2024-12-31';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g; // g 플래그 필수

// 기존 exec 방식 (반복적)
let match;
const dates = [];
while ((match = dateRegex.exec(text)) !== null) {
dates.push({ full: match[0], year: match[1], month: match[2], day: match[3] });
}

// matchAll 방식 (간결)
const allMatches = [...text.matchAll(dateRegex)];
const parsedDates = allMatches.map(match => ({
full: match[0],
year: match[1],
month: match[2],
day: match[3],
index: match.index
}));

// Named Groups와 조합
const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
for (const match of text.matchAll(namedRegex)) {
const { year, month, day } = match.groups;
console.log(`${year}${month}${day}`);
}

버전별 요약

기능버전설명
async/awaitES2017비동기 문법 혁신
Object.entries/valuesES2017객체 순회 편의
String.padStart/padEndES2017문자열 패딩
객체 스프레드/나머지ES2018객체 병합/분해
Promise.finallyES2018정리 작업
for await...ofES2018비동기 순회
Array.flat/flatMapES2019중첩 배열 처리
Object.fromEntriesES2019항목 → 객체 변환
String.trim{Start,End}ES2019공백 제거
Optional catch bindingES2019catch 변수 생략
Optional Chaining ?.ES2020안전한 프로퍼티 접근
Nullish Coalescing ??ES2020null 기본값
BigIntES2020대형 정수
Promise.allSettledES2020전체 완료 대기
globalThisES2020전역 객체 통일
String.matchAllES2020정규식 전체 매칭

고수 팁

1. Optional Chaining과 단락 평가

// Optional Chaining은 단락 평가됨
// user?.profile에서 user가 null이면 profile은 평가 안 됨
const result = user?.profile?.expensiveMethod();
// user가 null이면 expensiveMethod() 자체가 호출되지 않음

// 함수 인수도 평가 안 됨
user?.log(expensiveCalculation());
// user가 null이면 expensiveCalculation()도 실행되지 않음

2. Nullish Coalescing Assignment 체인

// 여러 기본값을 연쇄 적용
const config = {};

config.timeout ??= 5000;
config.retries ??= 3;
config.baseUrl ??= 'https://api.example.com';

// 또는 Object.assign과 결합
const defaults = { timeout: 5000, retries: 3 };
const merged = { ...defaults, ...config };

3. BigInt 직렬화 주의

// BigInt는 JSON.stringify 불가
JSON.stringify({ id: 100n }); // TypeError!

// 해결책: toJSON 또는 replacer
const obj = { id: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"100"}'
Advertisement