ECMAScript 2015

ECMAScript 표준 개정판
ECMAScript 5
ECMAScript 2015
ECMAScript 2016
ECMAScript 2017
ECMAScript 2018
ECMAScript 2019
ES Next

스펙 문서

1 개요[편집]

ECMAScript 2015는 2015년 6월에 승인된 ECMAScript 표준(JavaScript)의 6번째 개정판이다. ES6이라고도 부른다. 1999년에 나온 ES3 이후 2009년 ES5의 작은 변경만 빼면 거의 16년간 변화가 없던 자바스크립트, 더 나아가 웹 생태계에 대격변을 가져온 업데이트이다. 이 개정판부터 버전을 연도로 지칭하게 되었으며, 매년 새 개정판이 나오게 되었다.

2 브라우저 지원[편집]

ES6 호환성 테이블

인터넷 익스플로러는 가장 최신 버전인 IE11도 ES6의 기능들을 거의 이용할 수가 없다. 모질라 파이어폭스구글 크롬, 마이크로소프트 엣지, 사파리 등, 최신 업데이트가 계속 나오는 브라우저들은 2019년 기준으로 ES6의 기능들을 거의 다 지원하고 있다. 삼성 브라우저나 모바일 크롬과 같은 모바일 브라우저 역시 대부분 ES6을 지원하고 있다. 그러나 그놈의 인터넷 익스플로러가 아직 살아 있어 이를 지원하기 위해 Babel과 같은 트랜스파일러가 많이 사용되고 있다. Babel은 ECMAScript 2015와 그 이후 개정판의 기능을 사용한 코드를 ES5 문법으로 변경해주어 IE11 등에서 작동하도록 해준다.

3 추가 및 변경점[편집]

3.1 let, const[편집]

ES6 이전에는 변수를 선언하기 위해 var키워드를 사용했다. 기존 var로 선언한 변수는 함수 스코프를 가지고 있었지만 letconst로 선언한 변수나 상수는 블록 스코프를 가진다. 즉 중괄호 블록을 벗어나면 변수를 참조할 수 없게 된다.

{
  var a = 42
}
console.log(a) // 42

{
  let b = 42
}
console.log(b) // ReferenceError: b is not defined

const로 선언하면 값을 변경할 수 없는 상수가 된다.

const x = 0;
x = 3; // 에러가 발생한다.

var로 선언한 변수와는 달리, let을 이용해 같은 스코프 내에 같은 이름의 변수를 두 번 만들면 에러를 발생시킨다. var로 선언한 변수를 let으로 다시 선언하거나, 그 반대의 경우에도 에러가 발생한다.

var a = 3;
var a = 7; // 문제 없음

let b = 3;
let b = 7; // SyntaxError: redeclaration of let b

let a = 3; // SyntaxError: redeclaration of var a
var b = 3; // SyntaxError: redeclaration of let b

let을 이용해 for 반복문 내에서 클로저를 더 쉽게 사용할 수 있게 되었다. 다음 예제를 보자.

var arr = [];
for (var i = 0; i < 3; i++) {
  arr.push(function() { console.log(i) });
}
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3

i를 출력하는 함수를 세 개 만들어 배열에 넣었는데, 결과는 모두 3을 출력하고 있다. 반복문이 끝나고 나면 i의 값이 3이 되기 때문이다. 반면 다음과 같이 let을 사용하면 좀 더 사용자가 원하는 동작을 볼 수 있다.

var arr = [];
for (let i = 0; i < 3; i++) {
  arr.push(function() { console.log(i) });
}
arr[0](); // 0
arr[1](); // 1
arr[2](); // 2

let을 이용해 변수를 선언시 매 반복마다 새 변수 i가 선언되기 때문에, 다음 반복이 일어나도 기존에 클로저에 캡쳐된 변수의 값이 변하지 않는다.

3.2 화살표 함수 (Arrow function)[편집]

기존의 function() {}를 이용한 익명 함수 정의 대신 화살표 함수라고 부르는 새로운 함수 문법이 도입되었다.

let fn1 = function(a, b) {
  let c = a + b
  return c
}
console.log(fn1(1, 2)) // 3

let fn2 = (a, b) => {
  let c = a + b
  return c
}
console.log(fn2(1, 2)) // 3

위와 아래는 같은 역할을 한다. 다음과 같은 변형 문법도 존재한다.

// 매개변수가 하나인 경우 괄호를 생략할 수 있다.
let fn1 = a => {
  let c = 2 * a
  return c
}
console.log(fn1(1)) // 2

// 간단한 식을 바로 리턴하려면 다음과 같이 블록과 return을 생략할 수 있다.
let fn2 = (a, b) => a + b
console.log(fn2(1, 2)) // 3

// 둘 다 적용하는 것도 가능하다.
let fn3 = obj => obj.name
console.log(fn3({ name: "aa" })) // "aa"

문법이 약간 간단해진 것 외에, this 바인딩 문제를 쉽게 해결할 수 있다는 장점이 있다. function으로 정의한 함수는 호출시 this가 전역 객체(브라우저에선 window)가 된다. 객체의 메서드인 경우 this가 객체를 가리키고 있는데, 여기서 익명 함수를 정의하여 사용하고자 할 때 this가 원래 객체가 아니라 window가 되는 문제를 많은 사람들이 겪었다. 이를 해결하기 위해 원래 thisself 등으로 옮겨 놓거나, .bind를 이용해 this를 바인딩해주는 등의 귀찮음이 있었다. 반면 화살표 함수로 함수를 정의할 경우, 정의할 당시의 this를 그대로 화살표 함수의 this로 사용하여 이런 문제가 없다.

3.3 함수 기본 매개변수[편집]

함수의 매개변수에 기본값을 지정해 줄 수 있게 되었다. 기존에는 매개변수로 넘어온 값이 undefined인지 일일히 검사하여 값을 할당했다.

function f(x, y) {
  if (x === undefined) x = 3;
  if (y === undefined) y = 2;
  return x + y;
}
console.log(f()); // 5

이제 매개변수 이름 뒤에 = 기본값을 붙여 기본값을 지정할 수 있다.

function f(x = 3, y = 2) {
  return x + y;
}
console.log(f()); // 5

3.4 해체 할당 (Destructuring Assignment)[편집]

비구조화 할당, 구조 분해 할당 등등으로도 번역하는데, 딱 정해진게 없다. Python의 unpack과 같은 기능으로, 배열이나 객체의 구조를 분해해 그 안의 값들을 변수에 각각 할당하는 기능이다.

const arr = [1, 2, 3];
let [x, y, z] = arr; // x, y, z에 각각 1, 2, 3이 대입된다.
console.log(x, y, z); // 1 2 3

이를 이용해 두 변수의 값을 임시 변수 없이 바꿀 수 있다.

let x = 3;
let y = 10;
[x, y] = [y, x];
console.log(x, y); // 10 3

받고 싶지 않은 값은 생략할 수 있다. 또한, ...을 이용하면 남은 값을 새 배열로 받을 수도 있다.

const arr = [1, 2, 3, 4, 5];
let [, y, z, ...rest] = arr; // y, z에 각각 2, 3이 대입된다. rest는 [4, 5]가 된다.
console.log(y, z); // 2 3
console.log(rest); // [4, 5]

객체에도 적용할 수 있다. 할당 구문에서, :가 없으면 같은 이름의 프로퍼티를 가져와 대입하며, :가 있으면, 그 왼쪽과 같은 이름의 프로퍼티를 가져와서 오른쪽 이름의 변수에 대입한다.

const obj = { x: 1, y: 2, z: 3 };
let { x, y, z: a } = obj; // 프로퍼티 x, y, z를 가져와서 변수 x, y, a에 각각 대입한다.
console.log(x, y, a); // 1 2 3

let이나 var등의 변수 선언 키워드가 앞에 오지 않으면 문 전체를 소괄호(...)로 감싸줘야 한다. 그렇게 하지 않으면 해체 할당 문법이 아니라 블록 문법으로 해석해버리기 때문이다.

const obj = { x: 1, y: 2, z: 3 };
let x, y, a;
({ x, y, z: a } = obj); // 프로퍼티 x, y, z를 가져와서 변수 x, y, a에 각각 대입한다.
console.log(x, y, a); // 1 2 3

객체에서 ...를 사용해 나머지를 새 객체에 할당하는 기능은 ECMAScript 2018에 표준화되었다.

3.5 class[편집]

자바스크립트도 JavaC++와 같은 다른 많은 언어처럼 클래스 문법을 지원하게 되었다. 기존에는 클래스와 그 멤버를 정의하기 위해 다음과 같이 했다.

function Person(name) {
  this.name = name;
}
Person.prototype.say = function() {
  console.log("My name is " + this.name + ".");
}

이제 다음과 같이 작성할 수 있다.

class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log("My name is " + this.name + ".");
  }
}

const p = new Person("Kim");
p.sayName() // "My name is Kim."

extends를 이용해 상속도 가능하다. 상속받은 클래스의 생성자에서는 super()를 이용해 부모 클래스의 생성자를 호출해야 한다.

class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log("My name is " + this.name + ".");
  }
}

class Student extends Person {
  constructor(name, score) {
    super(name);
    this.score = score;
  }
  sayScore() {
    console.log("My score is " + this.score + ".");
  }
}

const s = new Student("Kim", 80);
s.sayName() // "My name is Kim."
s.sayScore() // "My score is 80."

3.6 Promise[편집]

비동기 처리를 위한 객체인 Promise가 추가되었다. 기존 자바스크립트에서는 비동기 처리를 위해 흔히 콜백(callback)함수를 이용했다. 그러나 콜백을 이용한 비동기 처리가 반복되면 계속 함수가 안쪽으로 중첩되어 가독성과 직관성이 떨어지는 문제가 생기게 된다. 이를 콜백 지옥이라고도 부른다. 또한 콜백 패턴에서의 에러 처리는 전통적인 방식인 try...catch를 적용할 수 없고 콜백의 인자에 넘겨줘야 하는데, 에러를 미처 처리하지 못하고 빼먹는 등 실수에 취약했다. 이러한 문제를 해결하기 위해 도입된 패턴이 Promise 패턴이다. ES6 이전에도 라이브러리를 통해 지원되어 사용하는 경우가 있었는데, ES6에서 이를 표준화한 것이다.

전통적인 비동기 처리 방식은 다음과 같았다. asyncTask1asyncTask2를 수행해서 그 반환값을 더해 출력하는 작업이다.

asyncTask1(function (value1) {
    asyncTask2(function (value2) {
        console.log(value1 + value2);
    });
});

비동기 작업을 하는 함수가 Promise를 지원한다면, 다음과 같이 코드를 쓸 수 있다.

asyncTask1()
.then(() => asyncTask2())
.then(() => {
  console.log(value1 + value2);
});

Promise를 리턴하는 함수를 실행하고 .then()에 다음 실행할 함수를 넣는다. .then()을 계속 매달아 그 다음 실행할 함수를 지정할 수 있다. 에러 처리는 .catch()로 한다.

somethingAsync(value1)
  .then((result) => {
    // 성공시 수행할 작업
  })
  .catch((error) => {
    // 실패시 수행할 작업
  });

기존 콜백 방식의 비동기 작업을 Promise로 바꾸려면, new Promise()를 이용해서 Promise 객체를 반환하면 된다. new Promise()의 인자에는 (resolve, reject)를 매개변수로 하는 함수를 넣어야 하는데, 그 안에서 기존 방식으로 비동기 함수를 실행하고, 성공시 resolve를, 실패시 reject를 실행하면 된다.

function newAsyncFunction() {
  return new Promise((resolve, reject) => {
    oldAsyncFunction((error, ret) => {
      if (error) reject(error);
      else resolve(ret);
    });
  });
}

ECMAScript 2017에서 표준으로 확정된 async ... await는 이 Promise를 이용한다. 자세한 사항은 ECMAScript 2017을 참조.

3.7 컬렉션[편집]

다른 객체들을 담기 위한 컬렉션 객체들이 추가되었다. Map, Set, WeakMap, WeakSet이 새로 추가된 객체들이다.

Map은 키-값 쌍(key-value pair)을 저장할 수 있는 컬렉션이다. 이미 자바스크립트의 객체({})는 키-값의 쌍을 저장하기는 하지만 객체의 기본 내장 메서드들과 이름이 충돌하는 문제가 있을 수 있고, 키가 문자열과 ES6에서 추가된 Symbol로만 한정되는 문제가 있다. Map은 데이터를 추가하거나 가져오는 메서드를 분리하여 데이터와 내장 메서드가 충돌하는 문제를 해결하였고, 문자열 뿐 아니라 숫자나 심지어 다른 객체를 키로 사용할 수 있다. Map은 다음과 같이 사용한다.

let map = new Map();
map.set(300, "aa");
let keyObject = { a: "b" };
map.set(keyObject, "bb");

console.log(map.get(300)); // "aa"
console.log(map.get(keyObject)); // "bb"
map.set(keyObject, "cc");
console.log(map.get(keyObject)); // "cc", 같은 키를 두 번 사용하면 값을 덮어쓴다.

Set집합 자료형을 구현한 것으로, 값들을 저장하되 중복 없이 딱 한 번씩만 저장한다. 값이 존재하는지 여부를 빠르게 검사할 수 있게 된다.

let set = new Set();
set.add("aaaa");
console.log(set.has("aaaa")); // true
console.log(set.has("bbbb")); // false
set.add("aaaa"); // 이미 "aaaa"가 존재하므로 무시됨

MapSet 모두 데이터를 넣은 순서를 그대로 유지하기 때문에, 다음과 같이 순회할 때 일정한 순서를 보장한다. Python의 dictionary와 set에는 이러한 보장이 없다.

let set = new Set();
set.add("aaaa");
set.add("bbbb");
set.add("cccc");
for (let i of set) {
  console.log(i); // "aaaa", "bbbb", "cccc" 순서대로 출력
}

V8의 구현에 따르면 MapSet 모두 해시를 이용해 시간복잡도 [math]\displaystyle{ O(1) }[/math]로 빠르게 데이터를 넣고 존재여부를 판단하거나 값을 가져올 수 있다.

Map, Set의 경우 키와 값에 대해 강한 레퍼런스를 가지고 있기 때문에, 키(Set의 경우에는 키=값으로 생각하면 된다)를 정의한 블록에서 벗어나 더는 그 키를 이용할 수 없게 되었더라도 Map이나 Set이 살아있는 한 키와 값이 계속 메모리를 차지하고 있게 된다. 즉, 메모리 누수의 위험이 항상 있다.

이러한 문제를 해결하기 위해 도입된 것이 WeakMapWeakSet이다. 이들은 각각 Map, Set과 같은 기능을 수행한다. 단 여기 사용된 키와 값은 가비지 컬렉션을 막지 않고, 가비지 컬렉션이 일어나면 사라지게 된다. 원래 객체의 수명 동안에만 필요한 추가 데이터를 저장한다든지 할 때 유용하게 사용할 수 있다. 단 WeakMapWeakSet은 넣은 객체들을 순회하거나, 가진 키-값 쌍을 모두 배열로 바꾸는 기능 등은 지원하지 않는다. 이 기능이 없기 때문에 키에 대한 레퍼런스를 잃어버리면 컬렉션에서도 접근할 수 없게 되어 안전하게 가비지 컬렉션을 수행할 수 있다.

let map = new Map();
{
  let keyObject = { a: "b" };
  let valueObject = { b: "c" };
  map.set(keyObject, valueObject);
}
// 이 경우, 블록을 벗어나도 keyObject와 valueObject의 메모리는 반환되지 않는다.

let weakMap = new WeakMap();
{
  let keyObject = { a: "b" };
  let valueObject = { b: "c" };
  weakMap.set(keyObject, valueObject);
}
// 이 경우, 블록을 벗어나면 가비지 컬렉션이 수행될 때 안전하게 keyObject와 valueObject가 제거된다.