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/await | ES2017 | 비동기 문법 혁신 |
Object.entries/values | ES2017 | 객체 순회 편의 |
String.padStart/padEnd | ES2017 | 문자열 패딩 |
| 객체 스프레드/나머지 | ES2018 | 객체 병합/분해 |
Promise.finally | ES2018 | 정리 작업 |
for await...of | ES2018 | 비동기 순회 |
Array.flat/flatMap | ES2019 | 중첩 배열 처리 |
Object.fromEntries | ES2019 | 항목 → 객체 변환 |
String.trim{Start,End} | ES2019 | 공백 제거 |
| Optional catch binding | ES2019 | catch 변수 생략 |
Optional Chaining ?. | ES2020 | 안전한 프로퍼티 접근 |
Nullish Coalescing ?? | ES2020 | null 기본값 |
| BigInt | ES2020 | 대형 정수 |
Promise.allSettled | ES2020 | 전체 완료 대기 |
globalThis | ES2020 | 전역 객체 통일 |
String.matchAll | ES2020 | 정규식 전체 매칭 |
고수 팁
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"}'