ECMAScript(ES5나 ES2015의 ES
는 ECMAScript의 줄임말이다.)는 Ecma 인터내셔널에서 정의한 ECMA-262 기술 규격에 정의된 표준 스크립트 프로그래밍 언어이다. ECMAScript는 1997년에 1판이 배포되고 그 뒤로 매년 2판, 3판이 배포되었다. 그 뒤 10년 뒤에 5판(ECMAScript 5 이하 ES5), 다시 6년 뒤인 2015년에 6판(ECMAScript 2015)이 배포되었다. 6판의 정식 명칭은 ECMAScript 6가 아닌 ECMAScript 2015(이하 ES6)이다. 이전엔 배포 주기가 길었지만, 빠르게 변화하는 개발 환경을 반영하여 숫자 대신 연도를 붙여 배포된다.
ES6에서는 ES5 이하 명세에서 문제가 되었던 부분들이 해결되었고, 기존 코드를 더 간결하게 작성할 수 있는 새로운 문법이 추가되면서 더 가독성 및 유지 보수성이 향상되었다. 그 덕에 웹에서 사용하는 자바스크립트 유명 라이브러리들(lodash, React, Vue 등)의 개발 환경도 ES6로 바뀌었다. 따라서 최신 자바스크립트 라이브러리들도 ES6를 사용할 때 훨씬 편리하게 사용할 수 있다.
전 세계적으로 에버그린(Evergreen) 브라우저의 점유율이 높아지고 있으며, ES6 코드가 ES5 환경(인터넷 익스플로러 저 버전과 같은 구형 브라우저 환경)에서 실행되게 도와주는 도구들도 빠르게 발전하고 있기 때문에 하위 브라우저를 지원하는 것도 아주 어렵지 않다. 이제는 ES6의 실무 도입을 망설이지 않아도 된다. ES6 환경을 사용해서 새로운 기능과 개념도 활용하고 더욱 읽기 편한 코드를 작성해보자.
이 가이드는 ES5를 ES6 환경으로 개발 환경을 바꾸면 얻을 수 있는 이점과 ES6부터 최근까지 발표된 자바스크립트 추가 기능들을 알리고자 작성되었다. ES2015 이후의 스펙도 가이드에 포함하지만, 편의상 모두 ES6로 부르겠다.
이후에 설명할 ES6 스펙들은 IE에서는 동작하지 않는 코드들이다. 만약 IE도 지원해야 한다면 어떻게 해야 할까? ES6를 사용하지 못하는 것일까? 아니다. 트랜스파일러(Babel)를 이용해서 브라우저 대부분에서 동작하는 자바스크립트 코드로 쉽게 변경할 수 있다. 참고로 2018년 12월 17일 기준 ES6 명세의 기본 기능인 class
를 지원하는 브라우저는 엣지, 파이어폭스, 크롬 등이 있다.
ES6 코드가 트랜스파일러를 통해 어떻게 크로스 브라우징 가능한 코드로 변환되는지 보여주기 위해 ES6 맛보기 코드를 작성해보았다.
const callName = (person) => {
console.log(`Hey, ${person.name}`);
};
이 코드를 IE에서 직접 실행하면 에러가 발생하고 동작하지 않는다. 하지만 위 코드가 트랜스파일러를 거치면 아래의 ES5 코드로 바뀐다.
"use strict";
var callName = function callName(person) {
console.log("Hey, " + person.name);
};
Babel - Try it out에서 Babel이 우리가 작성하는 ES6 코드를 어떻게 변환해 주는지 간단히 확인할 수 있다. 트랜스파일러를 개발 환경에 어떻게 적용하는지 자세한 과정은 이 글에서는 다루지 않지만, [FE 가이드] 번들러의 하위 브라우저 대응 에서 적용방법을 확인할 수 있다.
크로스 브라우징은 트랜스파일러가 알아서 처리해준다. 개발자는 그저 ES6로 개발하면 된다.
ES5에서는 var
키워드를 이용해서 변수를 선언했었다. var
로 선언한 변수의 값은 언제나 변경할 수 있기 때문에 변경 불가능한 상수 변수를 선언할 방법이 없었다. 그래서 일반 변수와 구분하기 위해 상숫값에 대한 명명 규칙을 영문 대문자와 언더 스코어로만 제한하는 방식을 많이 사용했다. 또한, 타 언어들과는 달리 자바스크립트에서 var
로 선언한 변수는 함수 단위의 스코프를 갖기 때문에 if
문이나 for
문 블록 내에서 var
를 선언한 변수들도 블록 외부에서 접근할 수 있다. 게다가 var
를 이용하면 선언 전에 변수의 사용이 가능한 호이스팅(hoisting)도 발생한다. 이러한 var
키워드의 특징 때문에 많은 개발자가 자바스크립트 개발을 하며 크고 작은 어려움을 겪는다.
앞서 얘기한 문제점들을 해결하는 방법으로 let
, const
키워드 두 가지가 추가됐다. let
, const
를 사용해서 얻을 수 있는 이점들을 차례차례 살펴보자.
let
,const
로 선언한 변수는 블록 스코프를 가진다. 반면에 var
로 선언한 변수는 함수 스코프를 가지므로 의도하지 않은 곳에서도 변수 변경이 가능하게 되어 에러가 발생할 수 있다. 변수 선언에 let
, const
를 사용하면 이러한 실수와 버그를 줄일 수 있다.
function sayHello(name) {
if (!name) {
var errorMessage = '"name" parameter should be non empty String.';
alert(errorMessage);
}
console.log('Hello, ' + name + '!');
console.log(errorMessage); // '"name" parameter should be non empty String.'
}
function sayHello(name) {
if (!name) {
let errorMessage = '"name" parameter should be non empty String.';
alert(errorMessage);
}
console.log('Hello, ' + name + '!');
console.log(errorMessage); // ReferenceError
}
ES5에서는 if
블록의 실행이 끝난 이후에도 errorMessage
변수에 접근이 가능하다. 그 이유는 var
로 선언한 변수는 현재 실행 중인 함수의 스코프에 추가되기 때문이다. 즉, 위 코드의 errorMessage
변수는 sayHello()
함수 스코프에 존재하기 때문에 if
블록을 빠져나온 이후에도 접근이 가능한 것이다. 하지만 let
, const
두 변수 선언 키워드를 사용하여 선언한 변수는 블록 스코프에 추가되므로 if
블록 외부에서 errorMessage
에 접근하는 경우 ReferenceError
가 발생한다.
var
키워드를 이용해서 변수를 선언하면 선언 이전에 변수를 사용할 수 있는 호이스팅 현상이 발생한다. 하지만 호이스팅이 없는 let
, const
를 사용해서 변수를 선언하면 에러가 발생해서 의도치 않은 실수를 줄일 수 있다.
here = '여기야~'; // 변수 초기화가 먼저 되있지만 에러가 발생하지 않는다.
console.log(here); // '여기야~'
var here; // 변수 선언은 이부분에 있다.
here = '여기야~'; // ReferenceError - 변수 here 가 선언되지 않았다.
console.log(here);
let here;
먼저 ES5의 코드를 보자. here
변수 선언보다 먼저 값을 초기화하고 있는데도 에러가 발생하지 않는다. 왜 이런 결과가 나오는 것일까? 자바스크립트는 코드를 실행하기 전에 가장 먼저 var
, function
을 찾아서 스코프의 최상단에 변수와 함수를 미리 등록하기 때문이다. 이러한 호이스팅으로 인해 실수에 의한 오류를 명확하게 감지하기가 어렵고, 의도치 않은 동작이 발생하기도 한다.
반면에 ES6의 let
을 사용해서 같은 코드를 작성해서 실행해보면 에러가 발생한다. here
변수 초기화 이전에 변수가 선언되지 않았기 때문에 참조 에러가 발생한다.
변수 선언 시에 변하지 않는 값은 const
를, 변할 수 있는 값은 let
을 사용한다. 아래 예제로 사용법을 확인해보자.
// 값 수정
let foo = 'foo';
foo = 'foo2'; // OK - 값 수정이 가능하다.
const bar = 'bar';
bar = 'bar2'; // Type error - bar 변수는 상수이므로 값 수정이 불가능하다.
// 선언, 초기화
const baz2; // Type error - const로 선언한 변수는 선언과 동시에 초기화 해야한다.
let baz; // OK - let으로 선언한 변수는 선언과 동시에 초기화할 필요 없다.
baz = 'baz';
위 예제를 보면 let
은 var
와 유사하게 동작하며, 값 변경이 가능한 것을 확인할 수 있다. 하지만 const
변수의 값은 한 번 정의하면 변경할 수 없다. 따라서 변수 선언과 동시에 초기화해야 하고, const
로 선언된 변수의 값을 변경하려고 하면 문법 에러가 발생한다.
하지만 const
를 사용한다 해도 프로퍼티까지 수정할 수 없는 것은 아니다.
// const 변수의 프로퍼티 값 수정
const foo2 = {
bar2: 'bar'
};
foo2.bar2 = 'bar2'; // OK - foo2의 프로퍼티는 수정이 가능하다.
객체나 배열 선언에 const
를 사용했으므로 프로퍼티나 배열 요소까지 변경 불가능하다고 생각할 수 있기 때문에 참조 값을 사용할 때는 주의해야 한다.
화살표 함수는 this
바인딩 이슈를 해결해주고, 함수 표현식의 긴 문법을 좀 더 단축해준다. 화살표 함수는 함수 표현식의 =>
가 화살표를 닮아서 화살표 함수라고 이름이 붙었다. 화살표 함수의 문법을 사용하면 기존 함수 표현식의 function
키워드가 사라지고 더 짧은 문법으로 사용할 수 있다. 함수 호출 시 this
바인딩 이슈를 해결해주는 장점도 있다.
var sum = function(a, b) {
return a + b;
}
const sum = (a, b) => {
return a + b;
};
this
바인드ES5에서는 DOM의 이벤트 핸들러의 함수를 실행할 때 핸들러가 의도한 대로 동작하지 않는 문제가 있다. 아래 예제를 보자.
var buzzer = {
name: 'Buzzer1',
addClickEvent: function() {
this.element = document.getElementById(this.name);
var logNameAndRemoveButton = function() {
console.log(this.name + ' buzzing!');
}
this.element.addEventListener('click', logNameAndRemoveButton.bind(this)); // logNameAndRemoveButton 핸들러 함수 실행시 this 객체가 엘리먼트 객체 이므로 "bind(this)"를 이용해서 this객체를 지정해준다.
}
};
buzzer.addClickEvent();
const buzzer = {
name: 'Buzzer1',
addClickEvent() {
this.element = document.getElementById(this.name);
this.element.addEventListener('click', () => { // buzzerElement에 다시 this 바인드를 하지 않아도 의도한 대로 실행된다.
console.log(this.name + ' buzzing!');
document.body.removeChild(this.element);
});
}
};
buzzer.addClickEvent();
엘리먼트에 등록된 이벤트 핸들러 함수가 실행될 때는 non-strict 모드로 동작해서 핸들러에서 this
객체에 접근하면 이벤트를 처리하는 엘리먼트 객체가 탐색 된다. 그래서 메서드를 이벤트 핸들러로 사용할 때는 내부에서 this
를 사용하는지 살펴본 후 handler.bind(this)
처럼 필요한 컨텍스트의 this
객체를 함수에 바인드 해서 넘겨야 했다. 하지만 화살표 함수를 이용하면 의도한 대로 동작한다. 화살표 함수는 해당 컨텍스트의 this
객체를 바인드 한 함수 표현식처럼 동작한다.
간단한 함수를 한 줄로 표현할 수 있다. 기존 함수에서 리턴하는 값에 항상 return
키워드를 붙여야 했었다면, 화살표 함수는 리턴 값이 표현 식인 경우에 return
키워드 없이 값을 리턴 할 수 있다. 함수를 짧게 바꾸는 방법은, 함수 본문을 감싸는 {
,}
와 return
키워드를 생략하고 리턴할 표현식을 =>
뒤에 작성하면 해당 표현식이 함수 실행 결과로 리턴된다.
// 더 짧은 화살표 함수 표현식 사용
const sum = (a, b) => a + b;
console.log(sum(10, 100)); // 110
이런 짧은 화살표 함수 표현식은 특히 Array.prototype.map()
이나, Array.prototype.filter()
등에 넘겨주는 콜백 함수로 사용할 때 더욱 간결하게 표현할 수 있다.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 함수 표현식 사용
const numbersOverFive = numbers.filter(function(number) {
return number > 5;
});
console.log(numbersOverFive); // [6, 7, 8, 9, 10] ;
// 화살표 함수로 콜백함수 1줄 표현
const numbersOverFive = numbers.filter(number => number > 5)
console.log(numbersOverFive); // [6, 7, 8, 9, 10] ;
인자로 넘겨주는 함수가 함수 표현식을 콜백 함수로 넘겨줄 때 보다 훨씬 짧고 간결해졌다.
하지만 내부적으로 function
으로 선언한 함수와 몇 가지 차이점이 있다. 화살표 함수는 자신의 실행 컨텍스트에 this
, arguments
, super
나 new.target
을 가지고 있지 않은 함수 표현식이다. 자신의 실행 컨텍스트에 별도의 this
가 존재하지 않는 대신, 해당 화살표 함수가 정의된 실행 컨텍스트의 this
를 그대로 따른다는 특징이 있다. 따라서 생성자 함수로는 사용할 수 없다. 메서드나 생성자로 사용되지 않는 간단한 함수를 표현하는 용도로 사용하면 된다.
자바스크립트는 프로토타입 기반 언어다. 클래스 기반의 언어와는 달리 자바스크립트에서는 프로토타입 객체를 재사용하면서 클래스와 유사한 형태의 API를 만들어서 사용해왔다. ES5 환경에서 클래스를 구현하는 방법은 라이브러리마다 달랐다. 하지만 ES6부터 자바스크립트에 클래스 문법이 추가되었고, 라이브러리들도 클래스 문법을 사용하면서 구현과 사용법도 한가지로 통일되었다.
class SomeClass {
static staticClassMethod() {
// 정적 메서드
}
constructor() {
// 생성자 함수
}
someMethod() {
// 클래스 매서드
}
}
const instance = new SomeClass();
instance.someMethod();
SomeClass.staticClassMethod();
클래스 문법은 class
키워드, 클래스 이름, 생성자 함수인 constructor()
, 메서드들, 클래스 상속을 위한 extends
키워드, 그리고 정적 멤버인 static
키워드로 구성되어 있다. 클래스를 선언할 때는 class
키워드 뒤에 클래스의 이름을 적어주고, 다른 클래스의 멤버를 상속하기 위해서는 extends
키워드 뒤에 상속받을 클래스를 작성하면 된다. 클래스도 함수 사용과 같이 선언식과 표현식 두 가지로 사용할 수 있다.
class SomeClass {
//class body
}
클래스 문법이 없는 ES5에서는 생성자 함수와 그 함수의 프로토타입 객체를 확장해서 클래스를 흉내 낼 수 있다. 생성자 함수로 인스턴스 객체에 속성을 설정할 수 있고, 프로토타입 체인을 이용해서 인스턴스 내에 메서드를 생성하지 않고 같은 메서드를 모든 객체에서 공유할 수 있다. 하지만 이런 구현 방법은 실수를 유발할 수도 있으며, 문법이 같기 때문에 일반 함수인지 클래스 생성자 함수인지 혼동되어 코드 가독성이 좋지 않다.
function Person(name) {
this.name = name;
}
Person.prototype.sayMyName = function() {
console.log('My name is "' + this.name + '"');
}
var fred = new Person('fred');
ES5 예제 코드와 똑같은 기능을 하는 클래스를 class
키워드로 쉽게 작성할 수 있다.
class Person {
constructor(name) {
this.name = name
}
sayMyName() {
console.log(`My name is "${this.name}"`);
}
}
const fred = new Person('fred');
ES6 예제 코드를 보면 어느 코드가 클래스이고 생성자 함수인지 쉽게 확인할 수 있고, 메서드도 클래스 내부에 캡슐화되어 가독성이 좋아진 것을 확인할 수 있다.
ES6에서는 객체 리터럴의 key
텍스트와 value
에 올 변수 이름이 같은 경우 한 번만 입력해도 된다. 기존 객체 리터럴에서 반복적으로 입력했던 콜론(:
)과 변수명을 한 번의 입력으로 해결할 수 있다.
var iPhone = '아이폰';
var iPad = '아이패드';
var iMac = '아이맥';
var appleProducts = {
iPhone: iPhone,
iPad: iPad,
iMac: iMac
};
const iPhone = '아이폰';
const iPad = '아이패드';
const iMac = '아이맥';
const appleProducts = {iPhone, iPad, iMac};
ES5 코드에서는 appleProducts
를 정의할 때 프로퍼티의 이름, 값으로 올 표현식을 매번 콜론으로 나누어 작성해야 했지만, ES6 코드를 보면 콜론 없이 미리 정의된 변수만 입력하고 있다. 이렇게 작성하기만 해도 객체 리터럴이 생성하는 새 객체에 변수명과 같은 프로퍼티 키를 만들고 변수의 값을 프로퍼티의 값으로 대입해준다.
그리고 객체의 메서드를 정의할 때 유용한 축약형 메서드 이름도 지원한다. function
키워드와 메서드 이름 뒤의 콜론은 생략할 수 있다.
var dog = {
name: 'Lycos',
bark: function () {
console.log('Woof! Woof!')
}
};
dog.bark(); // 'Woof! Woof!';
const dog = {
name: 'Lycos',
bark() {
console.log('Woof! Woof!')
}
};
dog.bark(); // 'Woof! Woof!';
ES5에서는 객체를 먼저 생성 후 []
접근자를 이용해서 동적으로 프로퍼티 할당을 해주었지만, ES6부터는 표현식의 연산 값을 객체의 키로 사용할 수 있게 되었다. 사용법은 객체 프로퍼티의 키가 올 자리에 [
,]
로 감싸진 표현식을 작성하면 된다.
var ironMan = 'Iron Man';
var captainAmerica = 'Captain America';
var MarvelHeros = {};
MarvelHeros[ironMan] = 'I`m the Iron Man.';
MarvelHeros['Groot'] = 'I am Groot.';
MarvelHeros[captainAmerica] = 'My name is Steve Rogers.';
MarvelHeros['3-D' + 'MAN'] = 'I`m the 3-D Man!';
const ironMan = 'Iron Man';
const captainAmerica = 'Captain America';
const MarvelHeros = {
[ironMan]: 'I`m the Iron Man.',
['Groot']: 'I am Groot.',
[captainAmerica]: 'My name is Steve Rogers.',
['3-D' + 'MAN']: 'I`m the 3-D Man!'
}
템플릿 리터럴 문법은 백 틱(`)으로 감싸진 문자열로 이루어져 있다. 기존의 문자열 조작 시에는 각기 분리된 문자열 리터럴을 +
연산자로 연결해주어야 했다면, 템플릿 리터럴은 내부에 표현식을 바로 작성하여 더욱더 간결한 문법으로 구현할 수 있다. 문자열 사이에 표현식의 리턴 값을 추가하려면 표현식이 올 자리에 ${expression}
를 작성하면 된다.
var brandName = 'TOAST';
var productName = 'UI';
console.log('Hello ' + brandName + ' ' + productName + '!'); // 'Hello TOAST UI!';
const brandName = 'TOAST';
const productName = 'UI';
console.log(`Hello ${brandName} ${productName}!`); // 'Hello TOAST UI!';
ES6 코드를 보자. brandName
과 productName
을 각기 표현식으로 사용할 수도, 둘을 합친 템플릿 문자열을 표현식으로 중첩해서 사용할 수도 있다. 또한 개행 문자를 직접 사용하지 않으면 한 줄 이상의 문자열을 표현할 수 없는 기존 문자열 리터럴과는 달리, 템플릿 리터럴은 두 줄 이상의 문자열을 표현할 수 있으며, 이 경우 개행 문자가 문자열 내에 자동으로 포함된다.
var howToDripCoffee = '1. 물을 끓인다.\n2. 커피 원두를 간다.\n3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.\n4. 끓인물을 천천히 필터 위로 흘려내린다.';
const howToDripCoffee = `1. 물을 끓인다.
2. 커피 원두를 간다.
3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
4. 끓인물을 천천히 필터 위로 흘려내린다.`;
ES5 버전의 예제보다 훨씬 더 자연스럽게 표현이 된 것을 볼 수 있다.
또한 템플릿 태그라는 기능이 지원되어서, 변수가 포함되는 문자열과 그 문자열을 사용하는 함수 실행에 있어 조금 더 간결하게 표현할 수 있다. 자세한 설명은 템플릿 리터럴:태그된 템플릿에서 확인할 수 있다. 이런 기능을 가진 템플릿 리터럴을 통해 템플릿 엔진이나 라이브러리를 별도로 로드하지 않고도 문자열을 더욱더 편하게 조작할 수 있다.
템플릿 태그를 직접 구현해도 되지만, common-tags와 같은 라이브러리를 사용할 수도 있다. 아래는 제공되는 태그 중 하나인 stripIndents
태그이다. 바로 위 ES6 드립 커피 만들기 예제를 보면 첫 줄이 개행되지 않았고, 들여 쓰기가 맞지 않아 코드 가독성이 좋지않다. 하지만 stripIndents
를 사용하면 첫 개행을 무시해주고 각 줄의 들여 쓰기 또한 제거해주어 개행된 문자열들 처리가 간편하다.
import {stripIndents} from 'common-tags'
stripIndents`
1. 물을 끓인다.
2. 커피 원두를 간다.
3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
4. 끓인물을 천천히 필터 위로 흘려내린다.
`
// 1. 물을 끓인다.
// 2. 커피 원두를 간다.
// 3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
// 4. 끓인물을 천천히 필터 위로 흘려내린다.
자바스크립트 개발을 하다 보면 객체를 함수끼리 주고받는 상황이 아주 많다. 전달받은 객체의 프로퍼티를 변수로 선언하려면 각 프로퍼티를 별도의 변수로 할당하기 위해서 각 프로퍼티마다 독립된 할당문을 작성해야 했었다. 하지만 디스트럭처링이라고 불리는 문법이 추가되어 변수 선언이 훨씬 더 편해졌고, 코드가 간결해졌다.
먼저 변수의 프로퍼티를 쉽게 선언하는 예제이다. 객체 디스트럭처링은 변수로 선언하고자 하는 객체의 프로퍼티명을 {
, }
안에 나열하면 각 프로퍼티의 이름으로 변수가 생성되고 프로퍼티의 값이 자동으로 할당된다. 배열 디스트럭처링도 비슷한데 [
, ]
안에 나열하는 변수의 이름에 맞는 인덱스의 배열 요소가 변수의 값으로 할당된다.
function printUserInformation(data) {
var name = data.name;
var age = data.age;
var hobby = data.hobbies;
var firstHobby = hobbies[0];
console.log('이름: ' + name);
console.log('나이: ' + age);
console.log('가장 좋아하는 취미: ' + firstHobby);
}
function printUserInformation(data) {
const {name, age, gender, hobbies} = data;
const [firstHobby] = hobbies;
console.log(`이름: ${name}`);
console.log(`나이: ${age}`);
console.log(`가장 좋아하는 취미: ${firstHobby}`);
}
배열 디스트럭처링으로 []
접근자 사용을 하지 않고도 변수를 선언했다. 그리고 객체 디스트럭처링으로 반복되는 var * = data.*
가 사라지고 한 줄짜리 간결한 코드로 바뀌었다.
파라미터로 받은 객체의 프로퍼티를 변수로 선언하여 사용할 수 있다. 이때는 별도의 변수 선언문 없이 파라미터의 위치에 디스트럭처링 코드를 작성하면 된다. 선언할 변수의 이름은 기존 객체에 선언된 이름 말고 다른 이름으로도 선언 가능하다.
function printError(error) {
var errorCode = error.errorCode;
var msg = error.errorMessage;
console.log('에러코드: ' + errorCode);
console.log('메시지:' + msg);
}
function printError({
errorCode,
errorMessage: msg
}) {
console.log(`에러코드: ${errorCode}`);
console.log(`메시지: ${msg}`);
}
먼저 ES5 예제의 var * = data.*
같은 반복적인 코드 작성 부분이 객체 리터럴처럼 간결하게 바뀌었다. 그리고 printError()
함수의 매개변수를 디스트럭처링해서 별도의 변수 선언 키워드를 사용하지 않았다. 또한 매개변수의 프로퍼티 이름 errorMessage
를 :
로 연결해서 변수명을 쉽게 바꿀 수 있다.
함수 매개변수의 디폴트 값 설정을 자바스크립트 문법에서 지원하게 되었다. 디폴트 값 설정이란 함수의 오동작을 막기 위해 특정 타입 혹은 값을 가져야 할 매개변수가 undefined
로 전달된 경우, undefined
대신 사용할 수 있는 값을 할당하는 것이다. ES5에서는 디폴트 값을 설정하기 위해 if
문으로 매개변수가 undefined
인지 확인한 뒤, 해당 매개변수의 값이 undefined
라면 대체할 값을 해당 매개변수에 할당하는 방식으로 처리해왔다. 하지만 ES6에서는 더욱더 간결한 문법으로 해결할 수 있다.
function sayName(name) {
if (!name) {
name = 'World';
}
console.log('Hello, ' + name + '!');
}
sayName(); // "Hello, World!"
const sayName = (name = 'World') => {
console.log(`Hello, ${name}!`);
}
sayName(); // "Hello, World!"
위에서 다룬 내용은 원시 타입의 매개변수 디폴트 값 설정이다. 하지만 보다 복잡한 매개변수 디폴트 값 설정도 가능하다. ES5에서는 함수 매개변수가 객체일 때 프로퍼티 값, 혹은 매개변수 자체의 optional 처리를 위해 함수의 기능 구현보다 더 긴 코드를 작성해야 했었다. 객체를 전달하는 함수가 많으면 매번 각 프로퍼티를 optional 처리해주는 것이 상당히 귀찮은 작업의 연속이었지만, 디스트럭처링과 유사한 형태의 문법으로 함수 매개변수의 디폴트 값을 간결하게 설정할 수 있다.
function drawES5Chart(options) {
options = options || {};
var size = options.size || 'big';
var cords = options.cords || {x: 0, y: 0};
var radius = options.radius || 25;
console.log(size, cords, radius);
// now finally do some chart drawing
}
drawES5Chart({
cords: {x: 18, y: 30},
radius: 30
});
function drawES6Chart({size = 'big', cords = {x: 0, y: 0}, radius = 25} = {}) {
console.log(size, cords, radius);
// do some chart drawing
}
drawES6Chart({
cords: {x: 18, y: 30},
radius: 30
});
예제 출처: MDN Destructuring_assignment
함수 매개변수 디폴트 값 설정에도 주의할 점이 있다. 만약 매개변수 안에 있는 객체의 프로퍼티 중 일부만 디폴트 값으로 처리를 하고 싶은 경우가 있고 가정하자. 다시 말해 2-depth의 디폴트 값 처리이다.
Bad
function drawES6Chart({size = 'big', cords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
console.log(size, cords.src.x, cords.src.y, cords.dest.x, cords.dest.y, radius);
}
drawES6Chart({
cords: {src: {x: 18, y: 30}},
radius: 30
}); // 에러: undefined의 x, y를 참조하려고 해서 에러 발생.
위의 예제 코드는 잘 동작할 것처럼 보이지만 에러가 발생한다. 왜 에러가 발생하는 것일까?
에러의 원인을 살펴보자. 함수의 매개변수로 넘어온 cords
가 {src: {x: 18, y: 30}}
로 채워져 있기 때문에 함수 실행 시 매개변수 디폴트 값 설정 부분이 수행되지 않는다. 그러므로 coods.dest
는 undefined
이므로 dest
의 x
, y
프로퍼티를 읽게 되면 undefined
의 프로퍼티에 접근하게 되어 참조 에러가 발생한다.
앞서 보았듯이 매개변수 디폴트 값 설정은 1-depth 즉, 매개변수 자체의 프로퍼티까지만 지원한다. 위 예제 코드를 정상 동작하게 하려면 ES5 버전의 코드처럼 다시 2-depth부터 각각 optional 처리를 해주어야 한다.
Good
function drawES6Chart({size = 'big', cords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
if (cords.src === undefined) {
cords.src = {x: 0, y: 0};
}
if (cords.dest === undefined) {
cords.dest = {x: 0, y: 0};
}
console.log(size, cords.src.x, cords.src.y, cords.dest.x, cords.dest.y, radius);
}
drawES6Chart({
cords: {src: {x: 18, y: 30}},
radius: 30
}); // 정상동작함.
2-depth 이상의 매개변수 디폴트 값 설정 시만 주의하여 사용한다면 기존 ES5 코드보다는 더 간결하고 읽기 쉬운 코드로 유지할 수 있다.
ES6는 객체 리터럴이나 배열 리터럴의 사용성이 대폭 좋아졌다. Rest 파라미터나 Spread 표현 식도 그중에 하나이다.
배열이나 객체 리터럴 내부에 ...ids
와 같이 작성하면 해당 위치에 ids
의 각 배열 요소나 프로퍼티를 풀어낸다. Spread 표현식은 함수 호출이나 배열 및 객체 리터럴 내부에서 사용할 수 있다.
따라서 배열 복사나 불변(immutable)객체 생성도 손쉽게 할 수 있다. ...
연산자와 함께 풀어낼 객체를, 그리고 그 뒤에 추가/변경될 내용을 작성하면 된다.
배열을 함수의 파라미터들로 변경할 때 Spread 표현식으로 편리하게 작성할 수 있다.
var friends = ['Jack', 'Jill', 'Tom'];
textToFriends(friends[0], friends[1], friends[2]);
const friends = ['Jack', 'Jill', 'Tom'];
textToFriends(...friends);
새로운 배열에 다른 배열의 요소를 한 번에 추가하거나 새로운 객체에 다른 객체의 프로퍼티들을 추가할 때도 코드가 훨씬 깔끔하게 유지된다. 새로운 객체를 만드는 경우, Spread 표현식의 계산 결과로 인해 중복되는 키가 생긴다면 가장 나중에 작성된 표현식이 할당된다.
var friends = ['Jack', 'Jill', 'Tom'];
var anotherFriedns = [friends[0], friends[1], friends[2], 'Kim'];
var drinks = {
coffee: 'coffee',
juice: 'orange juice'
};
var newDrinks = {
coffee: drinks.coffee,
juice: 'tomato juice',
water: 'water'
};
const friends = ['Jack', 'Jill', 'Tom'];
const anotherFriedns = [...friends, 'Kim'];
const drinks = {
coffee: 'coffee',
juice: 'orange juice'
};
const newDrinks = {
...drinks,
juice: 'tomato juice',
water: 'water'
};
파라미터의 개수가 가변적인 함수에서 파라미터들을 사용하려면 arguments
객체를 배열처럼 접근해서 사용해야 했다. 하지만 someFunction(target, ...params)
형태로 Rest 파라미터 연산자를 작성하면 target
뒤에 오는 파라미터들을 모두 params
배열로 쉽게 바꿀 수 있다. 모든 인수를 바꿀 수도 있고 다음과 같이 앞서 선언한 변수를 제외한 매개변수들만 배열로 변환할 수도 있다.
function textToFriends() {
var message = arguments[0];
var friends = [].slice.call(arguments, 1); // argunemts 2번째 요소부터 친구들 배열로 만들기.
console.log(message, friends);
}
function textToFriends(message, ...friends) {
console.log(message, friends);
}
ES5 코드처럼 arguments
객체를 배열처럼 사용하지 않더라도 매개변수들을 변수와 배열로 분리해서 사용할 수 있다.
ES6의 제너레이터는 Generator
생성자나 function*
키워드로 선언한다. 제너레이터는 코드의 진행 흐름에서 잠시 빠져나갔다가 다시 돌아올 수 있는 함수이다.
function* gen() {
yield 1;
yield 2;
yield 3;
yield 4;
}
const g = gen();
참조: MDN - Generator
제너레이터를 실행하면 yield
를 만날 때까지 코드를 수행하고 대기하며 제어가 다시 제너레이터를 실행한 다음 라인으로 넘어간다. 멈춰있는 제너레이터를 재개하려면 제너레이터 객체의 g.next()
메서드를 실행하면 된다. 제너레이터의 g.next()
메서드를 수행하면 멈춰있던 위치의 yield
에서부터 다음 yield
문을 만날 때까지 코드를 수행한다. 그리고 다시 제어가 제너레이터에서 빠져나와 g.next()
메서드를 실행한 다음 라인으로 넘어간다. g.next()
의 리턴 값은 객체이며 제너레이터가 모두 수행되었는지를 알려주는 불리언 값 done
과 yield
문의 수행 결괏값인 value
프로퍼티로 구성되어있다.
function* gen() {
yield 1;
yield 2;
yield 3;
yield 4;
}
const g = gen();
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 4
console.log(g.next().value); // undefined
위 예제에서 마지막 g.next().value
가 undefined
인 이유는 모든 yield
구문이 수행되어 제너레이터가 종료되었기 때문이다.
참고: MDN - Generator 참고: MDN - function*
프로미스는 비동기 처리가 추상화된 객체이다. 사용자가 작성한 비동기 처리가 완료되거나 실패되었는지 알려주고 비동기 처리 결괏값을 반환해준다. 이를 통해 성공 시 실행할 함수, 실패 시 실행할 함수를 등록해서 편리하게 비동기 처리 코드 작성이 가능하다. 프로미스를 이용하면 비동기 처리를 위한 콜백 함수들로 여러 겹 감싸진 콜백 지옥 코드를 간결하게 작성할 수 있다. 문법부터 천천히 살펴보자.
const p = new Promise((resolve, reject) => {
// 비동기가 처리 필요한 코드
});
p.then(onFulfilled, onRejected).catch(errorCallback);
프로미스 생성자에 전달되는 함수 매개변수는 실행자(executor)라고 하며, 실행자는 프로미스 생성자가 생성한 객체를 리턴하기 전에 실행된다. 실행자의 인자인 resolve
와 reject
는 프로미스의 구현에 의해 실행자에 매개변수로 전달되는 함수이며 프로미스를 해결하거나 거부하는 함수이다. 이 두 개의 인자를 이용해서 실행자 내부의 비동기 처리의 결과를 판단하고, resolve
나 reject
함수에 후속 처리를 위해 전달할 값을 인자로 넘겨주면서 실행하여 프로미스가 완료되도록 하면 된다. 만약 실행자 내부에서 resolve
가 실행되면 then
의 첫 번째 인자인 onFulfilled
가 받게 되고, 반대로 reject
가 실행되면 두 번째 인자 onRejected
받게 된다.
const checkNumIsExceedTen = new Promise((resolve, reject) => {
setTimeout(() => {
const num = getRandomNumberFromServer();
if(Number.isNaN(num)) {
throw new Error('Value that from server must be a "number" type.');
}
if (num > 10) {
resolve(num);
} else {
reject(num);
}
});
});
checkNumIsExceedTen
.then((num) => {
console.log(`'num' is ${num}. It is exceed 10.`);
}, (num) => {
console.log(`'num' is ${num}. It is not exceed 10.`);
})
.catch((error) => {
console.log(error.message);
});
예제의 코드는 서버에서 가져온 num
변수의 값이 10을 초과하는지 확인하는 프로미스 객체를 생성했다. 그리고 프로미스가 종료된 후 실행할 콜백들을 then
으로 등록했고, 에러가 발생했을 때 에러를 출력할 콜백도 catch
를 이용해서 등록했다.
기존 ES5에서는 비동기 처리를 하기 위해서 보통 콜백 지옥, 콜백 피라미드라고 하는 형태의 코드를 작성했다. 어떤 비동기 처리의 결과를 전달받는 함수를 콜백 함수의 형태로 계속 생성하고 최종 결과를 가장 안쪽의 콜백 함수에서 전달받아 실행이 종료된다.
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
doSomethingPromise
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log('Got the final result: ' + finalResult))
.catch(failureCallback);
겹겹이 쌓여있던 콜백함수 코드가 훨씬 간단해졌다. 두 코드의 차이를 보자. 콜백 피라미드 코드에서는 함수마다 에러 처리 콜백을 전달했었다면, 프로미스 코드에서는 한 번의 catch
로 해결한다. 또한 겹겹이 쌓여가는 콜백 함수와 비교하면 프로미스는 비동기 처리들을 순서대로 연결해서 읽기 쉽게 작성할 수 있다.
프로미스는 단일 비동기 요청을 다루기 위한 객체이다. 여러 개의 비동기 요청을 처리하기 위해서는 프로미스 객체를 여러 개 사용해야 한다. 이때 Promise.all
, Promise.race 사용하면 객체들이 완료되는 상태에 따라 처리할 수 있다. 함수의 파라미터로는 배열같이 순회 가능한(iterable) 객체를 받는다.
Promise.all은 모든 프로미스가
resolve될 때까지 기다리고
Promise.race는 가장 먼저
resolve` 되는 프로미스의 이행 값을 사용한다. 각 프로미스는 순차 처리되는 것이 아니라 병렬적으로 수행된다.
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 100);
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});
Promise.race([p1, p2, p3]).then(value => {
console.log(value); // 3
});
순회 가능한 객체의 인자가 모두 resolve
되면 resolve
된 프로미스를 반환하고, 하나라도 reject
되면 첫번째로 reject
된 이유를 사용해서 reject
프로미스를 반환한다. Promise.all
에 대한 자세한 설명은 MDN - Promise.all()에서 확인할 수 있다.
async 함수를 이용해서 비동기 처리를 더욱더 간결하게 작성할 수 있다. async 함수는 여러 개의 프로미스를 사용하는 코드를 동기 함수 실행과 비슷한 모습으로 사용할 수 있게 해준다. async 키워드가 앞에 붙은 함수선언문의 함수 본문에는 await 식이 포함될 수 있다. 이 await 식은 async 함수 실행을 일시 중지하고 표현식에 전달된 프로미스의 해결을 기다린 다음 async 함수의 실행을 재개하고, 함수의 실행이 모두 완료된 뒤에 값을 반환한다. 물론 await 식은 async 함수 내에서만 유효하다. 외부에서 사용한다면 문법 에러가 발생한다.
async 함수의 반환 값은 프로미스이며, returnValue
를 반환하면 암묵적으로 Promise.resolve(returnValue)
형태로 감싸져서 반환된다. 프로미스에 catch
로 처리하던 에러는 일반 함수에서의 try/catch
문으로 작성하면 된다. 에러가 발생하면 프로미스의 reject
에 전달되는 값이 에러 객체로 넘어온다.
function fetchMemberNames(groupId) {
return getMemberGroup(groupId)
.then(memberGroup => getMembers(memberGroup))
.then(members => members.map(({name}) => name))
.catch(err => {
showNotify(err.message);
});
}
fetchMemberNames('gid-11')
.then(names => {
if (names) {
addMembers(names);
}
});
async function fetchMemberNames(groupId) {
try {
const memberGroup = await getMemberGroup(groupId);
const members = await getMembers(memberGroup);
return members.map(({name}) => name);
} catch (err) {
showNotify(err.message);
}
}
fetchMemberNames('gid-11')
.then(members => {
if (members) {
addMembers(members);
}
});
자바스크립트도 모듈 개발이 가능하다. ES5에서는 Webpack, Rollup, Parcel 같은 번들러나 Babel 같은 트랜스파일러를 사용해서 브라우저에서 실행할 수 있도록 바꿔주어야 했다. ES6에서는 모듈을 이용해서 개발할 수 있는 간결한 문법을 지원한다. 모듈 명세를 구현한 모던 브라우저들부터는 import
, export
문을 사용해서 모듈을 가져올 수 있다.
자바스크립트의 모듈은 .js
확장자로 만들어진 파일을 뜻한다. 파일 내부에서 별도의 module
등의 키워드로 선언할 필요가 없으며, 자바스크립트 모듈의 코드는 기본적으로 strict 모드로 동작한다. 모듈 안에서는 import
, export
키워드를 통해 다른 모듈과 객체를 주고받을 수 있다. 한 개의 모듈 안에서 선언된 변수나 함수 등은 그 모듈 내부 스코프를 가진다. 그렇다면 내가 작성한 모듈의 변수를 다른 모듈에서 사용하려면 어떻게 해야 할까? 바로 export
를 통해 모듈 외부에서 접근할 수 있도록 만들어주면 된다. export
문을 통해 함수, 클래스, 변수 들을 모듈 외부로 내보낼 수 있다.
그렇다면 이제 export
문을 사용하는 법을 살펴볼 것이다. 모듈 외부로 내보내는 방법은 Named export, Default export 두 가지가 있다.
students.js
export const student = 'Park';
export const student2 = 'Ted';
const student3 = 'Abby';
export {student3};
Named export는 한 파일에서 여러 번 할 수 있다. Named export를 통해 내보낸 것 들은 추후 다른 모듈에서 내보낼 때와 같은 이름으로 import
해야 한다.
studentJack.js
export default 'Jack'
반면에 Default export는 한 스크립트 파일당 한 개만 사용할 수 있다. 그리고 export default
의 뒤에는 표현식만 허용되므로 var
, let
, const
등의 키워드는 사용하지 못한다.
이렇게 내보낸 객체들은 모듈들에서 접근할 수 있다. 그렇다면 지금부터는 모듈에서 export
한 객체들을 가져오는 import
문을 살펴보자.
import {student, student2, student3} from 'students.js';
console.log(student); // "Park";
console.log(student2); // "Ted";
console.log(student3); // "Abby";
위처럼 Named export 된 객체를 가져올 때는 각 객체의 이름들을 {
, }
로 감싸면 된다. 만약 가져올 객체의 이름을 바꿔서 가져오고 싶을 때는 어떻게 할까? 별도의 변수를 선언하지 않더라도 바꾸고 싶은 객체 이름 뒤에 as [[바꿀 변수명]]
형태로 작성해서 쉽게 바꿀 수 있다.
import {student as park, student2 as ted, student3 as abby} from 'students.js';
const student = 'Kim';
console.log(student); // "Kim"
console.log(park); // "Park"
console.log(ted); // "Ted"
console.log(abby); // "Abby"
이 방법은 이미 작성한 코드의 지역 변수명과 같은 이름의 객체를 가져올때 유용하게 사용할 수 있다.
그럼, 이렇게 Named export 된 객체가 많을 때 모두 가져오려면 반드시 위 예제 처럼 각 객체를 하나씩 열거해야 할까? 아니다. *
을 이용해서 한꺼번에 가져오는 방법이 있다.
import * as students from 'students.js';
console.log(students.student); // "Park"
console.log(students.student2); // "Ted"
console.log(students.student3); // "Abby"
이번에도 *
문법으로 students.js 파일 내부의 모든 Named export 객체를 나타내주고, 바로 뒤에 as [[변수명]]
형태로 해당 객체들을 가지고 있을 변수명을 정한다.
import jack from 'studentJack';
console.log(jack); // "Jack"
사용법은 Named export와 비슷하지만 {
, }
로 감싸지 않고 변수명을 import
문 뒤에 작성한다. 변수 이름을 바꿔서 가져올 수 있는데, Default export 된 객체는 파일마다 유일하므로 as
키워드를 사용하지 않더라도 이름을 바꿔서 불러올 수 있다. import
키워드 뒤에 사용한 이름이 객체의 변수명이 된다.
앞서 소개한 객체를 가져오는 방법 Named export와 Default export는 export
와 마찬가지로 가져올 때도 한 번에 두 가지를 모두 사용할 수 있다. 한 students.js 파일에서는 객체를 내보내고 school.js 파일에서는 그 객체들을 가져와 보자.
students.js
const jack = 'Jack';
export default jack
const student = 'Park';
const student2 = 'Ted';
const student3 = 'Abby';
export {student, student2, student3};
school.js
import jack, {student, student2, student3} from 'students';
console.log(jack); // "Jack"
console.log(student); // "Park"
console.log(student2);// "Ted"
console.log(student3);// "Abby"
지금까지 ES6에서 추가된 새로운 기능들을 코드 예제를 통해 어떤 점이 좋아졌고 사용법이 어떻게 바뀌었는지 알아보았다. ECMA Script 명세가 발전함에 따라 브라우저, 자바스크립트 라이브러리, 개발에 도움이 되는 개발 환경도 함께 발전하고 있다. 개발 환경을 ES6로 바꿔서 더욱 편리해진 자바스크립트의 이점들을 누려보자.
이 문서의 내용과 연관된 FE개발랩 사내 교육은 아래와 같다. 추가로 교육을 수강할 것을 권장 한다.
이 문서는 NHN Cloud의 FE개발랩에서 작성하고 관리하는 공식 웹 프론트 개발 가이드이다. 가이드 적용 관련 문의나 문서의 오류, 개선 제안은 공식 문의 채널(dl_javascript@nhn.com)을 통해 할 수 있다.
Last Modified |
---|
2019. 03. 29 |