안녕하세요. 사원사업부의 마루야마@h13i32maru입니다. 최근의 Web 프론트엔드의 변화는 매우 격렬해서, 조금 눈을 땐 사이에 점점 새로운 것이 나오고 있더라구요. 그런 격렬한 변화중 하나가 ES6이라는 차세대 JavaScript의 사양입니다. 이 ES6는 현재 재정중으로 집필시점에서는 Draft Rev31이 공개되어있습니다.
JavaScript는 ECMAScript(ECMA262)라는 사양을 기반으로 구현되어있습니다. 현재 모던한 Web 브라우저는 ECMAScript 5.1th Edition을 기반으로 한 JavaScript실행 엔진을 탑재하고 있습니다. 그리고 다음 버전인 ECMAScript 6th Edition이 현재 재정중으로, 약칭으로 ES6이라는 명칭이 사용되고 있습니다.
이 번엔, 다른 언어에 있고 JavaScript에도 있으면 하는 기능과, JavaScript에서 잘 나오는 패턴을 통합적으로 적을 수 있는 기능을 중심으로 소개하겠습니다.
JavaScript는 “프로토타입 기반의 OOP”라 불리고 있는 것 처럼, Java나 Ruby등의 “클래스 기반의 OOP”와는 조금 다릅니다. 하지만 프로토타입 베이스의 기능을 효과적으로 사용한다는 것은 여태까지 별로 없었다고 생각합니다. 오히려 가짜 클래스 기능을 구현하던가, 클래스를 실현하기 위해 라이브러리를 사용해 프로그래밍을 적는 것이 많을 것입니다. 이것은 npm에서 class
로 검색하면 많은 패키지가 나오는 것만 생각해도 알수 있다고 생각합니다. 여기서 ES6에서는 클래스 기능을 도입해서, 클래스를 간단히 취급할 수 있게 되었습니다.
// ES5
'use strict';
function User(name){
this._name = name;
}
User.prototype = Object.create(null, {
constructor: {
value: User
},
say: {
value: function() {
return 'My name is ' + this._name;
}
}
});
function Admin(name) {
User.apply(this, arguments);
}
Admin.prototype = Object.create(User.prototype, {
constructor: {
value: Admin
},
say: {
value: function() {
var superClassPrototype = Object.getPrototypeOf(this.constructor.prototype);
return '[Administrator] ' + superClassPrototype.say.call(this);
}
}
});
var user = new User('Alice');
console.log(user.say()); // My name is Alice
var admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob
// ES6
'use strict';
class User {
constructor(name) {
this._name = name;
}
say() {
return 'My name is ' + this._name;
}
}
class Admin extends User {
say() {
return '[Administrator] ' + super.say();
}
}
var user = new User('Alice');
console.log(user.say()); // My name is Alice
var admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob
JavaScript에서 함수의 디폴트 인수나 가변길이 인수를 사용하고 싶다고 생각해도 언어에서 직접적인 방법이 없었기 때문에, ||
를 사용한 마법같은 방법이나 arguments
을 사용한 메타프로그래밍같은 방법을 쓰고 있었습니다. 여기서 ES6는 함수의 가 인수의 선언 방법이 강화되어, 자연스럽게 적을 수 있게 되었습니다. 이것은 나중에 프로그램을 읽을 때에, 시그니쳐만으로 보면 그 함수가 기대하는 인수를 어느정도 알 수 있게 되었다는 효과가 있습니다.
// ES5
'use strict';
function loop(func, count) {
count = count || 3;
for (var i = 0; i < count; i++) {
func();
}
}
function sum() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10
// ES6
'use strict';
function loop(func, count = 3) {
for (var i = 0; i < count; i++) {
func();
}
}
function sum(...numbers) {
return numbers.reduce(function(a, b) { return a + b; });
}
loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10
실제로 이 디폴트 인수나 가변 길이 인수는 함수의 반인수부분만으로 사용하지는 않고, 변수의 대입처리전반이 강화된 것의 일부분이 됩니다. ES6에서 변수의 대입처리에 관해서는 Destructuring and parameter handling in ECMAScript 6에서 샘플을 포함한 다양한 패턴이 소개되어 있습니다.
JavaScript에서는 이벤트 구동의 처리를 자주 적습니다. 예를들어 DOM이 클릭되면 뭔가 처리하거나, XHR 리퀘스트가 완료되면 뭔가 처리하는 경우입니다. 이런 처리를 JavaScript에서 구현하려면, 콜백 함수나 이벤트리스너라고 불리는 것을 대상 객체(DOM이나 XHR)에 설정합니다. 이 콜백함수를 등록하는 시점에서 this
에 콜백 함수안의 억세스하고 싶은 경우도 자주 있습니다만, 여태까지는 클로져를 사용해 this
를 보존하던가, Function.prototype.bind
를 사용해 this
를 속박하거나 했습니다. ES6에서는 Arrow Function라고 불리는 새로운 함수 정의 식이 도입되어, 이 this
에 관한 번거로움을 해소하고 있습니다.
// ES5
'use strict';
var ClickCounter = {
_count: 0,
start: function(selector) {
var node = document.querySelector(selector);
node.addEventListener('click', function(evt){
this._count++;
}.bind(this));
}
};
ClickCounter.start('body');
// ES6
'use strict';
var ClickCounter = {
_count: 0,
start: function(selector) {
var node = document.querySelector(selector);
node.addEventListener('click', (evt)=>{
this._count++;
});
}
};
ClickCounter.start('body');
여태까지 XHR등의 비동기처리는 시작전에 콜백함수를 정의해서, 비동기처리가 끝나면 그 콜백함수가 호출되는 것이 일반적이었지만, 여러 콜백함수의 설정방법이 있었습니다. 예를들어, 비동기처리의 함수에 콜백 함수를 인수로 넘기거나(setTimeout
이나 setInterval
), 비동기처리를 행하는 객체에 콜백 함수를 등록하거나(XHR
나 WebWorker
), 비동기처리의 반환값에 콜백 함수를 등록하거나(IndexedDB
)등이 있습니다.
이런 여러 방법이 있기 때문에, 사용하는 측에서는 각각의 방법을 나눠서 사용할 필요가 있습니다. 여기서 ES6에서는 Promise라는 비동기처리를 종합적으로 처리하는 방법이 언어레벨에서 재공되게 되었습니다. 사용법은, 비동기처리를 행하는 함수는 Promise를 반환값으로 반환해, 부른 쪽에서는 Promise에 콜백 함수를 등록한다는 것입니다.
// ES5
'use strict';
function sleep(callback, msec) {
setTimeout(callback, msec);
}
sleep(function(){
console.log('wake!')
}, 1000);
// ES6
'use strict';
function sleep(msec) {
return new Promise(function(resolve, reject){
setTimeout(resolve, msec);
});
}
sleep(1000).then(function(){
console.log('wake!');
});
또 비동기 처리에서는 예외처리가 문제가 됩니다. 단순히 try-catch
로 감싸도 비동기에서 예외가 일어나면 보충할 수 없습니다. 여기서 Promise에서는 비동기 처리의 예외처리도 종합적으로 처리하는 방법이 제공되고 있습니다. 이 Promise에 관해서는 Web에서 무료로 읽을 수 있는 JavaScript Promiseの本(번역)이 무척 참고가 됩니다.
마지막으로 Generator에 관해 소개하겠습니다. 여기까지는 JavaScript에서 있지만 사용하기 힘들거나, 통일되지 않았던 것을 개선한 기능이지만, 이 Generator라는 것은 완전 새로운 개념으로 ES6에 도입되었습니다.1 Generator는 함수 처리안의 임의의 장소에서 처리를 중단/재개할 수 있는 구조를 제공하는 것입니다. 이 구조는 일반적으로 코루틴(co-rutine)이라 불립니다. 코루틴을 사용하면 무한 리스너나 이터레이터등의 구현을 할 수 있습니다. 이 Generator와 Promise를 조합해서 비동기 처리를 동기처리처럼 직렬로 적을 수 있게 되었습니다. 기본적인 생각 법은 "비동기 처리가 개시되면 처리를 중단해, 비동기처리가 완료되면 처리를 재개한 뒤 연결 처리를 싱행해 나감"입니다. 아까, Promise 절에서 소개한 샘플 코드를 Generator를 사용해 직렬로 적어 보겠습니다. 밑의 샘플코드에서는 Generator와 Promise를 사용해 비동기 처리를 직렬로 적을수 있게 되는 co라는 라이브러리를 사용하고 있습니다.
// ES6
'use strict';
co(function*(){
console.log('sleep...');
yield sleep(1000);
console.log('wake!');
});
이번에는 co를 사용해 해설했지만, co를 사용하지 않고 비동기처리를 직렬로 적는 구조로 async/await라는것이 ES7에서는 제공되고 있습니다.2 Generator에 관해서는 저의 블로그에서 ES6 Generatorを使ってasync/awaitを実装するメモ로 해설 하고 있습니다. 흥미가 있으시면 봐주세요.
// ES5
var sayYeah = function() {
console.log('Yeah');
};
var a = 1;
var b = 'Wow';
var object = {
sayHello: function() {
console.log('hello');
},
sayYeah: sayYeah
};
object[a + 3] = 'four';
object['say' + b] = function() {
console.log('Wow');
}
// ES6
const object = {
sayHello() {
console.log('hello');
},
sayYeah,
[a + 3]: 'four',
['say' + b]() {
console.log('Wow');
}
}
메소드를 줄여쓸 수 있게 되었다. 객체의 메소드는 원래 항상 이름: function(매개변수) { 내용 }
형식 이었으나, 이름(매개변수) { 내용 }
으로 작성할수 있다.
참조하는 변수의 이름과 속성의 이름이 같은 경우 (위에서 sayYeah) { 이름: 이름 }
대신 { 이름 }
한 번 만 써도 되게 바뀌었다.
기존에는 속성의 이름에 변수의 값이 사용될 때, 자체적으로 계산하지 못했다. 그래서 객체를 선언할 때 바로 추가하지 못하고, 계산한 후에 속성을 따로 추가해주어야 했으나, 키 값으로 [a + 3]
을 해주면 자동으로 4로 계산해준다. 함수도 같이 동작한다.
// ES5
var a = 3;
var b = 'hi';
var object = {
c: 'friends'
};
var string = b + ', my ' + a + ' ' + object.c; // 'hi, my 3 friends'
var string3 = 'hello\nfriends!';
// ES6
const string2 = `${b}, my ${a} ${object.c}`;
const string4 = `hello
friends!`;
백틱이라고 불리는 기호를 사용한다. 따옴표 대신 백틱을 사용하고 그 안에 변수들은 ${ }
로 감싸주면 된다. 그러면 자바스크립트 엔진이 알아서 합쳐준다.
// ES5
var array = [1, 2, 3];
var a = array[0];
var b = array[array.length - 1];
console.log(a); // 1
console.log(b); // 3
// ES6
const [c, ,d] = [1, 2, 3];
console.log(c); // 1
console.log(d); // 3
가운데 , ,
는 요소들이 몇개나 있든지 상관하지 않겠다 하는 것으로 위의 예제는 배열 해체를 표현한것이다.
// ES5
var obj = {
e: 'Eeee',
f: {
g: 'Gee'
}
};
var e = obj.e;
var g = obj.f.g;
// ES6
const obj = {
e: 'Eeee',
f: {
g: 'Gee'
}
};
const { e, f: {g}, k} = obj;
console.log(e, j, k); // 'Eeee', 'Gee', undefined
const destruct = ({value: x}) => {
console.log(x);
};
destruct({value: 3}); // 3
Map은 Object를 변형한 것이고, Set은 Array를 변형한 것이다.
Object와 달리 순서가 존재 하고, 키가 문자열이 아이어도 된다.
// ES6
var map = new Map([['zero', 'Zero']]);
map.set('hero', 'Hero');
map.get('zero');
map.size;
map.has('hero');
map.entries();
map.keys();
map.values();
map.delete('hero');
map.clear();
// ES6
var weakMapObj = {example: 'any'};
var weaMap = new WeakMap();
weakMap.set(weakMapObj, 'zero');
weakMap.get(weakMapObj); // 'zero'
굳이 WeakMap을 쓰는 이유는 WeakMap의 키는 기존 객체를 약한 참조해서 가비지컬렉션을 방해하지 않기 때문이라고 한다. 대신에 entries, keys, values 메소드를 사용할 수 없습니다.
var set = new Set(['zero']);
set.add('hero');
set.has('zero'); // true
set.has('nero'); // false
set.size; // 2
set.entries(); // {['zero', 'zero'], ['hero', 'hero']}
set.keys(); // {'zero', 'hero'}
set.values(); // {'zero', 'hero'}
set.delete('hero');
set.clear();
중복 불가능하며, 중간 값 확인(set[0]
) 분가능 하다.
WeakSet은 객체만을 값으로 받는다.
// ES6
var weakSetObj = {example: 'any'};
var weakSet = new WeakSet();
weakSet.add(weakSetObj);
기존엔 commonJS나 requireJS같은 의존성 관리를 하기 위하여 사용하였다.
// Example.js ES6
const a = 1;
const b = 2;
export { a };
export const c = 3;
export default b;
// Example2.js ES6
import d, { a, c as e} from 'Example';
console.log(a, d, e); // 1, 2, 3
default로 export한 b는 괄호를 사용하지 않아도 import할 수 있다. 그리고 변수명도 마음대로 지을 수 있다.
배열이 아닌 것(문자열, 유사 배열(arguments 등등), 반복 가능 객체)을 배열로 만들어 준다.
// ES6
Array.from('Zero'); // ["Z", "e", "r", "o"]
인자를 요소를 갖는 배열을 만든다.
// ES6
Array.of('Zero', 'Nero', 'Hero'); // ["Zero", "Nero", "Hero"]
배열을 요소 인자로 제공한 값으로 대체 한다. 시작값과 끝값은 위치를 특정할 때 상용한다. 인텍스 값이기 때문에 0부터 시작한다.
// ES6
[1, 3, 5, 7, 9].fill(4); // [4, 4, 4, 4, 4]
[1, 3, 5, 7, 9].fill(4, 1, 3); // [1, 4, 4, 7, 9]
배열 안의 요소를 찾는다. 조건을 만족하는 첫 번째 요소만 반환한다.
// ES6
[1, 2, 3, 4, 5].find(function(number) {
return number % 2 === 0;
}); // 2
[{ name: 'Zero' }, { name: 'Nero' }, { name: 'Hero' }].find(function(person) {
return person.name === 'Zero';
}); // { name: 'Zero' }
find 메소드와 비슷하지만 해당 요소 대신에 그 요소의 위치를 반환한다.
// ES6
[1, 2, 3, 4, 5].findIndex(function(number) {
return number % 2 === 0;
}); // 1
[{ name: 'Zero' }, { name: 'Nero' }, { name: 'Hero' }].findIndex(function(person) {
return person.name === 'Zero';
}); // 0
목교의 위치에 시작점 부터 끝점 까지의 배열을 덮어씌운다.
// ES6
[1, 2, 3, 4, 5].copyWithin(1, 2); // [1, 3, 4, 5, 5]
[1, 2, 3, 4, 5].copyWithin(1, 2, 3); // [1, 3, 3, 4, 5]
fromCharCode의 업그레이드 버전. length가 2인 문자들까지 지원한다.
charCodeAt의 업그레이드 버전. length가 2인 문자들까지 지원한다.
문자열이 주어진 인자로 시작하는지 체크한다.
// ES6
'Zero is Great'.startsWith('Zero'); // true
'Zero is Great'.startsWith('is', 5); // true
문자열이 주어진 인자로 끝나는지 체크한다. 길이가 주어지면, 해당 문자열의 길이가 그것이라고 가정한 상태에서 체크한다.
// ES6
'Zero is Great'.endsWith('eat'); // true
'Zero is Great'.endsWith('is', 7); // true
문자열을 반복하는 메소드.
// ES6
'Zero'.repeat(3); // 'ZeroZeroZero'
문자열에 찾는 문자열이 있는지 확인하고 있으면 true 없으면 false를 반환.
// ES6
'Zero is great'.includes('gr'); // true
객체를 얕은 복사하는 메소드. 소스들을 목표에 모두 복사해서 합친다.
// ES6
Object.assign({ }, { a: 1 }); // { a: 1 }
Object.assign({ a: 1, b: 1 }, { a: 2 }, { a: 3 }); // { a: 3, b: 1 }
두 값이 같은 지 비교 한다. 몇 가지 예외는 있지만 대다수의 경우 일치한다.
// ES6
Object.is(window, window); // true
Object.is(0, -0); // false
Object.is(null, null); // true
숫자의 부호를 알려 준다. 1, -1, 0, -0, NaN 다섯 가지로 반환한다. 각각 양수, 음수, 양의 0, 음의 0, 숫자 아님을 뜻 한다.
// ES6
Math.sign(-3); // -1
Math.sign(-0); // -0
소수점을 버린다. 양수에서는 Math.floor과 같고, 음수에서는 Math.ceil과 같다.
// ES6
Math.trunc(1.5); // 1
Math.trunc(-1.5); // -1
숫자가 정수인지 아닌지 확인.
// ES6
Number.isInteger(1); // true
Number.isInteger(0.1); // false
기존에 window 객체의 메소드였는데, ES2015에서는 Number 객체의 메소드가 되었다. 그러면서 문제점을 수정했습니다. 기존의 메소드는 숫자가 아닌 자료형을 강제로 숫자로 변환한 후 체크했지만, 이 메소드들은 더 이상 숫자로 강제 변환하지 않는다. 이제 NaN
도 안전하게 체크할 수 있다.
기존의 글로벌 함수와 동일. 하지만 ES2015에서 모듈을 장려함에 따라, window 객체에 있던 메소드들이 Number로 이동하였다.
이번에는 JavaScript에서 안타까웠던 곳이 ES6에서 어떻게 변하는가를 중심으로 설명했습니다. 여기서 소개하는 내용은 ES6의 일부로, 이외에도 Modules, Symbol, Data Structures, Proxy, Template String등 여러 기능이 추가되어 있습니다. 현시점에서는 ES6로 적은 코드를 그대로 브라우져나 node에서 실행하는 것은 어려운 상황입니다만, ES6를 ES5로 트랜스컴파일하는 툴로 traceur-compiler나 6to5가 있으므로 가볍게 시험해 보실 수 있습니다. 또 각 브라우져나 툴이 ES6의 어느 기능에 대응하고 있는지는 ECMAScript compatibility table이 참고가 됩니다. ES6시대의 JavaScript를 준비해서 지금부터 조금씩 건드려 보시면 어떨까요?
- Firefox에서는2006년 쯤에 이미 구현되어 있습니다.
- C#의 async/await와 같은 것
좋은 글 감사합니다.