[개발일지] 22년 26주차 - ES5와 ES6의 차이

FeRo 페로·2022년 7월 3일
3

ES란?

ECMA

EcmaScript(ES)는 넷스케이프가 인터넷상의 다양한 스크팁트 언어를 하나로 묶기 위해 제시한 표준안이다. 이 스펙(설명)을 개발 언어로 구현한 것이 JavaScript이다. 즉 ES는 JavaScript에 대한 설명서이다. 현재는 EcmaScript2022가 최신인데(6월 22일) 매년 새로운 버전을 발표하지만, 큰 기능차이가 있는 버전은 2009년에 발표된 ES5와 2015년에 발표된 ES6이다. 이후에 발표된 것을 통칭해서 ES6+라고 부른다.

ES5와 ES6의 차이

변수선언

ES5까지는 var 변수 밖에 존재하지 않았다. var는 변수를 선언할 때 undefined가 할당이 되고 최초 선언된 이후 재할당과 재선언은 물론 블록 안과 밖에서 자유롭게 쓸 수 있다. 굉장히 동적인 변수이지만, 프로그래밍에서 동적이라는 것은 그만큼 리스크를 안고 있다는 뜻이기 때문에 단점으로 지적되어 오고 있었다. 이러한 단점을 보완하기 위해 ES6에서는 두 가지의 변수가 새로 나왔다.

const 변수와 let 변수이다. const, let 변수는 선언과 동시에 초기화가 이루어진다. 그렇기 때문에 선언 전에 호출을 하면 에러가 발생한다. 또한 let은 재할당이 가능하지만 const는 재할당을 할 수 없다. 그래서 변수가 let 인지 const 인지만 보고도 변수의 쓰임을 어느 정도 예측을 할 수 있다.

다른 중요한 점은 블록 스코프 안에 선언된 let, const 변수는 블록 밖에서 접근할 수 없고 함수 안에 선언된 let, const 변수에도 접근을 할 수 없다. 그만큼 안정성을 확보할 수 있다.

위와 같은 장점을 바탕으로 let과 const 변수가 var 변수를 완전히 대체할 수 있기 때문에 let과 const 변수가 발표된 이후 var 변수의 사용을 지양한다.

화살표 함수

화살표 함수는 매우 간편하다. 문법적으로도 그렇고 아키텍처적으로도 그렇다.

function hello1 () {
  console.log("hello");
}

const hello2 = () => {
  console.log("hello");
}

화살표 함수는 function 키워드를 쓰지 않고 이렇게 함수 선언을 해줄 수 있기 때문에 문법적으로 굉장히 간편하다. 그럼 아키텍처적으로 그렇다는 것은 어떤 의미일까?

엔진은 function 키워드를 만나면 함수 객체를 생성해서 할당한다. 이때 prototype에 있는 함수 관련 메소드도 함께 바인딩이 된다. 사용을 자주 하는 메소드이면 모르겠지만 bind, call, apply는 쓰임이 있는 경우 빼고는 사용을 잘 하지 않는 메소드들이다.

화살표 함수는 이런 함수 객체의 메소드들이 바인딩이 되지 않는다. 그래서 아키텍처적으로도 훨씬 더 간편하다.

또한 화살표 함수의 아주 큰 특징이 this를 갖고 있지 않다는 것이다. 이렇게 된 이유는 strict 모드와 관련이 있다. strict 모드에서는 함수를 호출할 때 함수 앞에 오브젝트를 반드시 작성해야 한다. 아래 코드처럼 말이다.

"use strict"
function house(){
  function room(){
    console.log(this);
  }
  room(); // window.room();라고 적어야 하나?
}
window.book();

strict 모드이기 때문에 window.house()로 함수를 호출했다. 함수 안에 있는 room 함수는 this를 콘솔로 출력해 준다. 그런데 여기서 room()을 호출해서 this를 출력할 수 있을까? 없다. 앞에 undefined가 출력된다. strict 모드이기 때문에 함수 앞에 오브젝트를 작성해야 하는데 적지 않았기 때문이다.

하지만 이를 피하기 위해 window.room이라고 적어주어도 window 오브젝트에 room 함수가 없기 때문에 에러가 발생한다. 결국 strict 모드에서 사용을 하기 위해서는 this를 별도로 저장해 주어야 한다.

이런 번거로움을 화살표 함수로 해결할 수 있게 되었다.

"use strict"
function house(){
  const room = () => {
    console.log(this);
  }
  room();
}
window.book();

화살표 함수로 작성하면 room을 호출해서 this를 출력할 수 있다. 왜냐하면 room을 화살표 함수로 작성했기 때문에 this가 없다. 이때 this는 window 객체를 참조한다. 그래서 room 안에서 this는 undefined가 아니라 window 객체가 출력되게 된다. 항상 window 객체를 참조하는 것이 아니라 상황에 따라 다르지만 대부분의 경우에는 window 객체를 참조한다.

화살표 함수를 엔진이 읽으면 function 키워드를 읽었을 때 처럼 함수 객체를 생성해서 변수에 할당을 하는데, 이때 화살표 함수가 속한 스코프를 생성한 함수 객체에 바인딩 한다. 이 객체에 바인딩 된 스코프의 this를 화살표 함수에서 this로 사용하기 때문에 그런 것이다. 그래서 항상 window 객체를 참조하지는 않는다.

Q. 그럼 const, let 변수처럼 function키워드를 완전히 대체할 수 있는가?

그건 절대 아니다. 대표적인 이유로 화살표 함수는 함수 인스턴스를 생성할 수 없다. 왜냐하면 prototype이 없이 때문에 constructor도 없기 때문이다. 이런 부분들 때문에 둘은 역할이 다르다고 할 수 있다. 그래서 하나만 계속 쓰는 것 보다는 상황에 맞게 화살표 함수와 function 키워드를 잘 쓰는 것이 최고의 선택일 것이다.

템플릿 리터럴

템플릿 리터럴은 뭘까? 아래의 코드를 보면 '아, 이거였어?'라고 할 수도 있다.

const age = 10;
const name = "홍길동";
console.log(`${age}${name} 입니다.`); // 10살 홍길동 입니다.
console.log(age+"살"+name+" 입니다.");

한 번쯤 써 본 문법인가? 그렇다면 이게 템플릿 리터럴이다. 백틱을 이용해서 문자열과 변수 값을 간단하게 사용할 수 있다.

Destructuring

비구조화 할당으로 번역이 된 이 문법은 배열이나 객체의 요소를 해체하여 별개의 변수로 할당할 수 있다. 아래 예시를 참고하면 된다.

const arr = [1,2];
// 배열의 구조 분해
const [one, two] = arr;
console.log(one, two); // 1 2

const obj = {
  one: 1,
  two: {
    two_first: 21,
    two_second: 22,
  },
};
// Object 구조 분해
const {
  two: { two_first, two_second },
} = obj;
console.log(two_first, two_second); // 21, 22

// Object구조에 맞게 변수를 할당하지 않으면 undefined가 된다.
const { two_first, two_second } = obj;
console.log(two_first, two_second); // undefined, undefined

Object는 구조에 맞게 할당을 하기 때문에 const {two:{...}}형식으로 변수를 선언한 것을 확인할 수 있다.

getter & setter

ES6에서는 ES5에서 생긴 getter과 setter를 간편하게 변형시켰다. 아래의 코드에서 차이점을 확인할 수 있다.

// ES5
const book = {};
const anotherBook = {};
Object.defineProperty(book, "title", {
  	set: function(param){
      anotherBook.title = param;
    },
	get: function(){
      return anotherBook.title;
    }
});
book.title = "곰돌이 푸";
console.log(anotherBook.title); // 곰돌이 푸
console.log(book.title); // 곰돌이 푸

// ES6
const book = {
  set title(param){
    anotherBook.title = param;
  },
  get title(){
    return anotherBook.title;
  }
};
const anotherBook = {};
book.title = "곰돌이 푸";
console.log(anotherBook.title); // 곰돌이 푸
console.log(book.title) // 곰돌이 푸

ES5에서는 object에 프로퍼티 단위로 getter와 setter를 선언하는 방식이다. 그래서 프로퍼티에 종속이라고 볼 수 있다. 하지만 ES6에서는 프로퍼티 안의 함수를 선언하는 방식으로 사용할 수 있다. 훨씬 더 확장성이 좋다.
그리고 작성이 편리하다. function 키워드를 따로 선언하지 않아도 된다. 또 다수의 getter를 쓸 수 있다.

class

ES5에서는 class라는 키워드는 없었지만 프로토타입을 통해 OOP의 구현은 가능했다. ES6에서는 class라는 키워드가 생겨서 쉽게 생성을 할 수 있다. 함수의 function 키워드처럼 키워드이지만 다른 점이 있다면 호이스팅이 안된다. 그래서 class를 선언하기 전에 호출할 수 없다. 아래 예시를 보면서 기존의 프로토타입으로 객체를 만드는 방법과 class키워드를 이용해서 만드는 방법을 비교해보자.

const Calc = function(arg1, arg2){
  this.arg1 = arg1;
  this.arg2 = arg2;
}
Calc.prototype.add = function(){
  return console.log(this.arg1 + this.arg2);
}
const num_prototype = new Calc(1,2);
num_prototype.add(); // 3

// 위 함수의 class키워드 버전
class Calc {
  constructor(arg1,arg2){
    this.arg1 = arg1;
    this.arg2 = arg2;
  }
  add() {
    return console.log(this.arg1+this.arg2);
  }
}
const num_class = new Calc(1,2);
num_class.add();  // 3

Class에는 몇 가지 규칙이 있다. 먼저, constructor는 하나여야 한다. constructor가 2개인 빌트인 객체를 본적있나? 없다. 당연한 것이라고 볼 수 있다. 다른 규칙으로는 클래스의 몸통{}은 strict mode로 실행이 된다는 점이 있다. 이 점은 빌트인 객체와 다른 점이라고 할 수 있다.

Extends & Super

부모 클래스로부터 상속을 받고 부모 클래스를 호출하기 위해서는 extends와 super를 사용한다. 참고로 extends는 프로토타입 기반의 클래스에서도 사용이 가능하다.

const Calc = function (arg1, arg2) {
  this.arg1 = arg1;
  this.arg2 = arg2;
};
Calc.prototype.add = function () {
  return console.log(this.arg1 + this.arg2);
};

class Sub extends Calc { // 프로토타입 기반의 클래스 Calc를 상속
  subtract() {
    super.add(); // 부모 클래스에서 add 사용
    console.log(this.arg1 - this.arg2);
  }
}
const ex = new Sub(2, 1);
ex.subtract(); 
// 3, 1 -> subtract 호출 -> 부모 클래스에서 add 호출 -> subtract 내 콘솔 실행

Map

Map은 key와 value의 컬렉션이지만 다양한 타입을 key로 사용할 수 있다는 점이 특징이라고 할 수 있다. 그리고 다양한 key로 작성된 Map은 for-of 문에서 작성한 순서대로 읽혀진다. 이것을 보장한다.

const obj = new Map([
  ["key", "value"],
  [()=>{console.log("익명함수")}, "Arrow function"],
  [100, "숫자"],
  [{one:"1"}, "객체"]
])
for(let inx of obj){
  console.log(inx);
}

위 코드에서 확인할 수 있는 기본 형태를 보면 Map([[...],[...],[...]]), 즉 대괄호가 연속된 모습이다. 대괄호 안에 배열의 형태로 key와 value를 설정한다. 하지만 빈 Map 인스턴스를 만들 때에는 Map()으로 사용한다.
또 new를 사용했는데, new를 사용하지 않으면 TypeError가 발생하기 때문에 new Map(...)으로 항상 인스턴스를 생성해주어야 한다.
Map은 기본적으로 key값을 비교하는데 key값이 같으면 value가 대체된다. 스펙에서는 이를 SameValueZero 비교 알고리즘이라고 기술되어 있다.

Map을 굳이 써야 하나?

Map을 보면 형태는 2차원 배열 같고, key, value로 구성되는 건 object 같은데 꼭 써야 할까? Map은 유용한 메소드들을 가지고 있다. set, get, has, entries, keys, values, clear, forEach, delete, clear처럼 값을 설정하고 삭제하고 조회하고 가공할 수 있는 꼭 필요한 메소드들을 다 가지고 있다. 그래서 mdn에서도 빈번하게 데이터를 추가 및 삭제를 할 때는 Map이 Object보다 좋은 경우가 있다고 한다. 나도 이번 공부를 하면서 알게 된 거지만 Object를 쓸 바엔, 특별한 이유가 없다면 Map을 이용해야겠다는 생각을 했다.

Set

Map과는 다르게 Set오브젝트는 value의 컬렉션이다. 형식도 Map과는 다르다. 아래 코드로 확인을 해보자.

const obj = new Set([1,2,3,4,4,"String", [], {}]);

for(let value of obj){
    console.log(value);
}

값으로 프리미티브, 오브젝트 타입 모두 사용이 가능하다. 위 출력 결과를 보면 한 가지 특이한 부분을 확인할 수 있다. 왜 4가 2개인데, 한 번만 출력이 되는걸까? Set은 SameValueZero 비교 알고리즘으로 같은 value가 있으면 나중에 것을 제외시킨다. value는 key역할도 겸하게 되는 셈이다.

Set도 Map과 마찬가지로 메소드들이 알차다. 그리고 중복 값을 허용하지 않는다는 특성 때문에 알고리즘 문제 중에 중복 값 제거로 쓰기도 한다.

profile
주먹펴고 일어서서 코딩해

0개의 댓글