👨‍🏫
ECMAScript 튜토리얼

ECMAScript 6를 바탕으로 JavaScript에서 새롭게 사용할 수 있는 문법들을 정리해보았습니다.

Posted by 재그지그 on April 29, 2019

JavaScript Translation


이 글은 SoloLearn의 『Intro to ES6』 시리즈를 참조하여 의역, 번역하여 작성한 것임을 밝힙니다.

ECMAScript 6를 왜 배워야 하나요

ECMAScript(ES)는 표준화된 JavaScript를 만들기 위해 만들어진 사양(specifications)입니다. 그 중에서도 ECMAScript 6(ES6) 또는 ECMAScript 2015라고도 알려진 6번째 버전에서, 다양한 문법들이 새로 추가되었습니다.

ES6는 JavaScript에서 이전에는 없었던 새로운 코딩 규칙(Coding Convention)과 OOP 개념을 추가하면서, JavaScript를 사용하는 개발자들로부터 큰 인기를 얻게 되었답니다. 따라서 이번 포스트에서는 이제 JavaScript를 쓰는 사람이라면 거의 필수적으로 알아야하는 ES6의 문법과 그 특징에 대해서 알아보고자 합니다.

ECMAScript와 JavaScript의 차이점에 대해 궁금하신 분들은, 이전에 작성했던 포스트 JavaScript와 ECMAScript는 무슨 차이점이 있을까?를 참고해주세요.

var vs let, const

ES6에서는 변수를 크게 3가지 방법으로 선언(Declare)할 수 있습니다.

var a = 10;
const b = 'hello';
let c = true;

변수를 선언할 때 쓰는 타입은 변수의 스코프(Scope)에 따라서 결정하게 됩니다. 스코프는 모든 프로그래밍 언어에서 해당 변수에 접근가능한 영역을 나타낼 때 사용됩니다.

JavaScript에서 일반적으로 변수 선언에 사용되는 키워드 var는 전체 함수에서 블록 스코프(Block Scope)와 관계 없이 전역(Global)변수나 지역(Local)변수를 선언할 때 쓰이죠. 따라서 블록 스코프 밖에서도 해당 변수를 참조할 수 있었는데, 이것이 혼란을 일으키곤 했습니다. 하지만 let은 변수의 범위를 현재 선언되어 있는 블록까지로 제한합니다.

if (true) {
    let hello = 'Hello';
}
console.log(hello); // undefined

위와 같은 예제에서 변수 hellolet으로 선언되었기 때문에, if문 내부에서만 접근 가능합니다. varlet의 스코프 차이는 아래의 예시에서 확인해 볼 수 있습니다.

var x = 1;
if (true) {
    var x = 2;
    console.log(x); // 2
}
console.log(x); // 2
let x = 1;
if (true) {
    let x = 2;
    console.log(x); // 2
}
console.log(x); // 1

var는 블록 스코프가 아니기 때문에, 해당 블록보다 상위에 있는 동명의 변수에 접근하여 값을 바꿀 수 있습니다. 하지만 let은 블록 스코프이면서 호이스팅(Hoisting, 스코프 안의 어디에서든 변수 선언은 최상위 블록에서 선언된 것과 동등하다는 개념)이 아니기 때문에, let의 선언은 현재 실행되고 있는 블록 스코프 밖으로 이동하지 않습니다.

다음으로는 const에 대해서 알아봅시다. constlet처럼, 블록 스코프의 범위를 가지고 있습니다. 하지만 let과는 다르게 const는 변수 재할당이 불가능하다는 특징을 갖고 있습니다.

const a = 'Hello';
a = 'Bye'; // Uncaught TypeError: Assignment to constant variable.

여기서 정말 주의해야 할 점! 변수의 재할당이 불가능한 것이지, 변수의 수정은 가능하답니다. 특히 변수가 Object거나 Array일 경우에 헷갈릴 수 있습니다. 아래 예시를 참조해주세요.

// Object의 property 수정은 가능합니다.
const man = {
    name: 'john'
};
man.name = 'mike';
console.log(man.name); // mike

// 하지만 Object를 재할당 할 수는 없습니다.
man = {
    name: 'peter'
} // Uncaught TypeError: Assignment to constant variable.
// Array의 item들을 수정할 수 있습니다.
const dishes = ['bread', 'coffee'];
dishes.push('butter');
console.log(dishes); // ['bread', 'coffee', 'butter']

// 하지만 Array를 재할당 할 수는 없습니다.
dishes = ['rice', 'cake'] // Uncaught TypeError: Assignment to constant variable.

템플릿 리터럴(Template Literal)

템플릿 리터럴은 복잡한 변수를 String타입으로 나타낼 때 매우 유용하게 사용됩니다. 아래는 템플릿 리터럴 없이 변수를 String으로 표현하는 예시입니다.

var name = 'John';
var age = 24;
var message = name + '\'s age is ' + age + '.';
console.log(message); // John's age is 24.

ES6 이전에는 위와 같은 방법으로 각각의 변수를 + 연산자를 이용해 붙여야 했죠. 하지만 ES6에서는 템플릿 리터럴을 이용해 좀 더 직관적인 String 표현 방식을 제공합니다.

const name = 'John';
const age = 24;
const message = `${name}'s age is ${age}`;
console.log(message); // John's age is 24.

템플릿 리터럴은 문자열을 따옴표나 쌍따옴표 대신, 키보드의 탭 키 위에 있는 `(backtick, 백틱)으로 감싸며, 그리고 String에서 동적으로 삽입되는 내용들은 ${}를 이용해 감쌉니다. 이 ${}안에는 복잡한 논리적 표현식들이 들어와도 됩니다.

let a = 70;
let b = 30;
let message = `a is ${a > b ? 'bigger': 'smaller'} than b.`;
console.log(message); // a is bigger than b.

화살표 함수(Arrow Function)

ES6 이전에는 함수를 아래와 같은 방법으로 선언했습니다.

function add(x, y) {
    var sum = x + y;
    return sum;
}

ES6에서는 함수를 선언하는 새로운 방법을 만들었는데요, 화살표가 들어있다고 해서 일반적으로 화살표 함수(Arrow Function)라고도 부릅니다.

const add = (x, y) => {
    const sum = x + y;
    return sum;
}

화살표 함수에서 파라미터가 1개인 경우에는 아래와 같이 괄호를 생략할 수 있습니다.

const double = x => {
    const sum = x * 2;
    return sum;
}

화살표 함수에서 파라미터가 없는 경우에는 빈 괄호를 넣으면 됩니다.

const alert = () => {
    console.log('Hello');
    alert('Hello');
}

그리고 함수가 한 줄로 표현 가능한 식일 경우 중괄호를 생략할 수 있습니다. 이때 해당 표현식은 return식으로 표현됩니다.

const triple = x => x * 3; // return x * 3;

따라서 콜백 함수에서 유용하게 쓰일 수 있습니다. 아래의 예제는 배열의 각 요소에 2를 곱해 출력하는 함수입니다.

const arr = [1, 2, 3];

// Normal
arr.forEach(function(item) {
    console.log(item * 2);
})

// Arrow Function
arr.forEach((item) => console.log(item * 2))

하지만 일반 함수와 화살표 함수 사이의 가장 큰 차이점은 바로 this의 바인딩이 서로 다르다는 점입니다. 이 부분은 심화된 부분이라 JavaScript 개발자라면 꼭 알아야 하는 this 포스트에서 자세히 확인할 수 있습니다.

기본 매개변수(Default Parameters)

ES6에서는 함수를 선언할 때, 대입 연산자(=)를 이용해 각 매개변수의 기본 값을 설정할 수 있습니다.

function sum(a, b = 3, c = 42) {
    return a + b + c;
}

console.log(sum(5)); // 50

기본 매개변수의 값은 함수가 호출될 때, 왼쪽 값에서부터 오른쪽 값으로 대입됩니다.

반복문(Loop)

일반적인 JavaScript 구문에서는 반복을 위한 for문을 아래와 같은 문법으로 사용했습니다.

let arr = [1, 2, 3];
for (let k = 0; k < arr.length; k++) {
    console.log(arr[k]); // 123
}

for...in 문법을 이용하면 객체의 열거형(Enumerable) 속성명 값을 반복적으로 순환할 수 있습니다.

let obj = {
    a: 1,
    b: 2,
    c: 3,
    d() {
        console.log('d');
    }
};
for (let v in obj) {
    console.log(v); // abcd
}

let arr = ['a', 'b', 'c'];
for (let v in arr) {
    console.log(v); // 012
}

for...in 문법은 JavaScript 엔진에 따라 임의의 순서로 순환을 할 수 있기 때문에, 배열을 순환하기 위한 목적으로 사용하는 것은 적절하지 않습니다. 또한, 순환하면서 참조하는 변수를 항상 String 타입으로 가져오므로 주의해야 합니다.

ES6에서는 반복 가능한 객체를 순환하는 새로운 문법 for...of를 소개합니다. 사용하는 방법은 for...in과 거의 비슷합니다.

let list = ['x', 'y', 'z'];
for (let val of list) {
    console.log(val); // xyz
}

for...of에서 사용할 수 있는 반복 가능한 객체의 종류는 Array, String, Map, Set 등이 있습니다.

객체(Object)

JavaScript의 객체(Object)속성(Property)라 불리는 다양한 값들을 포함한 데이터 타입입니다. 이 속성들 중에는 객체 내부에서 선언된 함수인 메소드(Method)를 포함할 수 있습니다.

ES6에서는 객체를 선언할 때 축약된 문법을 사용할 수 있습니다. 메소드의 경우, 콜론(:)이나 함수 선언 없이 선언할 수 있습니다.

let tree = {
    height: 10,
    color: 'green',
    grow() {
        this.height += 2;
    }
};
tree.grow();
console.log(tree.height); // 12

또한 ES6에서는 객체를 초기화 할 때, 외부에서 이미 선언되어 있는 변수와 같은 이름의 속성을 축약해 표기할 수 있습니다.

let height = 5;
let health = 100;

let athlete = {
    height, // same as `height: height`
    health // same as `health: health`
}

객체를 생성할 때, 중복되는 속성 키 값이 있다면 마지막에 선언된 값만 유효합니다.

var a = {
    x: 1,
    x: 2,
    x: 3,
    x: 4
}

console.log(a); // {x: 4}

계산된 속성명(Computed Property Name)

ES6에서는 대괄호([])를 이용해 객체의 속성을 동적으로 선언할 수 있습니다.

let prop = 'name';
let id = '1234';
let mobile = '08923';

let user = {
    [prop]: 'Jack',
    [`user_${id}`]: `${mobile}`
};

console.log(user); // {name: 'Jack', user_1234: '08924'}
var i = 0;
var a = {
    ["foo" + ++i]: i,
    ["foo" + ++i]: i,
    ["foo" + ++i]: i
};

console.log(a); // {foo1: 1, foo2: 2, foo3: 3}

이 문법은 특정 변수에 따라 달라지는 객체의 속성명이 필요할 때 유용합니다.

Object.assign()

ES6에서는 하나 이상의 출처 객체(source object)로부터 대상 객체(target object)로의 속성을 복사할 때 사용되는 Object.assign() 메소드가 추가되었습니다.

let person = {
    name: 'Jack',
    age: 18,
    sex: 'male'
};
let student = {
    name: 'Bob',
    age: 20,
    xp: '2'
}

let newStudent = Object.assign({}, person, student)

console.log(newStudent); // {age: 20, name: "Bob", sex: "male", xp: "2"}

위의 예제에서, Object.assign()의 첫 번째 파라미터는 새 속성들이 복사될 대상 객체(target object)입니다. 그리고 첫 번째 파라미터 이후로 전달되는 객체들은 출처 객체(source object)로, 대상 객체에 복사될 값들을 전달하는 객체입니다.

파라미터로 전달할 출처 객체의 갯수에는 제한이 없으나, 파라미터에 나중에 들어온 출처 객체가 대상 객체의 속성을 덮어쓰기 때문에 파라미터를 전달하는 순서는 매우 중요합니다.

다음은 Object.assign()를 이용해 객체를 복사하는 예시입니다. 다만 중첩된 객체의 경우에는 여전히 변수를 참조하고 있으니 주의해야 합니다.

let person = {
    name: 'Jack',
    age: 18
};

let newPerson = Object.assign({}, person);
newPerson.name = 'Bob';

console.log(person.name); // Jack
console.log(newPerson.name); // Bob

비구조화 할당(Destructing Assignment)

ES6에서는 비구조화 할당 문법을 이용해 배열이나 객체의 속성을 새로운 변수로 할당할 수 있습니다.

배열의 경우, 좌측의 변수 배열은 원본 배열로부터 0번 째 인덱스부터 차례대로 변수를 할당받습니다.

let arr = ['1', '2', '3'];
let [one, two, three] = arr;

console.log(one); // 1
console.log(two); // 2
console.log(three); // 3

함수의 return 값이 배열일 경우에도 사용할 수 있습니다.

let a = () => {
    return [1, 3, 2];
}

let [one, ,two] = a();

위의 예시에서 두 번째 전달인자가 비어있다는 것에 주의하세요.

배열의 비구조화 할당을 이용하면, ES6에서는 두 변수의 값을 바꾸는 함수를 간략하게 나타낼 수 있습니다.

let a = 4, b = 8;
[a, b] = [b, a];
console.log(a, b); // 8 4

객체에서도 비구조화 할당을 이용할 수 있습니다.

let obj = {
    height: 100,
    isBig: true
};
let {height, isBig} = obj;

console.log(height, isBig); // 100 true

변수에 새 이름을 붙여 비구조화 할당을 하는 것도 가능합니다.

var o = {
    height: 42,
    isBig: true
};
var {height: foo, isBig: bar} = o;

console.log(foo); // 42;
console.log(bar); // true;

Rest 파라미터와 Spread 연산자

ES6 이전에, 몇 개의 전달인자를 함수에 전달하기 위해 argument 객체를 사용했었습니다. argument는 유사 배열의 역할을 하는 객체로, 해당 함수의 파라미터를 참조하는 객체입니다.

ES6에서는 이를 대체하는 Rest 파라미터를 사용해 더 간략하게 나타낼 수 있습니다. Rest 파라미터는 함수의 선언문에 Spread 연산자를 이용해 가변 길이의 파라미터 배열을 받는 용도로 사용됩니다.

const foo = [1, 2, 3, 4, 5];
const [first, second, ...nums] = foo;

console.log(first); // 1
console.log(second); // 2
console.log(nums); // [3, 4, 5]
function myFun(a, b, ...manyMoreArgs) {
    console.log("a", a); // a, one
    console.log("b", b); // b, two
    console.log("manyMoreArgs", manyMoreArgs); // manyMoreArgs, [three, four, five, six]
}

myFun("one", "two", "three", "four", "five", "six");

위의 예제에서 ...numsmanyMoreArgs가 Rest 파라미터입니다. Rest 파라미터의 타입은 배열이며, 오직 마지막 파라미터만이 Rest 파라미터가 될 수 있습니다. 만약 Rest 파라미터에 아무것도 전달되지 않는다면 Rest 파라미터는 빈 배열이 됩니다.

Spread 연산자는 Rest 파라미터와 유사하지만, 객체나 배열, 함수 호출에서 다른 목적으로 사용됩니다.

함수 호출

함수 호출에서 Spread 연산자는 함수의 전달인자를 개별 요소로 분리합니다. 아래와 같이 사용됩니다.

function myFunction(x, y, z) {
    // ...
};
var args = [0, 1, 2];
myFunction(...args);

배열

배열에서 Spread 연산자는 배열의 각 요소를 개별적으로 분리합니다. 이 때 ...는 배열의 어디에서든 사용할 수 있으며, 여러 번 사용될 수도 있습니다.

var parts = ['shoulders', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes'];
// ["head", "shoulders", "knees", "and", "toes"]

객체

객체에서 Spread 연산자를 이용해 객체의 열거 가능한 속성들을 복사할 수 있습니다.

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// Object { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// Object { foo: "baz", x: 42, y: 13 }

클래스(Class)

ES6에서는 클래스 문법을 이용해 JavaScript를 좀 더 객체지향적으로 이해할 수 있게 도와줍니다. 이 때, 실제로 객체지향적 모델을 제공하는 것은 아닙니다.

클래스는 class 키워드를 이용해 선언할 수 있으며, 생성자로 constructor 메소드를 이용합니다. 새 객체 인스턴스를 생성할 때는 new 키워드를 이용합니다. 생성자는 클래스에 하나만 존재할 수 있습니다.

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
}

const squre = new Rectangle(5, 5);
const rect = new Rectangle(2, 3);

클래스는 호이스팅 되지 않으므로, 반드시 선언 이후에 사용해야 합니다.

var p = new Rectangle(); // ReferenceError

class Rectangle {}

클래스 메소드

ES6에서는 클래스에서 메소드를 선언할 때 function 키워드를 붙이지 않아도 됩니다. 특정 함수에 키워드를 붙이면 클래스 객체에서 사용 가능한 메소드인 프로토타입 메소드를 선언할 수 있습니다.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // 메소드
  calcArea() {
    return this.height * this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100

static 메소드

클래스의 메소드에 static 키워드를 붙이면 정적 메소드가 정의됩니다. 이 메소드는 클래스의 인스턴스에서는 호출할 수 없으며, 인스턴스 없이 호출되는 특별한 메소드입니다.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.sqrt(dx * dx + dy * dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

위의 예제에서, distance 메소드는 Point 인스턴스의 선언 없이 클래스에서 바로 호출되고 있습니다.

상속(Inheritance)

ES6에서는 extends 키워드를 이용해 클래스 선언이나 자식 클래스를 생성할 수 있습니다.

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(this.name + ' makes a noise.');
    }
}

class Dog extends Animal {
    speak() {
        console.log(this.name + ' barks.');
    }
}

위의 예제에서, Dog 클래스는 Animal 클래스의 자식 클래스이며 속성과 메소드를 상속하고 있습니다.

Map

Map 객체는 한 쌍의 키와 값을 저장하는 객체입니다.

Object 역시 키와 값을 저장한다는 점에서 Map과 매우 유사하지만 몇 가지 차이점이 있습니다.

  1. Map의 키 값은 함수, 객체, 원시 자료형(Primitive)등 어떤 값도 사용할 수 있습니다.
  2. Map은 삽입된 순서대로 정렬합니다.
  3. Mapsize 속성을 제공합니다.
  4. Map은 순회(iterate)가 가능합니다.
  5. Map은 키의 잦은 삽입 또는 삭제의 경우 더 빠릅니다.

아래의 예제는 Map에서 키와 값 쌍이 몇 개인지 반환하는 예시입니다.

let map = new Map([
    ['k1', 'v1'],
    ['k2', 'v2']
]);

console.log(map.size); // 2

아래의 예제는 Map을 활용한 예제입니다. Map의 메소드 목록은 MDN의 Map에서 참조할 수 있습니다.

let map = new Map();

map.set('k1', 'v1').set('k2', 'v2');

console.log(map.get('k1')); // v1
console.log(map.has('k2')); // true

for(let kv of map.entries()) {
    console.log(kv[0], kv[1]); // k1 v1, k2 v2
}

Set

Set 객체는 유일한 값을 저장하는 객체입니다. 즉, 하나의 Set 안에는 중복되는 값이 없습니다.

아래의 예제는 Setsize를 반환하는 예제입니다.

let set = new Set([1, 2, 4, 2, 59, 9, 4, 9, 1]);

console.log(set.size); // 5

아래 예제는 Set을 활용한 예제입니다. Set의 메소드 목록은 MDN의 Set에서 참조할 수 있습니다.

let set = new Set();

set.add(5).add(9).add(59).add(9);

console.log(set.has(9)); // true

for (let v of set.values()) {
    console.log(v); // 5, 9, 59
}

모듈(Modules)

ES6에서는 JavaScript를 파일 별로 나누어 관리할 수 있는 문법을 제공합니다. 이를 통해, JavaScript 파일을 모듈화하여 재사용성을 높이고 유지보수에 도움을 줍니다. 이 때 exportimport 키워드를 사용합니다.

우선 export 키워드를 이용해 현재 JavaScript 파일의 함수, 객체, 원시 값들을 내보낼 수 있습니다.

// 먼저 선언한 함수 내보내기
export { myFunction };

// 상수 내보내기
export const foo = Math.sqrt(2);

// 기본 내보내기
export default function() {}

// 클래스 내보내기
export default class {}

그리고 내보낸 값들을 다른 파일에서 사용하기 위해 import 키워드를 이용합니다.

// 모듈 전체를 가져와 myModule이라는 변수에 바인딩
import * as myModule from "my-module.js";

// 모듈에서 myMember만 가져오기
import {myMember} from "my-module.js";

// 모듈에서 foo와 bar만 가져오기
import {foo, bar} from "my-module.js";