JavaScript ES6+는 2015년에 공식 표준으로 채택된 ECMAScript 2015(ES6)와 그 이후 버전(ES7, ES8 등)에서 도입된 새로운 문법과 기능들을 통칭하는 용어이다. 이는 2009년에 표준화된 ES5 이후 가장 큰 변화로, 현대적인 JavaScript 개발의 기반을 형성한다.
주요 목적은 코드의 가독성, 유지보수성, 표현력을 높이고, 비동기 프로그래밍을 보다 쉽게 처리하며, 대규모 애플리케이션 개발을 지원하는 것이다. 핵심 기능으로는 화살표 함수, 템플릿 리터럴, let/const 키워드를 통한 블록 스코프 변수 선언, 구조 분해 할당, 모듈 시스템, 클래스 문법, 프로미스와 async/await 등이 포함된다.
이러한 기능들은 기존 ES5 문법으로도 구현 가능한 기능들을 보다 간결하고 명확한 문법으로 대체하며, JavaScript를 단순한 스크립팅 언어에서 본격적인 애플리케이션 개발 언어로 진화시키는 데 기여했다. 모든 현대 웹 브라우저와 Node.js 환경에서 광범위하게 지원되며, 트랜스파일러인 바벨을 이용하면 구형 환경에서도 호환성을 유지할 수 있다.
ES6 이전에는 변수를 선언하는 키워드로 var만 존재했다. var는 함수 스코프를 가지며, 호이스팅과 재선언 가능성으로 인해 예기치 않은 동작을 초래할 수 있었다. ES6에서는 이러한 문제를 해결하고 더 명확한 스코프 규칙을 제공하기 위해 let과 const라는 새로운 변수 선언 키워드를 도입했다.
let은 변수를 선언하며, 값의 재할당이 가능하다. const는 상수를 선언하며, 선언과 동시에 초기화해야 하며, 재할당이 불가능하다[1]. 두 키워드 모두 블록 스코프를 따른다. 이는 변수의 유효 범위가 함수, if문, for문, while문 등의 중괄호({})로 둘러싸인 블록 내부로 제한된다는 의미이다.
키워드 | 스코프 | 재선언 | 재할당 | 호이스팅 |
|---|---|---|---|---|
함수 스코프 | 가능 | 가능 | 발생 (undefined로 초기화) | |
블록 스코프 | 불가능 | 가능 | 발생하지만, 초기화 전 접근 시 ReferenceError 발생 (일시적 사각지대) | |
블록 스코프 | 불가능 | 불가능 | 발생하지만, 초기화 전 접근 시 ReferenceError 발생 (일시적 사각지대) |
블록 스코프의 도입으로 코드의 예측 가능성이 높아졌다. 예를 들어, for 루프의 카운터 변수를 let으로 선언하면 각 루프 반복마다 독립적인 스코프를 생성하여 클로저 관련 문제를 해결한다. 기본적으로 변수 선언 시 const를 사용하고, 재할당이 필요한 경우에만 let을 사용하는 것이 권장되는 패턴이다. var 키워드는 기존 코드와의 호환성을 위해 남아있지만, 새로운 코드에서는 사용을 지양한다.
ES6에서 도입된 let과 const는 변수 선언을 위한 새로운 키워드이다. 기존의 var 키워드가 가진 몇 가지 문제점을 해결하기 위해 설계되었다.
let은 블록 스코프를 가지는 변수를 선언한다. 이는 변수가 선언된 블록 스코프({if}, {for}, 함수 등) 내에서만 유효함을 의미한다. 같은 스코프 내에서 동일한 이름으로 재선언할 수 없으며, 호이스팅 현상은 발생하지만 선언 전에 변수를 참조하려고 하면 일시적 사각지대에 빠져 참조 에러가 발생한다. const는 let과 동일한 블록 스코프와 호이스팅 특성을 가지지만, 반드시 선언과 동시에 값을 할당해야 하며, 한 번 할당된 값(원시 타입)이나 참조(객체 타입)를 변경할 수 없다는 점이 다르다.
키워드 | 스코프 | 재선언 가능 | 재할당 가능 | 호이스팅 | 초기화 필요 |
|---|---|---|---|---|---|
| 함수 스코프 | 가능 | 가능 | 선언과 초기화 동시 | 아니요 |
| 블록 스코프 | 불가능 | 가능 | 선언만 호이스팅[2] | 아니요 |
| 블록 스코프 | 불가능 | 불가능[3] | 선언만 호이스팅 | 예 |
일반적으로 변수 선언 시 기본값은 const를 사용하는 것이 권장된다. 값의 재할당이 필요한 경우에만 let으로 변경하고, var 키워드는 하위 호환성을 위해 남겨두었다. 이 접근 방식은 의도하지 않은 재할당을 방지하고 코드의 안정성을 높이는 데 기여한다.
var 키워드로 선언된 변수는 함수 스코프를 가지지만, ES6에서 도입된 let과 const는 블록 스코프를 가집니다. 블록 스코프는 중괄호({})로 둘러싸인 코드 블록 내에서만 변수가 유효함을 의미합니다. 이는 if문, for문, while문 또는 단독 블록 내에서 선언된 변수가 해당 블록 외부에서는 접근할 수 없게 만듭니다.
블록 스코프의 도입은 변수의 유효 범위를 명확히 하여 코드의 예측 가능성을 높이고, 의도치 않은 호이스팅이나 재선언으로 인한 오류를 줄입니다. 예를 들어, for 루프의 카운터 변수를 let으로 선언하면, 루프가 끝난 후에도 변수에 접근할 수 없어 전역 네임스페이스의 오염을 방지합니다.
선언 키워드 | 스코프 유형 | 재선언 가능 | 재할당 가능 |
|---|---|---|---|
| 함수 스코프 | 가능 | 가능 |
| 블록 스코프 | 불가능 | 가능 |
| 블록 스코프 | 불가능 | 불가능 |
블록 스코프는 TDZ라는 개념과도 연결됩니다. let과 const로 선언된 변수는 선언문에 도달하기 전까지 해당 스코프 내에서 접근할 수 없습니다. 이는 변수가 초기화되기 전에 사용되는 것을 방지하는 안전장치 역할을 합니다.
화살표 함수는 ES6에서 도입된 새로운 함수 표현식 문법이다. function 키워드 대신 => 화살표 표기법을 사용하여 함수를 간결하게 정의한다. 기본적인 문법은 (매개변수) => { 함수 본문 } 형태를 가지며, 매개변수가 하나일 때는 괄호를 생략할 수 있고, 본문이 단일 표현식일 때는 중괄호와 return 문을 생략할 수 있다.
화살표 함수의 가장 중요한 특징은 자신의 this 바인딩을 생성하지 않는다는 점이다. 일반 함수는 호출 방식에 따라 this가 동적으로 결정되지만, 화살표 함수는 자신을 둘러싸고 있는 렉시컬 스코프의 this 값을 그대로 상속받는다. 이는 콜백 함수나 이벤트 핸들러 내에서 this 컨텍스트를 유지해야 할 때 유용하다. 또한 arguments 객체와 new 키워드를 통한 생성자 호출도 지원하지 않는다.
특징 | 일반 함수 | 화살표 함수 |
|---|---|---|
| 동적 (호출 방식에 따라 다름) | 정적 (상위 스코프의 |
| 있음 | 없음 |
생성자 함수로 사용 | 가능 | 불가능 (TypeError 발생) |
| 있음 | 없음 |
화살표 함수는 주로 짧은 콜백 함수, 배열 메서드(map, filter, reduce 등)와의 조합, 그리고 클래스 필드 내에서 메서드를 정의할 때 널리 사용된다. 특히 프로미스 체이닝이나 async/await 문맥에서 비동기 코드의 가독성을 높이는 데 기여한다. 그러나 메서드 정의나 생성자 함수, prototype에 함수를 할당해야 하는 경우에는 일반 함수 선언이 더 적합하다.
화살표 함수는 function 키워드 대신 => 화살표를 사용하여 함수를 정의하는 ES6의 새로운 문법이다. 기본적인 형태는 (매개변수) => { 함수 본문 }이며, 매개변수가 하나일 때는 괄호를 생략할 수 있고, 본문이 단일 표현식일 때는 중괄호와 return 문을 생략할 수 있다.
화살표 함수의 가장 중요한 특징은 자신만의 this 바인딩을 생성하지 않는다는 점이다. 일반 함수에서 this는 함수가 호출되는 방식에 따라 동적으로 결정되지만, 화살표 함수는 자신을 둘러싼 렉시컬 스코프의 this 값을 그대로 상속받아 사용한다. 이는 콜백 함수나 이벤트 핸들러 내에서 this 컨텍스트를 유지해야 할 때 특히 유용하다.
함수 유형 |
|
|
|
|---|---|---|---|
일반 함수 | 호출 방식에 따라 동적 | 자체적으로 생성 | 가능 |
화살표 함수 | 렉시컬 스코프에서 정적으로 상속 | 상위 스코프의 것을 참조 | 불가능 |
이러한 특성 때문에 화살표 함수는 메서드 정의나 생성자 함수로는 사용하지 않는 것이 일반적이다. 대신, 배열 메서드의 콜백(map, filter, forEach 등)이나 프로미스 체인 내부, setTimeout과 같은 타이머 콜백에서 this의 혼란을 줄이기 위해 널리 사용된다.
화살표 함수는 주로 콜백 함수나 고차 함수의 인자로 사용될 때 그 간결함이 빛을 발한다. 배열의 map, filter, reduce 같은 메서드와 함께 사용되는 것이 대표적인 예시이다. 예를 들어, numbers.map(x => x * 2)와 같은 코드는 함수 표현식을 매우 간략하게 표현한다.
this의 정적 바인딩 특성 덕분에, 메서드 내부에서 콜백 함수를 정의할 때 특히 유용하다. ES5에서는 var self = this;와 같은 패턴을 사용해 this를 보존했지만, 화살표 함수를 사용하면 이러한 번거로움 없이 렉시컬 스코프를 통해 상위 스코프의 this를 자연스럽게 참조할 수 있다.
사용 사례 | 예시 코드 | 설명 |
|---|---|---|
배열 메서드와의 조합 |
| 간결한 콜백 함수 정의 |
|
| 상위 컨텍스트의 |
고차 함수 반환 |
| 간단한 클로저 생성 |
그러나 객체의 메서드로는 사용하기에 적합하지 않다. 화살표 함수는 자신의 this를 가지지 않기 때문에, 객체의 메서드로 정의되면 예상과 다르게 동작할 수 있다. 또한 생성자 함수로 사용할 수 없고, arguments 객체를 가지지 않는다는 점도 주요 제약 사항이다.
템플릿 리터럴(Template Literal)은 ES6에서 도입된 새로운 문자열 표기법이다. 백틱(` ``) 문자를 사용하여 문자열을 감싸며, 기존의 작은따옴표나 큰따옴표를 사용한 문자열 리터럴보다 향상된 기능을 제공한다. 가장 큰 특징은 문자열 내에 표현식(Expression)을 삽입할 수 있는 문자열 보간 기능과, 여러 줄 문자열을 쉽게 작성할 수 있는 것이다.
주요 기능으로는 문자열 보간(String Interpolation)이 있다. ${} 구문 안에 변수나 연산식 등 자바스크립트 표현식을 넣으면, 그 결과값이 문자열에 자동으로 삽입된다. 이는 기존의 문자열 연결 연산자(+)를 사용한 번거로운 방식을 대체한다. 또한, 백틱으로 감싼 문자열 내에서는 줄바꿈을 그대로 표현할 수 있어, \n 이스케이프 시퀀스를 사용하지 않고도 여러 줄 문자열을 작성할 수 있다.
기능 | 예시 코드 | 결과 |
|---|---|---|
문자열 보간 | ` | 변수 |
여러 줄 문자열 | ` | 'Line 1 Line 2'와 동일 |
표현식 사용 | ` |
|
또 다른 고급 기능으로 태그드 템플릿(Tagged Template)이 있다. 이는 함수를 사용하여 템플릿 리터럴을 처리하는 방식이다. 함수 이름 뒤에 템플릿 리터럴을 작성하면, 해당 함수가 호출되며 리터럴의 문자열 부분과 보간된 값들이 인자로 전달된다[4]. 이를 통해 문자열의 사용자 정의 파싱, 국제화(i18n), 이스케이프 처리, CSS-in-JS 라이브러리 구현 등 다양한 고급 활용이 가능해진다.
템플릿 리터럴은 백틱(` `) 문자로 감싸서 표현하며, 문자열 보간 기능을 통해 문자열 내부에 [[표현식]]을 삽입할 수 있다. 표현식을 ${} 구문으로 감싸서 삽입하면, 해당 표현식의 값이 문자열로 평가되어 최종 문자열에 포함된다. 이는 기존의 문자열 연결 연산자(+`)를 사용하는 방식보다 가독성이 높고 편리하다.
예를 들어, 변수 name과 age의 값을 문자열에 포함시키려면 ${name}과 ${age}와 같이 작성한다. 표현식 내부에서는 사칙연산, 함수 호출, 삼항 연산자 등 유효한 모든 자바스크립트 표현식을 사용할 수 있다.
```javascript
const user = 'Kim';
const score = 95;
const message = User ${user} scored ${score} points.;
// 결과: "User Kim scored 95 points."
const calculation = The result is ${5 * 10}.;
// 결과: "The result is 50."
function isAdult(age) {
return age >= 20;
}
const status = Status: ${isAdult(25) ? 'Adult' : 'Minor'};
// 결과: "Status: Adult"
```
또한, 템플릿 리터럴은 여러 줄 문자열을 자연스럽게 표현할 수 있다. 줄바꿈 문자(\n)를 명시적으로 입력할 필요 없이 코드상의 줄바꿈이 그대로 문자열에 반영된다. 이는 HTML 템플릿이나 긴 메시지를 작성할 때 특히 유용하다.
```javascript
const multiLine = `
This is
a multi-line
string.
`;
```
태그드 템플릿은 템플릿 리터럴의 고급 기능으로, 함수를 사용하여 템플릿 리터럴의 출력을 직접 처리할 수 있게 해준다. 일반적인 템플릿 리터럴 앞에 함수 이름(태그)을 붙여 사용한다. 이 함수는 첫 번째 인자로 문자열 리터럴의 배열을 받고, 이후 인자들로 각 삽입 표현식(인터폴레이션)의 계산된 값을 순서대로 받는다.
태그 함수는 이 인자들을 활용해 완전히 새로운 문자열을 생성하거나, 문자열이 아닌 다른 타입의 값을 반환할 수도 있다. 이를 통해 HTML 이스케이프 처리, 국제화(i18n), 도메인 특화 언어(DSL) 생성, CSS-in-JS 라이브러리 구현 등 다양한 고급 활용이 가능해진다. 다음은 간단한 태그드 템플릿의 예시이다.
```javascript
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ? <mark>${values[i]}</mark> : '');
}, '');
}
const name = "ES6";
const feature = "태그드 템플릿";
const output = highlight${name}의 ${feature} 기능을 소개합니다.;
// 결과: "ES6의 <mark>태그드 템플릿</mark> 기능을 소개합니다."
```
실제 개발에서는 styled-components나 lit-html과 같은 라이브러리가 이 기능을 기반으로 동작한다. 또한, 태그드 템플릿을 이용하면 문자열을 안전하게 처리할 수 있어, 사용자 입력값을 그대로 삽입할 때 발생할 수 있는 크로스사이트스크립팅(XSS) 공격 위험을 줄이는 데 도움을 줄 수 있다.
구조 분해 할당은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 ES6의 표현식이다. 이 문법을 사용하면 배열의 값이나 객체의 프로퍼티를 더욱 간결하고 명확하게 추출할 수 있다.
배열 구조 분해는 배열의 값을 순서대로 변수에 할당한다. 할당받을 변수는 대괄호 [] 안에 나열하며, 배열의 인덱스를 기준으로 매핑된다. 필요하지 않은 요소는 쉼표를 사용해 건너뛸 수 있다.
```javascript
const colors = ['red', 'green', 'blue'];
const [firstColor, , thirdColor] = colors;
console.log(firstColor); // 'red'
console.log(thirdColor); // 'blue'
```
객체 구조 분해는 객체의 프로퍼티를 이름을 기준으로 추출한다. 할당받을 변수는 중괄호 {} 안에 나열하며, 객체의 프로퍼티 키와 동일한 이름을 사용해야 한다. 변수명을 변경하려면 프로퍼티: 새이름 구문을 사용한다.
```javascript
const user = { name: 'Kim', age: 30 };
const { name: userName, age } = user;
console.log(userName); // 'Kim'
console.log(age); // 30
```
구조 분해 할당 시 변수에 기본값을 설정할 수 있어, 분해 대상 값이 undefined일 경우 대체 값을 사용한다. 배열과 객체 모두에서 적용된다.
상황 | 구문 예시 | 결과 |
|---|---|---|
배열 기본값 |
|
|
객체 기본값 |
|
|
또한, 중첩된 구조도 분해가 가능하다. 배열 안의 배열이나 객체 안의 객체에 접근할 때 유용하다. 나머지 매개변수와 유사한 나머지 연산자(...)를 사용하면 분해 후 남은 요소들을 별도의 배열 또는 객체로 모을 수 있다.
```javascript
// 중첩 객체 분해
const metadata = { title: 'ES6', info: { version: 6, year: 2015 } };
const { title, info: { version } } = metadata;
// 나머지 요소 수집
const numbers = [1, 2, 3, 4];
const [first, ...rest] = numbers; // first는 1, rest는 [2,3,4]
```
이 문법은 함수 매개변수에서도 자주 사용되어, 객체를 매개변수로 받는 함수에서 필요한 프로퍼티만 직접 추출하여 코드 가독성을 높인다.
구조 분해 할당은 배열이나 객체의 값을 추출하여 별개의 변수에 할당하는 편리한 문법이다. ES6에서 도입되었으며, 코드의 가독성을 높이고 반복적인 접근을 줄인다.
배열 분해 할당은 대괄호 []를 사용한다. 배열의 각 요소는 순서대로 변수에 할당된다.
```javascript
const colors = ['red', 'green', 'blue'];
const [firstColor, secondColor] = colors;
console.log(firstColor); // 'red'
console.log(secondColor); // 'green'
```
필요하지 않은 요소는 쉼표를 사용해 건너뛸 수 있다. 또한 나머지 매개변수와 유사한 ... 구문을 사용해 나머지 요소를 배열로 받을 수 있다.
```javascript
const numbers = [1, 2, 3, 4, 5];
const [a, , b, ...rest] = numbers; // 두 번째 요소는 건너뜀
console.log(a); // 1
console.log(b); // 3
console.log(rest); // [4, 5]
```
객체 분해 할당은 중괄호 {}를 사용한다. 객체의 프로퍼티 이름과 동일한 이름의 변수를 생성하여 값을 할당한다.
```javascript
const person = { name: 'Kim', age: 30, city: 'Seoul' };
const { name, age } = person;
console.log(name); // 'Kim'
console.log(age); // 30
```
프로퍼티 이름과 다른 변수명을 사용하려면 프로퍼티: 변수명 형식으로 지정한다. 배열 분해와 마찬가지로 나머지 프로퍼티를 객체로 모을 수 있다.
```javascript
const { name: fullName, city, ...otherInfo } = person;
console.log(fullName); // 'Kim'
console.log(otherInfo); // { age: 30 }
```
구분 | 배열 분해 할당 | 객체 분해 할당 |
|---|---|---|
문법 |
|
|
할당 기준 | 요소의 순서(인덱스) | 프로퍼티의 이름(키) |
변수명 변경 | 불가능 |
|
기본값 설정 |
|
|
이 문법은 함수의 매개변수에서도 직접 사용될 수 있어, 객체 형태의 옵션을 받는 함수를 작성할 때 특히 유용하다.
구조 분해 할당을 사용할 때, 변수에 undefined가 할당될 경우를 대비해 기본값을 미리 설정할 수 있다. 이는 배열과 객체 분해 모두에 적용된다.
배열 분해 시 기본값을 설정하는 문법은 다음과 같다. 할당하려는 배열 요소의 값이 undefined일 경우 지정한 기본값이 사용된다.
```javascript
const [a = 10, b = 20] = [1];
console.log(a); // 1
console.log(b); // 20 (두 번째 요소가 없으므로 기본값 20이 할당됨)
```
객체 분해에서도 유사한 방식으로 기본값을 설정한다. 객체의 해당 속성이 존재하지 않거나 그 값이 undefined일 때 기본값이 적용된다.
```javascript
const { x = 1, y = 2 } = { x: 100 };
console.log(x); // 100
console.log(y); // 2 (y 속성이 없으므로 기본값 2가 할당됨)
```
기본값 설정은 함수의 매개변수를 구조 분해할 때 특히 유용하다. 함수 호출 시 인수를 생략하거나 undefined를 전달해도 매개변수가 기본값으로 초기화된다.
```javascript
function drawChart({ size = 'big', coords = { x: 0, y: 0 }, radius = 25 } = {}) {
console.log(size, coords, radius);
}
drawChart({ coords: { x: 18, y: 30 } }); // 'big', { x: 18, y: 30 }, 25
```
이 예제에서 drawChart 함수는 객체를 매개변수로 받으며, 그 객체의 각 속성에 대한 기본값을 정의한다. 또한 함수 전체 매개변수의 기본값으로 빈 객체 {}를 지정하여 인수 없이 호출되는 경우를 방지한다.
ECMAScript 2015 (ES6)에서 도입된 모듈 시스템은 JavaScript 코드를 재사용 가능한 단위로 구성하고 관리하는 표준 방식을 제공한다. 이전에는 CommonJS나 AMD와 같은 비표준 라이브러리에 의존했으나, ES6 모듈은 언어 수준에서 공식적인 모듈 기능을 지원한다. 모듈은 독립적인 파일로 작성되며, 명시적으로 내보내지 않은 코드는 외부에서 접근할 수 없다.
모듈에서 값을 외부로 내보내려면 export 키워드를 사용한다. 기본 내보내기는 export default로 하나의 값을, 이름 있는 내보내기는 export 뒤에 변수, 함수, 클래스 선언을 붙여 여러 개를 내보낼 수 있다. 다른 모듈의 기능을 가져오려면 import 키워드를 사용한다. 기본 내보내기 값은 import 이름 from '모듈경로' 형식으로, 이름 있는 내보내기 값은 import { 이름1, 이름2 } from '모듈경로' 또는 import * as 별칭 from '모듈경로' 형식으로 가져올 수 있다.
내보내기(Export) 유형 | 문법 예시 | 가져오기(Import) 문법 예시 |
|---|---|---|
기본 내보내기 |
|
|
이름 있는 내보내기 |
|
|
여러 이름 내보내기 |
|
|
전체 가져오기 | - |
|
ECMAScript 2020에서 도입된 동적 임포트는 import() 함수를 사용하여 모듈을 조건부나 필요 시점에 비동기적으로 로드할 수 있다. 이 함수는 프로미스를 반환하며, 모듈 객체로 이행된다. 이는 초기 로딩 시간을 줄이거나 사용자 행동에 따라 모듈을 지연 로드할 때 유용하다. 예를 들어, import('./module.js').then(module => { ... }) 또는 async/await 문법과 함께 사용할 수 있다.
ES6에서 도입된 모듈 시스템은 JavaScript 코드를 재사용 가능한 단위로 나누고 관리하는 표준 방식을 제공한다. 이전에는 CommonJS나 AMD와 같은 비공식적인 모듈 로더에 의존했으나, ES6 모듈은 언어 수준에서 import와 export 키워드를 통해 공식적인 모듈 기능을 지원한다.
모듈에서 외부로 공개할 변수, 함수, 클래스는 export 키워드를 사용한다. export 방식에는 개별적으로 내보내는 named export와 모듈 전체를 하나의 객체로 내보내는 default export가 있다. named export는 한 모듈에서 여러 항목을 내보낼 수 있으며, import 시 중괄호 {}를 사용해 정확한 이름으로 가져와야 한다. default export는 모듈당 하나만 가능하며, import 시 임의의 이름으로 가져올 수 있다.
내보내기 방식 | 문법 예시 (내보내기) | 문법 예시 (가져오기) |
|---|---|---|
named export |
|
|
default export |
|
|
다른 모듈의 기능을 사용하려면 import 문을 사용한다. 모듈의 파일 확장자 .js는 생략할 수 없으며, 상대 경로나 절대 경로를 명시해야 한다. named export와 default export를 한 번에 가져오거나, as 키워드를 사용해 별칭을 지정할 수도 있다. 모듈 시스템은 정적으로 분석되므로, import 문은 일반적으로 모듈 최상단에 위치하며 조건부 실행 블록 안에 사용할 수 없다.
import 문은 정적으로 모듈을 불러오지만, ES2020에서 도입된 동적 임포트는 프로그램 실행 중에 조건부로 모듈을 불러올 수 있게 한다. import() 구문은 프로미스를 반환하는 함수 형태로 사용되며, 모듈의 전체 네임스페이스 객체를 resolve한다. 이를 통해 필요할 때만 모듈을 로드하는 지연 로딩이나 코드 스플리팅을 구현할 수 있어 초기 로딩 성능을 개선하는 데 유용하다.
주요 사용 패턴은 다음과 같다. 조건문 내부에서 사용하거나, 사용자 상호작용에 반응하여 모듈을 로드할 수 있다. 반환된 프로미스는 then과 catch를 사용하여 처리하거나, async/await 문법과 함께 사용할 수 있다.
```javascript
// 조건부 로딩 예시
if (userNeedsFeature) {
import('./moduleA.js')
.then(module => {
module.init();
})
.catch(error => {
console.error('모듈 로딩 실패:', error);
});
}
// async/await와 함께 사용
async function loadModule() {
try {
const module = await import('./moduleB.js');
module.run();
} catch (error) {
// 에러 처리
}
}
```
동적 임포트는 특히 싱글 페이지 애플리케이션에서 라우트 기반 코드 분할에 널리 활용된다. 웹팩이나 Vite 같은 번들러는 import() 구문을 만나면 자동으로 해당 모듈을 별도의 청크 파일로 분리하여, 애플리케이션의 초기 번들 크기를 줄여준다. 이는 대규모 애플리케이션의 초기 로드 시간을 단축하는 핵심 기법 중 하나이다.
프로미스는 자바스크립트에서 비동기 작업의 최종 완료 또는 실패를 나타내는 객체이다. 콜백 지옥을 해결하기 위해 ES6에서 도입된 표준 비동기 처리 방식이다. 프로미스는 세 가지 상태를 가진다. 대기 중, 이행됨, 거부됨[5]. 생성된 프로미스는 처음에 대기 중 상태이며, 비동기 작업이 성공적으로 끝나면 이행됨 상태가 되고 결과 값을 가지게 된다. 작업이 실패하면 거부됨 상태가 되고 오류 이유를 가지게 된다.
프로미스는 .then(), .catch(), .finally() 메서드를 통해 체이닝 방식으로 비동기 흐름을 제어할 수 있다. .then()은 프로미스가 이행되었을 때 호출될 콜백을 등록하며, 새로운 프로미스를 반환한다. .catch()는 프로미스가 거부되었을 때 발생하는 오류를 처리한다. .finally()는 이행 또는 거부 여부와 관계없이 무조건 실행되는 콜백을 등록한다. 이를 통해 여러 비동기 작업을 순차적으로 연결하는 프로미스 체이닝이 가능해진다.
메서드 | 설명 | 반환 값 |
|---|---|---|
| 이행 또는 거부 시 호출할 핸들러를 연결함 | 새로운 프로미스 |
| 거부된 경우만을 처리하는 핸들러를 연결함 | 새로운 프로미스 |
| 이행/거부 여부와 상관없이 실행할 핸들러를 연결함 | 새로운 프로미스 |
ES2017에서 도입된 async/await는 프로미스를 기반으로 하여 비동기 코드를 동기 코드처럼 작성할 수 있게 하는 문법적 설탕이다. async 키워드로 선언된 함수는 항상 프로미스를 반환한다. 함수 내부에서 await 키워드는 프로미스가 처리될 때까지 함수의 실행을 일시 중지시킨다. await는 오직 async 함수 내부에서만 사용할 수 있다. 이를 사용하면 .then() 체인 없이도 비동기 작업의 결과를 변수에 직접 할당하는 형태로 코드를 작성할 수 있어 가독성이 크게 향상된다. 오류 처리는 동기 코드처럼 try...catch 문을 사용하여 수행한다.
async와 await는 ES2017에서 도입된 비동기 프로그래밍을 위한 문법적 설탕이다. 이 키워드들은 프로미스 기반 코드를 동기식 코드처럼 읽고 작성할 수 있게 하여, 콜백 지옥과 프로미스 체이닝의 복잡성을 크게 줄인다.
async 키워드는 함수 선언 앞에 붙여 사용하며, 해당 함수는 항상 프로미스를 반환한다. 명시적으로 프로미스를 반환하지 않더라도, 함수의 반환 값은 자동으로 이행된 프로미스로 감싸진다. await 키워드는 async 함수 내부에서만 사용할 수 있으며, 프로미스가 처리(settled)될 때까지 함수의 실행을 일시 중지한다. await 표현식의 값은 프로미스가 이행(resolved)된 값이 된다. 만약 프로미스가 거부(rejected)되면, await 표현식은 거부 사유와 함께 예외를 던진다.
에러 처리는 전통적인 try...catch 문을 사용하여 수행할 수 있다. await로 호출한 프로미스가 거부되면, 해당 예외는 catch 블록에서 잡힌다. 이는 비동기 코드의 에러 흐름을 동기 코드와 동일한 방식으로 제어할 수 있게 해주는 큰 장점이다. 또한 여러 개의 독립적인 비동기 작업을 동시에 시작하려면 Promise.all과 함께 await를 사용하여 병렬 처리 성능을 유지할 수 있다[6])` 형태로 사용].
접근 방식 | 코드 구조 | 에러 처리 |
|---|---|---|
프로미스 체이닝 |
| 체인 내 |
async/await | 동기 코드와 유사한 평문(plain statement) 구조 |
|
async/await의 도입으로 비동기 코드의 가독성과 유지보수성이 획기적으로 향상되었으며, 복잡한 비동기 로직을 보다 직관적으로 표현하는 데 기여했다.
프로미스 체이닝은 여러 개의 비동기 작업을 순차적으로 실행해야 할 때 사용하는 패턴이다. then(), catch(), finally() 메서드를 연결하여 작성하며, 각 메서드는 새로운 프로미스를 반환하기 때문에 체인 형태로 연결이 가능하다. 이 방식을 사용하면 콜백 지옥에 빠지지 않고도 비동기 코드의 흐름을 명확하고 선형적으로 표현할 수 있다.
체이닝의 핵심은 각 then() 핸들러가 반환하는 값이 다음 then() 핸들러의 인자로 전달된다는 점이다. 핸들러가 일반 값을 반환하면, 그 값으로 이행된 프로미스가 생성되어 다음 체인으로 전달된다. 핸들러가 또 다른 프로미스를 반환하면, 그 프로미스의 처리 결과가 해결될 때까지 기다린 후 그 결과를 다음 체인으로 넘긴다. 이를 통해 비동기 작업의 순차 실행을 자연스럽게 구현할 수 있다.
에러 처리는 catch() 메서드를 통해 중앙 집중식으로 관리할 수 있다. 체인 중간 어디에서든 발생한 예외나 거부된 프로미스는 체인 아래로 전파되어 가장 가까운 catch() 핸들러를 만나게 된다. 이를 활용하면 여러 단계의 비동기 작업에서 발생할 수 있는 모든 에러를 한 곳에서 처리할 수 있어 코드의 안정성을 높인다.
메서드 | 설명 | 반환 값 |
|---|---|---|
| 프로미스가 이행되거나 거부되었을 때 호출될 콜백을 등록한다. | 새로운 프로미스 |
| 프로미스가 거부되었을 때 호출될 콜백을 등록한다. | 새로운 프로미스 |
| 프로미스의 성공/실패 여부와 상관없이 실행될 콜백을 등록한다. | 새로운 프로미스 |
finally() 메서드는 작업의 성공 여부와 관계없이 공통적인 정리 작업(예: 로딩 상태 해제)을 수행할 때 유용하다. finally() 핸들러는 인자를 받지 않으며, 이전 프로미스의 결과나 에러를 그대로 다음 체인으로 전달하는 역할을 한다.
ES6에서 도입된 클래스 문법은 기존 프로토타입 기반 상속을 더 명확하고 간결하게 작성할 수 있도록 하는 문법 설탕이다. 내부적으로는 여전히 프로토타입을 사용하지만, Java나 C++와 같은 전통적인 객체지향 언어와 유사한 형태로 코드를 구조화할 수 있다. 클래스 선언은 class 키워드를 사용하며, 생성자, 메서드, 정적 메서드, 게터, 세터를 정의할 수 있다. 또한 extends 키워드를 사용한 상속과 super를 통한 부모 클래스 호출을 지원한다.
키워드/개념 | 설명 |
|---|---|
| 클래스를 정의한다. |
| 인스턴스 생성 시 호출되는 특별한 메서드이다. |
| 다른 클래스를 상속받는다. |
| 부모 클래스의 생성자나 메서드를 호출한다. |
| 클래스 자체에 속하는 정적 메서드를 정의한다. |
동시에, 객체 리터럴 문법도 여러 편의 기능이 추가되었다. 객체 리터럴 내에서 프로퍼티 이름과 값이 동일한 경우 축약 표기가 가능해졌다. 또한 메서드를 정의할 때 function 키워드를 생략할 수 있으며, 계산된 프로퍼티 이름을 대괄호([]) 안에 표현식을 사용해 동적으로 지정할 수 있다. 이는 객체의 키를 런타임에 결정해야 할 때 유용하다.
ES6에서 도입된 클래스 문법은 기존 프로토타입 기반 상속을 더 명확하고 간결하게 표현할 수 있는 문법적 설탕이다. 이는 새로운 객체 지향 상속 모델을 제공하는 것이 아니라, 생성자 함수와 프로토타입 체인을 기반으로 한 상속을 클래스 형태로 정의할 수 있게 한다.
클래스는 class 키워드로 정의하며, 내부에는 constructor 메서드와 일반 메서드를 선언할 수 있다. constructor는 인스턴스 생성 시 호출되는 특별한 메서드이다. 메서드 정의 시 function 키워드를 사용하지 않으며, 메서드 사이에는 쉼표를 찍지 않는다.
```javascript
class Person {
constructor(name) {
this.name = name;
}
greet() {
return Hello, ${this.name};
}
}
```
클래스는 extends 키워드를 사용하여 다른 클래스를 상속받을 수 있다. 상속을 통해 부모 클래스(슈퍼클래스)의 속성과 메서드를 자식 클래스(서브클래스)가 물려받는다. 서브클래스의 constructor 내부에서는 super()를 호출하여 슈퍼클래스의 생성자를 실행해야 한다. 또한, 슈퍼클래스의 메서드를 오버라이드하거나 새로운 메서드를 추가할 수 있다.
```javascript
class Student extends Person {
constructor(name, major) {
super(name);
this.major = major;
}
study() {
return ${this.name} is studying ${this.major};
}
}
```
클래스에는 정적 메서드와 게터/세터도 정의할 수 있다. 정적 메서드는 static 키워드로 정의하며, 인스턴스가 아닌 클래스 자체에 속해 호출된다. 게터와 세터는 get과 set 키워드를 사용하여 객체 속성에 대한 접근을 제어한다.
ES6에서는 객체 리터럴 문법에 몇 가지 편리한 확장 기능이 추가되었다. 이는 객체를 더 간결하고 표현력 있게 생성할 수 있게 해준다.
가장 대표적인 기능은 프로퍼티의 축약 표현이다. 변수 이름과 동일한 이름의 프로퍼티 키를 설정할 때, 키를 생략하고 변수명만 작성할 수 있다. 예를 들어, const x = 1; const obj = { x: x }; 대신 const obj = { x };로 작성할 수 있다. 메서드 정의 시에도 function 키워드를 생략할 수 있다. { method: function() { ... } }는 { method() { ... } }로 간단히 쓸 수 있으며, 제너레이터 함수도 * 기호를 붙여 동일한 방식으로 정의할 수 있다.
계산된 프로퍼티명(Computed Property Name) 기능도 추가되었다. 객체 리터럴 내에서 대괄호([])를 사용하여 동적으로 프로퍼티 키를 생성할 수 있다. 이는 런타임에 평가된 표현식의 결과를 프로퍼티 키로 사용할 수 있게 한다. 예를 들어, const key = 'name'; const obj = { [key]: 'Alice' };는 { name: 'Alice' } 객체를 생성한다. 또한, 프로토타입을 명시적으로 설정하기 위한 __proto__ 할당이 표준화되었고, super 키워드를 사용하여 프로토타입 체인에 접근할 수 있게 되었다.
기능 | ES5 문법 | ES6+ 확장 문법 |
|---|---|---|
프로퍼티 축약 |
|
|
메서드 정의 |
|
|
계산된 프로퍼티명 | 별도 할당 후 사용 |
|
프로토타입 설정 |
|
|
이러한 확장은 코드의 가독성을 높이고, 객체를 생성하는 패턴을 더 직관적으로 만들어 준다. 특히 구조 분해 할당이나 모듈 시스템의 export와 함께 사용될 때 그 효과가 두드러진다.
전개 연산자(Spread Operator, ...)는 이터러블 객체나 문자열을 개별 요소로 확장하거나, 배열 리터럴이나 함수 호출에서 여러 인수를 펼칠 때 사용한다. 배열을 합치거나 복사할 때 유용하며, 함수의 나머지 매개변수(Rest Parameters)로도 활용되어 인수의 개수를 유연하게 처리할 수 있게 한다.
연산자 | 사용 예시 | 설명 |
|---|---|---|
전개 연산자 |
| 배열의 요소를 펼쳐서 새 배열을 생성한다. |
나머지 매개변수 |
| 함수의 인수들을 배열로 묶는다. |
심볼(Symbol)은 ES6에서 도입된 새로운 원시 데이터 타입으로, 유일하고 변경 불가능한 값을 생성한다. 주로 객체의 프로퍼티 키로 사용되어 이름 충돌을 방지한다. 내장 심볼(Well-known Symbol)인 Symbol.iterator는 객체가 이터레이션 프로토콜을 준수하도록 정의하며, 이를 통해 해당 객체는 for...of 루프나 전개 연산자와 함께 사용될 수 있다.
이터러블 프로토콜과 이터레이터 프로토콜은 컬렉션 데이터를 순회하기 위한 표준 메커니즘을 정의한다. 객체가 Symbol.iterator 메서드를 구현하면 이터러블이 되며, 이 메서드는 이터레이터 객체를 반환한다. 이터레이터는 next() 메서드를 통해 순차적으로 값을 제공한다. 이 프로토콜은 Array, Map, Set과 같은 내장 객체에 구현되어 있으며, 사용자 정의 객체에도 적용할 수 있다.
전개 연산자(Spread Operator)는 이터러블 객체나 객체 리터럴 앞에 ...을 붙여서 그 요소들을 개별적으로 펼쳐서 사용할 수 있게 하는 연산자이다. 반면, 나머지 매개변수(Rest Parameter)는 함수의 매개변수 앞에 ...을 붙여서 전달된 인수들을 배열로 묶어 받는 문법이다. 두 문법은 동일한 ... 기호를 사용하지만, 적용되는 위치와 목적이 명확히 다르다.
전개 연산자는 주로 배열이나 객체를 복사하거나 병합할 때, 또는 함수 호출 시 배열 요소를 개별 인수로 전달할 때 유용하게 사용된다. 예를 들어, Math.max(...[1, 5, 3])은 배열 [1, 5, 3]을 펼쳐 Math.max(1, 5, 3)과 동일하게 동작한다. 객체의 경우, {...obj1, ...obj2}와 같이 사용하여 얕은 복사 또는 병합을 수행할 수 있다.
나머지 매개변수는 함수가 받는 인수의 개수가 정해져 있지 않은 가변 인자 함수를 정의할 때 사용된다. 이는 기존의 arguments 객체를 대체하는 더 명시적이고 배열 메서드를 직접 사용할 수 있는 방법을 제공한다. 예를 들어, function sum(...numbers) { ... }와 같이 정의하면, 호출 시 전달된 모든 인수는 numbers라는 이름의 배열로 함수 내부에서 사용 가능하다. 나머지 매개변수는 반드시 함수 매개변수의 마지막에 위치해야 한다.
두 기능의 주요 차이점과 사용 예는 다음 표로 정리할 수 있다.
구분 | 전개 연산자 (Spread Operator) | 나머지 매개변수 (Rest Parameter) |
|---|---|---|
사용 위치 | 함수 호출 시 인수 자리, 배열/객체 리터럴 내부 | 함수 선언 시 매개변수 자리 |
역할 | 값을 '펼쳐서' 개별 요소로 분리 | 인수들을 '모아서' 하나의 배열로 결합 |
주요 사용 예 | 배열/객체 복사·병합, 함수 인수로 배열 전달 | 가변 인자 함수 정의, 구조 분해 할당 시 나머지 요소 수집 |
예시 코드 |
|
|
이러한 문법들은 코드의 가독성을 높이고, 배열 및 객체 조작을 더욱 간결하고 표현력 있게 만들어준다. 특히 나머지 매개변수는 이터러블 프로토콜을 따르는 모든 객체를 배열로 변환할 수 있어 유연성을 제공한다.
심볼(Symbol)은 ES6에서 도입된 새로운 원시 데이터 타입이다. 유일하고 변경 불가능한 값을 생성하며, 주로 객체의 프로퍼티 키로 사용되어 이름 충돌을 방지한다. Symbol() 함수를 호출하여 생성하며, 선택적으로 설명 문자열을 전달할 수 있다. 설명은 디버깅 용도로만 사용되고, 심볼의 식별이나 값에는 영향을 미치지 않는다. 심볼은 for...in 루프나 Object.keys() 등 일반적인 방법으로 열거되지 않아, 객체에 은닉된 프로퍼티를 정의하는 데 유용하다. 전역 심볼 레지스트리를 통해 공유 심볼을 생성하고 접근할 수 있는 Symbol.for()와 Symbol.keyFor() 메서드도 제공된다.
이터러블 프로토콜(Iterable Protocol)과 이터레이터 프로토콜(Iterator Protocol)은 객체의 순회 동작을 표준화한다. 이터러블 프로토콜을 준수하는 객체(예: 배열, 문자열, Map, Set)는 [Symbol.iterator]() 메서드를 구현해야 한다. 이 메서드는 이터레이터 객체를 반환한다. 이터레이터 프로토콜을 준수하는 이터레이터 객체는 next() 메서드를 가지며, 이 메서드는 { value: 값, done: boolean } 형태의 객체를 반환한다. done 프로퍼티가 true가 될 때까지 순회가 계속된다.
프로토콜 | 요구사항 | 반환 값/역할 |
|---|---|---|
이터러블 프로토콜 |
| 이터레이터 객체를 반환 |
이터레이터 프로토콜 |
|
|
이 프로토콜들은 for...of 루프, 전개 연산자(...), Array.from() 등이 동작하는 기반이 된다. 개발자는 사용자 정의 객체에 [Symbol.iterator]() 메서드를 구현하여 자신만의 이터러블 로직을 정의할 수 있다.
ES6 이후의 발전은 JavaScript 생태계에 지속적인 활력을 불어넣었다. 새로운 문법과 기능의 도입은 개발자 경험을 크게 향상시켰으며, 특히 프론트엔드 개발 분야에서 React, Vue, Angular와 같은 현대적 라이브러리와 프레임워크의 발전을 가능하게 하는 토대를 제공했다.
이러한 변화는 단순히 편의성을 넘어 언어의 패러다임 자체에 영향을 미쳤다. 예를 들어, 화살표 함수와 클래스 문법은 함수형 프로그래밍과 객체지향 프로그래밍 스타일을 더 명시적으로 지원하게 했다. 또한, 모듈 시스템의 표준화는 대규모 애플리케이션의 구조화와 의존성 관리 방식을 근본적으로 바꾸었다.
연도 | 명칭 | 주요 추가 사항 예시 |
|---|---|---|
2015 | ES6 (ES2015) | |
2016 | ES7 (ES2016) | 지수 연산자( |
2017 | ES8 (ES2017) |
|
2018 | ES9 (ES2018) | |
이후 | 연간 갱신 |
|
표준화 과정도 중요한 변화를 겪었다. ECMAScript는 이제 매년 새로운 사양을 발표하는 연간 갱신 체제로 전환되었다. 이는 더 작은 규모의 기능들이 빠르게 표준으로 채택되고 브라우저에 구현될 수 있도록 했다. 그러나 이러한 빠른 변화 속도는 때때로 트랜스파일러인 바벨의 중요성을 더욱 부각시키기도 한다. 최신 문법을 모든 환경에서 사용하기 위해서는 여전히 구형 브라우저 호환성을 위한 변환 과정이 필요하기 때문이다.