ES6 핵심 문법
ES6(ECMAScript 2015)는 JavaScript의 가장 큰 문법 개편이었습니다. 템플릿 리터럴, 화살표 함수, 클래스, 모듈 등 현대 JavaScript의 근간이 되는 기능들이 도입되었습니다.
템플릿 리터럴(Template Literals)
백틱(`)을 사용하는 문자열 표현 방식입니다.
const name = 'Alice';
const age = 30;
// 기존 방식
const msg1 = '안녕하세요, ' + name + '님! 나이: ' + age;
// 템플릿 리터럴
const msg2 = `안녕하세요, ${name}님! 나이: ${age}`;
// 표현식 삽입
const result = `2 + 3 = ${2 + 3}`;
const status = `상태: ${age >= 18 ? '성인' : '미성년자'}`;
const func = `길이: ${name.length}`;
// 멀티라인 문자열
const html = `
<div class="card">
<h2>${name}</h2>
<p>나이: ${age}</p>
</div>
`;
// 중첩 템플릿 리터럴
const items = ['사과', '바나나', '체리'];
const list = `항목 목록:
${items.map((item, i) => ` ${i + 1}. ${item}`).join('\n')}`;
태그드 템플릿 리터럴
// 태그 함수: 템플릿 리터럴을 직접 처리
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1];
return result + (value ? `<mark>${value}</mark>` : '') + str;
});
}
const name = 'Alice';
const score = 95;
const msg = highlight`${name}님의 점수는 ${score}점입니다.`;
// <mark>Alice</mark>님의 점수는 <mark>95</mark>점입니다.
// 실전: SQL 인젝션 방지
function sql(strings, ...values) {
const escaped = values.map(v => escapeSQL(v));
return strings.reduce((query, str, i) => {
return query + (escaped[i - 1] ?? '') + str;
});
}
const username = "Alice'; DROP TABLE users; --";
const query = sql`SELECT * FROM users WHERE name = ${username}`;
// 안전하게 이스케이프됨
// styled-components 방식 (React CSS-in-JS)
const Button = styled.button`
background: ${props => props.primary ? '#007bff' : 'white'};
color: ${props => props.primary ? 'white' : '#007bff'};
padding: 0.5rem 1rem;
`;
기본 매개변수(Default Parameters)
// 기존 방식
function greet(name, greeting) {
name = name || '손님';
greeting = greeting || '안녕하세요';
return `${greeting}, ${name}!`;
}
// ES6 기본 매개변수
function greet(name = '손님', greeting = '안녕하세요') {
return `${greeting}, ${name}!`;
}
greet(); // 안녕하세요, 손님!
greet('Alice'); // 안녕하세요, Alice!
greet('Bob', '하이'); // 하이, Bob!
// 표현식도 기본값으로 사용 가능
function createUser(
name,
id = Math.random().toString(36).slice(2),
createdAt = new Date().toISOString()
) {
return { name, id, createdAt };
}
// 이전 매개변수를 참조 가능
function makeRange(start, end = start + 10) {
return Array.from({ length: end - start }, (_, i) => start + i);
}
makeRange(5); // [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
makeRange(3, 7); // [3, 4, 5, 6]
// undefined만 기본값 트리거 (null은 그대로)
function test(x = 'default') {
return x;
}
test(undefined); // 'default'
test(null); // null
test(0); // 0
test(''); // ''
나머지/전개 연산자 심화
나머지 매개변수(Rest Parameters)
// 마지막 매개변수에만 사용 가능
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// 앞의 매개변수와 조합
function logMessage(level, ...messages) {
console[level](...messages);
}
logMessage('log', '안녕', '세상', '!');
logMessage('error', '에러 발생:', new Error('문제'));
// 비구조화와 나머지
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first: 1, second: 2, rest: [3, 4, 5]
const { name, age, ...others } = { name: 'Alice', age: 30, city: 'Seoul', job: 'Dev' };
// name: 'Alice', age: 30, others: { city: 'Seoul', job: 'Dev' }
전개 연산자(Spread Operator)
// 배열 전개
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const withMiddle = [...arr1, 10, ...arr2]; // [1, 2, 3, 10, 4, 5, 6]
const copy = [...arr1]; // 얕은 복사
// 함수 인수로
Math.max(...arr1); // 3
console.log(...arr1); // 1 2 3
// 문자열 전개
const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
// 객체 전개
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
const overridden = { ...obj1, b: 99, ...obj2 }; // { a: 1, b: 99, c: 3, d: 4 }
// 객체 복사 (얕은 복사)
const original = { name: 'Alice', settings: { theme: 'dark' } };
const copy = { ...original };
copy.name = 'Bob'; // original 영향 없음
copy.settings.theme = 'light'; // original.settings도 변경됨! (참조)
// 실전: 불변 업데이트 패턴 (React state)
const updateUser = (user, updates) => ({ ...user, ...updates });
const updated = updateUser(original, { name: 'Bob', email: 'bob@example.com' });
화살표 함수 심화
// 다양한 형태
const double = x => x * 2;
const add = (a, b) => a + b;
const greet = () => 'Hello!';
const getObject = () => ({ name: 'Alice' }); // 객체는 소괄호로 감싸기
// 여러 줄
const process = (data) => {
const filtered = data.filter(Boolean);
const mapped = filtered.map(x => x * 2);
return mapped;
};
// this 바인딩 차이 - 가장 중요한 특징!
const counter = {
count: 0,
// 일반 함수: 자신만의 this
incrementRegular: function() {
setInterval(function() {
this.count++; // this는 undefined (strict mode) 또는 window
console.log(this.count);
}, 1000);
},
// 화살표 함수: 상위 스코프의 this 상속
incrementArrow: function() {
setInterval(() => {
this.count++; // this는 counter 객체
console.log(this.count); // 올바르게 동작
}, 1000);
}
};
// 클래스에서 이벤트 핸들러
class Button {
constructor() {
this.clickCount = 0;
// 화살표 함수로 this 바인딩 유지
this.handleClick = () => {
this.clickCount++;
console.log(`클릭 ${this.clickCount}회`);
};
}
}
// 화살표 함수를 사용하면 안 되는 경우
const obj = {
name: 'Alice',
// 나쁨: 화살표 함수는 자신의 this가 없음
getName: () => this.name, // undefined
// 좋음
getName() {
return this.name; // 'Alice'
}
};
단축 메서드 표기법
// 객체 메서드 단축 표기
const user = {
name: 'Alice',
age: 30,
// 기존 방식
greet: function() {
return `안녕하세요, ${this.name}!`;
},
// 단축 메서드
greet() {
return `안녕하세요, ${this.name}!`;
},
// async 메서드
async fetchData() {
return await fetch('/api/data');
},
// getter/setter
get info() {
return `${this.name} (${this.age}세)`;
},
set info(value) {
[this.name, this.age] = value.split(',');
}
};
// 속성 단축 표기
const name = 'Alice';
const age = 30;
// 기존
const user1 = { name: name, age: age };
// 단축
const user2 = { name, age }; // 변수명과 키가 같으면 단축 가능
// 계산된 속성명
const prefix = 'get';
const dynamicObj = {
[`${prefix}Name`]() { return this.name; },
[`${prefix}Age`]() { return this.age; },
name: 'Bob',
age: 25
};
dynamicObj.getName(); // 'Bob'
for...of와 이터러블
// for...of: 이터러블 순회
const numbers = [1, 2, 3, 4, 5];
for (const num of numbers) {
console.log(num);
}
// 문자열도 이터러블
for (const char of 'hello') {
console.log(char); // h, e, l, l, o
}
// Map과 Set
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const [key, value] of map) {
console.log(key, value);
}
// for...in vs for...of
const arr = [10, 20, 30];
arr.custom = 'extra';
for (const key in arr) {
console.log(key); // '0', '1', '2', 'custom' (모든 열거 가능 속성)
}
for (const value of arr) {
console.log(value); // 10, 20, 30 (값만, 'custom' 없음)
}
// 인덱스가 필요하면 entries() 사용
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
Symbol
// Symbol: 유일한 원시값
const sym1 = Symbol('설명');
const sym2 = Symbol('설명');
console.log(sym1 === sym2); // false - 항상 유일
// 객체 키로 사용 (충돌 방지)
const ID = Symbol('id');
const user = {
[ID]: 12345,
name: 'Alice'
};
user[ID]; // 12345
// JSON.stringify에 포함되지 않음
// for...in으로 열거되지 않음
// 전역 Symbol 레지스트리
const globalSym = Symbol.for('app.id');
const sameSym = Symbol.for('app.id');
console.log(globalSym === sameSym); // true
// Well-known Symbol
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
for (const num of new Range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
Map과 Set
// Map: 키-값 쌍 (객체와 달리 모든 타입이 키 가능)
const map = new Map();
map.set('name', 'Alice');
map.set(42, '숫자 키');
map.set({ id: 1 }, '객체 키');
map.get('name'); // 'Alice'
map.has('name'); // true
map.size; // 3
map.delete('name');
map.clear();
// 생성자로 초기화
const map2 = new Map([
['key1', 'value1'],
['key2', 'value2']
]);
// 순회
for (const [key, value] of map2) {
console.log(key, value);
}
// 객체를 Map으로
const obj = { a: 1, b: 2 };
const mapFromObj = new Map(Object.entries(obj));
// Set: 유일한 값의 컬렉션
const set = new Set([1, 2, 3, 2, 1]);
console.log([...set]); // [1, 2, 3] - 중복 제거
set.add(4);
set.has(3); // true
set.delete(1);
set.size; // 3
// 배열 중복 제거
const unique = [...new Set([1, 2, 3, 2, 1, 3])]; // [1, 2, 3]
// 교집합, 합집합, 차집합
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union = new Set([...a, ...b]); // 합집합
const intersection = new Set([...a].filter(x => b.has(x))); // 교집합
const difference = new Set([...a].filter(x => !b.has(x))); // 차집합
WeakMap과 WeakSet
// WeakMap: 키는 반드시 객체, 약한 참조 (GC 가능)
const cache = new WeakMap();
function processUser(user) {
if (cache.has(user)) {
return cache.get(user);
}
const result = expensiveComputation(user);
cache.set(user, result);
return result;
}
// user 객체가 다른 곳에서 참조가 없어지면 WeakMap에서도 자동 제거
// 메모리 누수 방지!
// WeakSet: 객체만 저장, 약한 참조
const visited = new WeakSet();
function visitPage(pageObj) {
if (visited.has(pageObj)) {
console.log('이미 방문한 페이지');
return;
}
visited.add(pageObj);
renderPage(pageObj);
}
// Map vs WeakMap
// Map: 키가 참조되지 않아도 GC 안 됨 (메모리 누수 가능)
// WeakMap: 키 객체가 GC되면 항목도 자동 삭제
고수 팁
1. 태그드 템플릿으로 DSL 만들기
// HTML 이스케이프
function safe(strings, ...values) {
const escaped = values.map(v =>
String(v).replace(/&/g, '&').replace(/</g, '<')
);
return strings.reduce((result, str, i) => result + (escaped[i-1] ?? '') + str);
}
const userInput = '<script>alert("xss")</script>';
const html = safe`<p>사용자 입력: ${userInput}</p>`;
// <p>사용자 입력: <script>alert("xss")</script></p>
2. Symbol.toPrimitive로 타입 변환 커스터마이징
const temperature = {
celsius: 100,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.celsius;
if (hint === 'string') return `${this.celsius}°C`;
return this.celsius; // default
}
};
console.log(+temperature); // 100 (number hint)
console.log(`${temperature}`); // '100°C' (string hint)
console.log(temperature + 0); // 100 (default hint)
3. Proxy로 Map을 객체처럼 사용
function createReactiveObject(initial = {}) {
const data = new Map(Object.entries(initial));
const listeners = new Set();
return new Proxy({}, {
get(_, key) {
if (key === 'subscribe') return (fn) => listeners.add(fn);
return data.get(key);
},
set(_, key, value) {
data.set(key, value);
listeners.forEach(fn => fn(key, value));
return true;
}
});
}
const state = createReactiveObject({ count: 0 });
state.subscribe((key, val) => console.log(`${key} = ${val}`));
state.count = 1; // 'count = 1' 출력
state.count = 2; // 'count = 2' 출력