[Javascript] ECMAScript 2015(ES6)+ (1)

2025. 5. 19. 09:00Javascript/Javascript

728x90
반응형

 

 

 

ECMAScript(ES6나 ES2015의 ES는 ECMAScript의 줄임말)는 Ecma 인터내셔널에서 정의한 ECMA-262 기술 규격에 정의된 표준 스크립트 프로그래밍 언어다. ECMAScript는 1997년 1판 배포 이후로 매년 2, 3판이 배포되었고 그 뒤 10년 뒤에 5판(ECMAScript 5. 이하 ES5), 다시 6년 뒤인 2015년에 6판(ECMA2015)이 배포되었다(4판은 버려짐). 6판의 정식 명칭은 ECMAScript 6이 아닌 ECMAScript 2015(이하 ES6)이다. 이전에는 배포 주기가 길었지만 빠르게 변화하는 개발 환경을 반영하여 숫자 대신 연도를 붙여 배포되는 추세이다.

 

ES6에서는 ES5 이하 명서에서 문제가 되었던 부분들이 해결되었고, 기존 코드를 더 간결하게 작성할 수 있는 새로운 문법이 추가되면서 가독성 및 유지보수성이 향상되었다. 그 덕에 웹에서 사용하는 자바스크립트 유명 라이브러리들(lodash, React, Vue 등)의 개발 환경 또한 ES6으로 바뀌었다. 따라서 최신 자바스크립트 라이브러리들도 ES6을 사용할 때 훨씬 편리하게 사용될 수 있다.

 

이 글에서는 ES5를 ES6으로 개발 환경을 바꾸면 얻을 수 있는 이점과 함께 ES6부터 발표된 자바스크립트 추가 기능들을 소개하려 한다. ES2015 이후의 스펙도 일부 포함되겠지만 편의상 모두 ES6+로 부르려 한다.

 

 


 

 

트랜스파일러를 사용한 크로스 브라우징 지원

 

지금부터 소개할 ES6 스펙들은 IE에서는 동작하지 않는 코드들이다. 그럼에도 만약 IE도 지원해야 한다면 어떻게 해야 할까? ES6을 사용하지 못하는 걸까? 정답은 '그렇지 않다'이다. 트랜스파일러(Babel)을 이용하여 브라우저 대부분에서 동작하는 자바스크립트 코드로 쉽게 변경할 수 있다.

 

 

Babel 트랜스파일러 예제

 

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에서는 바벨이 우리가 작성하는 ES6 코드를 어떻게 변환해 주는지 간단히 확인할 수 있다. 이 글에서 트랜스파일러를 개발 환경에 어떻게 적용하는지 자세한 과정은 다루지 않겠지만 크로스브라우징은 트랜스파일러가 알아서 처리해 준다는 점 정도만 기억하자. 개발자는 그저 ES6로 개발만 하면 된다.

 

 


 

 

let과 const

 

ES5까지만 해도 var 키워드를 통해 변수를 선언해왔다. var로 선언한 변수의 값은 언제나 변경할 수 있기 때문에 변경 불가능한 상수를 선언할 방법이 없었다. 따라서 이를 일반 변수와 구분하기 위해 상수값에 대한 명명 규칙을 영문 대문자와 언더바로만 제한하는 방식을 택해왔다. 또한 타 언어들과는 달리 자바스크립트에서 var로 선언한 변수는 함수 단위의 스코프를 갖기 때문에 if문이나 for문 블록 내에서 var를 선언한 변수들도 블록 외부에서 접근할 수 있었다. 게다가 var를 사용하면 선언 전에 변수의 사용이 가능한 호이스팅이 발생한다. 이러한 var 키워드의 특징 때문에 많은 개발자들은 자바스크립트를 사용하며 크고 작은 어려움을 겪어 왔다.

 

그리고 ES6에서는 앞서 언급한 문제점들에 대한 해결책으로 let과 const 두 가지 키워드가 추가됐다. let, const를 사용하여 얻을 수 있는 이점들을 차례로 살펴 보자.

 

 

블록 스코프 지원

 

let, const로 선언한 변수는 블록 스코프를 가진다. 반면 var로 선언한 변수는 함수 스코프를 가지므로 의도하지 않은 곳에서도 변수 변경이 가능하게 되어 에러가 발생할 수 있다. 변수 선언에 let, const를 사용하면 이러한 실수와 버그를 줄일 수 있다.

 

ES5

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.'
}

 

ES6

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에 접근하는 경우 RefferenceError가 발생한다.

 

 

ES5 변수 호이스팅(Hoisting)의 문제점 개선

 

var 키워드를 이용해 변수를 선언하면 이전에 변수를 사용할 수 있는 호이스팅 현상이 발생한다. 하지만 호이스팅이 없는 let이나 const를 사용해서 변수를 선언하면 에러가 발생해서 의도치 않은 실수를 줄일 수 있다.

 

ES5

hi = '안녕~';    // 변수 초기화가 먼저 되어 있지만 에러가 발생하지 않는다.

console.log(hi); // '안녕~'

var hi;          // 변수 선언은 이 부분에 있다.

 

ES6

hi = '안녕~'; // ReferenceError - 변수 hi가 선언되지 않았다.

console.log(hi);

let hi;

 

먼저 ES5의 코드를 보면 hi 변수 선언보다 먼저 값을 초기화하고 있는데도 에러가 발생하지 않는다. 왜 이런 결과가 나오는 걸까? 자바스크립트는 코드를 실행하기 전에 가장 먼저 var, function을 찾아서 스코프의 최상단에 변수와 함수를 미리 등록하기 때문이다. 이러한 호이스팅으로 인해 실수에 의한 오류를 명확하게 감지하기가 어렵고 의도치 않은 동작이 발생하기도 한다.

 

반면에 ES6의 let을 사용해서 같은 코드를 작성해 보면 에러가 발생한다. hi라는 변수의 초기화 이전에 변수가 선언되지 않았기 때문에 참조 에러가 발생하는 것이다.

 

 

let과 const의 사용법과 차이점

 

변수 선언 시에 변하지 않는 값에는 const를, 변할 수 있는 값에는 let을 사용한다. 아래 예제를 통해 사용법을 확인해 보자.

// 값 수정
let foo = 'foo';
foo = 'foo2'; // 값 수정 가능.

const bar = 'bar';
bar = 'bar2'; // Type error - bar 변수는 상수이므로 값 수정 불가능.



// 선언, 초기화
const baz2;   // Type error - const로 선언한 변수는 선언과 동시에 초기화 해야함.

let baz;      // let으로 선언한 변수는 선언과 동시에 초기화할 필요 없음.
baz = 'baz';

 

위 예제를 보면 let은 var와 유사하게 동작하며 값 변경이 가능한 것을 확인할 수 있다. 하지만 const 변수의 값은 한 번 정의하면 변경할 수 없다. 따라서 변수 선언과 동시에 초기화해야 하고 const로 선언된 변수의 값을 변경하려고 하면 문법 에러가 발생한다.

 

하지만 const를 사용한다 하더라도 그 property까지 수정할 수 없는 것은 아니다.

// const 변수의 프로퍼티 값 수정
const foo2 = {
  bar2: 'bar'
};

foo2.bar2 = 'bar2'; // foo2의 프로퍼티는 수정 가능.

 

객체나 배열 선언에 const를 사용했으므로 property나 배열 요소까지 변경할 수 없다고 생각할 수 있겠지만 참조 값을 사용할 때에는 주의해야 한다.

 

 


 

 

화살표 함수

 

화살표 함수는 this 바인딩 이슈를 해결해 주고 함수 표현식의 긴 문법을 보다 더 단축시켜 준다. 화살표 함수는 함수 표현식의 =>가 화살표를 닮아서 그 이름이 붙었는데 화살표 함수의 문법을 사용하면 기존 함수 표현식의 function 키워드가 사라지고 더 짧은 문법을 사용할 수 있다. 또한 함수 호출 시 this 바인딩 이슈를 해결해 주는 장점도 있다.

 

ES5

var sum = function(a, b) {
  return a + b;
}

 

ES6

const sum = (a, b) => {
  return a + b;
};

 

 

this 바인딩 이슈

 

ES5에서는 DOM의 이벤트 핸들러 함수를 실행할 때 핸들러가 의도한 대로 동작하지 문제가 있었다. 아래 예제를 보자.

 

ES5

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();

 

ES6

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();

 

Element에 등록된 이벤트 핸들러 함수가 실행될 때에는 non-strict 모드로 동작해서 핸들러에서 this 객체에 접근하면 이벤트를 처리하는 Element 객체가 탐색된다. 그래서 메서드를 이벤트 핸들러로 사용할 때에는 내부에서 this를 사용하는지 살펴본 후 handler.bind(this)처럼 필요한 컨텍스트의 this 객체를 함수에 바인드해서 넘겨야 했다. 하지만 화살표 함수를 사용하면 의도한 대로 동작한다. 화살표 함수는 해당 컨텍스트의 this 객체를 바인드한 함수 표현식처럼 동작한다.

 

 

코드 단축

 

화살표 함수를 사용하면 간단한 함수를 한 줄로 표현할 수 있다. 기존 함수에서 리턴하는 값에 항상 return 키워드를 붙여야 했다면 화살표 함수는 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 numsOverFive = numbers.filter(function(number) {
  return number > 5;
});

console.log(numsOverFive); // [6, 7, 8, 9, 10] ;


// 화살표 함수 사용
const numsOverFive = numbers.filter(number => number > 5)

console.log(numsOverFive); // [6, 7, 8, 9, 10] ;

 

인자로 넘겨주는 함수가 함수 표현식을 콜백 함수로 넘겨줄 때보다 훨씬 짧고 간결해졌다.

 

하지만 내부적으로는 function으로 선언한 함수와 몇 가지 차이점이 있다. 화살표 함수는 실행 컨텍스트에 this, arguments, super, new.target을 가지고 있지 않은 함수 표현식이다. 자신의 실행 컨텍스트에 별도의 this가 존재하지 않는 대신 해당 화살표 함수가 정의된 실행 컨텍스트의 this를 그대로 따른다는 특징이 있다. 따라서 생성자 함수로는 사용할 수 없다. 메서드나 생성자로 사용되지 않는 간단한 함수를 표현하는 용도로 사용하면 된다.

 

 


 

 

클래스(Class)

 

자바스크립트는 프로토타입 기반 언어다. 클래스 기반의 언어와는 달리 자바스크립트에서는 프로토타입 객체를 재사용하면서 클래스와 유사한 형태의 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에서는 생성자 함수와 그 함수의 프로토타입 객체를 확장해서 클래스를 흉내낼 수 있다. 생성자 함수로 인스턴스 객체에 속성을 설정할 수 있고 프로토타입 체인을 이용해서 인스턴스 내에 메서드를 생성하지 않고 같은 메서드를 모든 객체에서 공유할 수 있다. 하지만 이러한 구현 방식은 실수를 유발할 수도 있으며 문법이 같이 때문에 일반 함수인지 클래스 생성자 함수인지 혼동을 일으킬 수 있어 코드 가독성이 좋지 않다.

 

ES5

function Person(name) {
  this.name = name;
}

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

var fred = new Person('fred');

 

그리고 이러한 ES5 예제 코드와 똑같은 기능을 하는 클래스를 ES6에서는 아래와 같이 class 키워드로 쉽게 작성할 수 있다.

 

ES6

class Person {
  constructor(name) {
    this.name = name
  }

  sayMyName() {
    console.log(`My name is "${this.name}"`);
  }
}

const fred = new Person('fred');

 

ES6 예제 코드를 보면 어느 코드가 클래스이고 생성자 함수인지 쉽게 확인할 수 있고 메서드도 클래스 내부에 캡슐화되어 가독성이 좋아진 것을 확인할 수 있다.

 

 


 

개선된 객체 리터럴(Object literal)

 

 

짧아진 객체 리터럴

 

ES6에서는 객체 리터럴의 key 텍스트와 value에 올 변수 이름이 같은 경우 한 번만 입력해도 된다. 기존 객체 리터럴에서 반복적으로 입력했던 콜론(:)과 변수명을 한 번의 입력으로 해결할 수 있다.

 

ES5

var iPhone = '아이폰';
var iPad = '아이패드';
var iMac = '아이맥';

var appleProducts = {
  iPhone: iPhone,
  iPad: iPad,
  iMac: iMac
};

 

ES6

const iPhone = '아이폰';
const iPad = '아이패드';
const iMac = '아이맥';

const appleProducts = {iPhone,  iPad,  iMac};

 

ES5 코드에서는 appleProducts를 정의할 때 프로퍼티의 이름, 값으로 올 표현식을 매번 콜론으로 나누어 작성해야 했지만 ES6 코드를 보면 콜론 없이 미리 정의된 변수만 입력하고 있다. 이렇게 작성하기만 해도 객체 리터럴이 생성하는 새 객체에 변수명과 같은 프로퍼티 key를 만들고 변수의 값을 프로퍼티의 값으로 대입해 준다.

 

 

축약형 메서드 이름

 

객체의 메서드를 정의할 때 유용한 축약형 메서드 이름도 지원한다. function 키워드와 메서드 이름 뒤의 콜론은 생략할 수 있다.

 

ES5

var dog = {
  name: '멍멍이',
  bark: function () {
    console.log('멍멍!')
  }
};

dog.bark(); // '멍멍!';

 

ES6

const dog = {
  name: '멍멍이',
  bark() {
    console.log('멍멍!')
  }
};

dog.bark(); // '멍멍!';

 

 

계산된 값의 사용

 

ES5에서는 객체를 먼저 생성 후 [] 접근자를 이용해서 동적으로 프로퍼티 할당을 해줬지만 ES6부터는 표현식의 연산 값을 객체의 key로 사용할 수 있게 되었다. 사용법은 객체 프로퍼티의 key가 올 자리에 [ ]로 감싼 표현식을 작성하면 된다.

 

ES5

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!';

 

 

ES6

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!'
}

 

 


 

 

탬플릿 리터럴(Template literal)

 

 

탬플릿 리터럴 문법은 백틱( ` )으로 감싼 문자열로 이루어져 있다. 기존에 문자열을 조작하는 경우에는 각기 분리된 문자열 리터럴을 + 연산자로 연결해 줘야 했다면, 탬플릿 리터럴은 내부에 표현식을 작성하여 더욱 간결한 문법으로 구현할 수 있게 해 준다. 문자열 사이에 표현식의 리턴 값을 추가하려면 표현식이 올 자리에 ${expression}을 작성하면 된다.

 

ES5

var brandName = 'TOAST';
var productName = 'UI';

console.log('Hello ' + brandName + ' ' + productName + '!'); // 'Hello TOAST UI!';

 

ES6

const brandName = 'TOAST';
const productName = 'UI';

console.log(`Hello ${brandName} ${productName}!`); // 'Hello TOAST UI!';

 

ES6 코드를 보자. brandName과 productName을 각기 표현식으로 사용할 수도, 둘을 합친 탬플릿 문자열을 표현식으로 중첩해 사용할 수도 있다. 도한 게행 문자를 직접 사용하지 않으면 한 줄 이상의 문자열을 표현할 수 없는 기존 문자열 리터럴과는 달리 탬플릿 리퍼털은 두 줄 이상의 문자열을 표현할 수 있으며 이 경우 게행 문자가 문자열 내에 자동으로 포함된다.

 

 

ES5

var howToDripCoffee = '1. 물을 끓인다.\n2. 커피 원두를 간다.\n3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.\n4. 끓인물을 천천히 필터 위로 흘려내린다.';

 

ES6

const howToDripCoffee = `1. 물을 끓인다.
2. 커피 원두를 간다.
3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
4. 끓인물을 천천히 필터 위로 흘려내린다.`;

 

 

탬플릿 태그

 

또한 탬플릿 태그라는 기능이 지원되어서 변수가 포함되는 문자열과 그 문자열을 사용하는 함수 실행에 있어 조금 더 간결하게 표현할 수 있다. 자세한 설명은 여기서 확인할 수 있다. 이런 기능을 가진 탬플릿 리터럴을 통해 탬플릿 엔진이나 라이브러리르 별도로 로드하지 않고도 문자열을 더욱 더 편하게 조작할 수 있다.

 

탬플릿 태그를 직접 구현해도 되지만 common-tags아 같은 라이브러리를 사용할 수도 있다. 아래는 제공되는 태그 중 하나인 stripIndents 태그이다. 바로 위의 ES6 예제에서는 첫 줄이 게행되지 않았고 들여쓰기가 맞지 않아 코드 가독성이 좋지 않다. 하지만 striptIndents를 사용하면 첫 게행을 무시해 주고 각 줄의 들여쓰기 또한 제거해 줘서 게행된 문자열에 대한 처리가 간편해진다.

 

import {stripIndents} from 'common-tags'

stripIndents`
  1. 물을 끓인다.
  2. 커피 원두를 간다.
  3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
  4. 끓인물을 천천히 필터 위로 흘려내린다.
`
// 1. 물을 끓인다.
// 2. 커피 원두를 간다.
// 3. 갈린 원두를 커피 필터 위에 두고, 필터를 컵에 올려놓는다.
// 4. 끓인물을 천천히 필터 위로 흘려내린다.

 

 

 

 

 

 

 

728x90
반응형