ES2015+(ES6) 문법
안녕하세요 🐸 최근 Node.js를 공부 중 인데요. 자바스크립트가 뒷받침이 되어야 Node.js도 할 수 있겠다 생각합니다.
알고 있었던 것도 있지만 모르는 것들도 있고, 알고는 있지만 현업에서 잘 써본적이 없어 까먹고 있었던 것들을 다시 상기 해볼 수 있는 기회네요. ES2015(ES6) 이후로 추가된 기능들에 대해 간략하게 짚고 넘어가보겠습니다.
물론 각 항목은 하나의 게시글을 통해 자세히 적어야 할 만큼 분량이 많지만, 오늘은 무엇이 있는지, 그리고 어떤 역할을 하는지에 대해서만 요약해보겠습니다.
const, let
var
와 같은 변수 선언 키워드입니다. 각각에 차이점이 있으니 간단히 정리해봅니다.
var
은 기존에도 사용하던 변수 선언 방법이지만 비교를 위해 함께 정리합니다.
변수 선언 키워드(var
, let
, const
)에서는 호이스팅, TDZ 등의 부분도 상이한 부분이 많습니다.
const
- 값 재할당 불가
- 선언과 초기화가 동시에 필요
- 객체의 속성은 변경 가능
- 같은 이름의 변수 선언 불가
1
2
3
4
5
const newObj; // 에러 - 초기화 필요
const constObj = {};
constObj = "new Obj"; // 에러 - 값 재 할당 불가
constObj.name = "속성 변경"
const constObj = ""; // 에러 - 동일한 이름의 변수 선언 불가
let
- 값 재할당 가능
- 선언과 초기화 동시 필요 X
- 같은 이름의 변수 선언 불가
1
2
3
4
5
6
let newObj;
newObj = {}; // 선언과 초기화를 따로 해도 허용
let letObj = {};
letObj = ""; // 값 재 할당 허용
let letObj; // 에러 - 동일한 이름의 변수 선언 불가
var
- 값 재할당 가능
- 선언과 초기화 동시 필요 X
- 같은 이름의 변수 선언 가능
템플릿 문자열
문자열의 선언을
\
(백틱) 으로 선언합니다. 그리고 문자열 내에서${}
키워드를 사용하여 변수를 문자열 내에 사용 할 수 있습니다.
기존 자바스크립트의 문자열 선언에는 싱글 쿼터('
)와 더블 쿼터("
) 를 사용했습니다.
그리고 문자열 중간에 변수를 입력하기 위해서는"문자열" + 변수 + "입니다"
이러한 형식을 가졌습니다.
코드가 지저분하고 가독성이 떨어지기 때문에 사용하면 좋습니다.
사용 예시와 기존 코드와의 비교입니다.
1
2
3
4
const oldData = "기존의 문자열";
const oldStr = "이것은 " + oldData;
const newData = "템플릿 문자열";
const newStr = `이것은 ${data} 입니다`;
객체 리터럴
기존보다 간결하게 객체 리터럴을 표현할 수 있게 되었습니다.
기존 코드와 비교하여 먼저 코드를 확인하며 보겠습니다..
기존 코드
1
2
3
4
5
6
7
8
9
10
11
12
// 기존 코드
var attrObj = "attribute";
var es = "es";
var oldObj = {
plus : function(a,b){ // 객체의 메서드에 function 사용
return a+b;
},
attrObj : attrObj, // 키:값이 동일해도 키:값을 명시
};
oldObj[es+6] = "javascript"; // 동적 속성을 외부에서 선언
ES2015+ 코드
1
2
3
4
5
6
7
8
9
const attrObj = "attribute";
const es = "es";
const newObj = {
plus(a,b){ // 객체의 메서드 선언시 function 생략
return a+b;
},
attrObj, // 키:값이 동일한 경우 코드 생략
[es+6] : 'javascript' // 동적 속성을 내부에서 선언
}
화살표 함수
함수 선언 방법을 보다 간략하게 할 수 있습니다.
다양하게 쓰입니다만 주로 익명 함수 선언에 많이 사용됩니다. 하지만 화살표 함수가 function(){}
을 완전히 대체하는 것은 아닙니다. this
가 가리키는 값이 다릅니다.
1
2
function plus1(a,b){ return a+b } // function 을 사용한 함수 선언
const plus2 = (a,b) => a+b;
함수가 return
만 가지고 있다면 return
을 생략할 수 있습니다.
위에서는 plus1()
함수는 return
외의 동작을 가지지 않기 때문에 이를 화살표 함수로 표현한 plus2
에서도 return
이 생략되었습니다.
function 과 다른 this
앞서 화살표 함수는 this
가 가리키는 값이 다르기 때문에 function(){}
을 완전히 대체하지는 않는다고 했습니다.
1
2
3
4
5
6
7
8
var parent = {
children : ['son','daughter'],
log(){
this.children.forEach(child=>{ // 화살표 함수 사용 부분
this;
});
}
}
여기서 this
는 parent
객체를 가리킵니다.
화살표 함수를 사용하지 않은 예제입니다.
1
2
3
4
5
6
7
8
var parent = {
children : ['son','daughter'],
log(){
this.children.forEach(function(child){ // 화살표 함수를 사용하지 않은 부분
this;
});
}
}
여기서 this
는 parent
객체를 가리키지 않습니다. 이 경우 this
는 전역 객체를 가리킵니다.
그렇기 때문에 this
의 사용을 원하지 않는다면 function(){}
을 사용할 수도 있습니다.
구조 분해 할당
객체 혹은 배열의 값을 가져오는 방법이 보다 간결해졌습니다.
객체의 구조 분해 할당
다음은 기존에 특정 객체의 속성을 변수에 담는 예시입니다.
1
2
3
4
5
6
const human = {
name : '김도형',
age : 24
}
const name = human.name;
const age = human.age;
human
이라는 객체의 name
과 age
에 접근하기 위해 2줄의 코드가 필요합니다.
다음은 구조 분해 할당을 사용한 방법입니다.
1
2
3
4
5
const human = {
name : '김도형',
age : 24
}
const {name, age} = human;
코드가 한 줄로 간결해졌습니다. 단 주의 사항은 가져오고자 하는 속성명과 변수명이 동일해야 합니다.
배열의 구조 분해 할당
다음은 기존에 배열의 값을 변수에 담는 예시입니다.
1
2
3
4
5
6
const arr = ['str',true,10,{},[0,1,2,3]];
const str = arr[0];
const bool = arr[1];
const num = arr[2];
const obj = arr[3];
const list = arr[4];
단순 반복 코드가 너무 많아 가독성이 떨어집니다.
다음은 구조 분해 할당을 사용한 방법입니다.
1
2
const arr = ['str',true,10,{},[0,1,2,3]];
const [str, bool, num, , list] = arr;
여기서 체크할 부분은 arr
의 4번째 데이터인 객체 형식의 값을 가져오지 않기 위해서 변수를 쓰지 않고 쉼표를 사용한 부분입니다.
클래스
자바스크립트의 프로토타입 문법을 보다 가독성 좋게 사용할 수 있습니다.
프로토타입 문법은 생성자나 상속 등을 표현하기 위한 코드가 가독성이 떨어지는데 이를 깔끔하게 처리할 수 있습니다.
자바나 C#같은 class 기반의 객체 지향 언어를 학습했던 사람에게는 특히나 프로토타입 문법은 새로 공부해야하고 적응도 쉽지 않기 때문에 큰 도움이 됩니다.
프로토타입 문법 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Parent 객체
var Parent = function(name, age){
this.name = name;
this.age = age;
}
Parent.isParent = function(parent){
return parent instanceof Parent;
}
Parent.prototype.myName = function(){
console.log(`my name is ${this.name}`);
}
// Parent를 상속받는 Father 객체
var Father = function(name, age){
Parent.apply(this, arguments);
this.name = name;
this.age = age;
}
Father.prototype = Object.create(Parent.prototype);
Father.prototype.constructor = Father;
Father.prototype.introduce = function(){
console.log(`${this.name} ${this.age}세`);
}
var father = new Father('신짱구',4);
console.log(
Parent.isParent(father)
);
father.introduce();
father.myName();
상속을 표현하기 위한 코드가 가독성이 떨어집니다.
그리고 같은 블록안에 있지 않기 때문에 Parent
의 코드 시작과 끝을 알아보기가 힘듭니다.
클래스 문법 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
static isParent(parent){
return parent instanceof Parent;
}
myName(){
console.log(`my name is ${this.name}`);
}
}
class Father extends Parent{
constructor(name,age){
super(name,age);
this.name = name;
this.age = age;
}
introduce(){
console.log(`${this.name} ${this.age}세`);
}
}
const father = new Father('신짱구',4);
console.log(
Parent.isParent(father)
);
father.introduce();
father.myName();
Parent
의 코드가 하나의 블록 안에 정리되어 가독성이 향상되었습니다.
그리고 class
문법을 통해 프로토타입 문법을 모르더라도 이해하기가 쉽습니다.
그리고 무엇보다도 상속 관계를 표현하는 것이 직관적이게 되었습니다.
했갈리기 쉬운 부분은 class
문법이 프로토타입 문법을 대체하는 것은 아니라는 점 입니다. class
문법을 사용해도 내부적으로는 프로토타입 문법으로 동작하고 있습니다.
프로미스
콜백지옥의 해결책입니다.
반복되는 콜백으로 인해 가독성이 떨어지는 코드를 개선할 수 있습니다.
짧은 코드에서 콜백 함수 사용은 코드를 단순하게 만들어 가독성을 높일 수 있습니다. 하지만 대부분의 경우 프로미스를 사용하는 것이 보다 효과적입니다. 프로미스는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. then()
, catch()
, finally()
구문으로 응답을 처리할 수 있습니다.
- then : resolve 상태의 콜백을 추가합니다.
- catch : rejected 상태의 콜백을 추가합니다.
- finally : 응답 결과에 상관없이 항상 수행될 콜백을 처리합니다.
또한 여기서 then()
은 이전의 then()
에서의 반환 값을 사용하여 다시 then()
으로 콜백을 처리할 수 있는데 이를 Promise Chaining 이라고 합니다
프로미스 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Operation successful");
}, 1000);
});
promise
.then((result) => {
console.log(result); // "Operation successful"
return "Promise Chaining" // 다음 then에게 전달할 값
})
.then((result)=>{ // 프로미스 체이닝
console.log(result); // "Promise Chaining"
})
.catch((error) => {
console.error(error);
})
.finally(() => {
console.log("Operation completed"); // 항상 실행됨
});
async/await
프로미스를 기반으로 동작하며 프로미스를 좀 더 편하게 사용할 수 있는 문법입니다. 그렇기 때문에 async
함수는 항상 promise 를 반환합니다.
async
는 함수 앞에 선언합니다. 그리고 await
는 async
를 선언한 함수 내에서만 선언할 수 있습니다.
async/await 문법은 코드를 마치 동기로 동작하는 것처럼 보이게 하여 가독성을 향상 시킵니다.
async/await 예제 코드
1
2
3
4
5
6
7
async function test(param){
const promise = new Promise((resolve,reject)=>setTimeout(()=>resolve(param),1000)); // 1초 후에 파라미터를 resolve
const temp = await promise;
console.log(temp);
}
test("hello!");
비동기 처리지만 콜백을 따로 작성하지 않고 일반적인 동기적 방식으로 동작하는 코드의 작성과 동일한 구성입니다. 일반적인 비동기 처리였다면 console.log에서 에러가 발생할 확률이 높았을 것입니다.
만약 Promise가 reject되는 경우는 try...catch
를 사용하여 처리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
async function rejectTest(param){
const promise = new Promise((resolve,reject)=>setTimeout(()=>reject(param),3000)); // 3초 후에 파라미터를 reject
try{
const temp = await promise;
console.log(temp);
}catch(err){
console.error(err);
}
}
rejectTest("fail :(")
reject
된 에러 데이터는 catch
에서 처리됩니다. 이처럼 단순한 Promise가 아닌 복잡한 비동기 처리의 경우 코드가 줄고 가독성이 향상됩니다.
하지만 try...catch
를 사용하여 처리를 하기 때문에 예외 핸들링이 익숙치 않다면 이 부분에서 고민이 많이 될 수 있는 부분입니다.
⚠️ 하나의 try...catch
안에는 하나의 await가 들어가야 합니다.
for await of
async/await
문의 확장입니다. 이터러블 객체를 순환합니다. await
를 사용하기 때문에 반드시 async
함수 내에서 선언해야합니다. for...of
문과 형식은 유사하지만 for await of
는 비동기 이터러블 까지 처리가 가능합니다.
for await of 예제
1
2
3
4
5
6
7
8
9
10
11
const promise1 = new Promise((resolve,reject)=>setTimeout(()=>resolve("1번 종료"),1000)); // 1초 후에 파라미터를 resolve
const promise2 = new Promise((resolve,reject)=>setTimeout(()=>resolve("2번 종료"),3000)); // 1초 후에 파라미터를 resolve
const promiseArr = [promise1, promise2];
async function foo() {
for await ( promise of promiseArr){
console.log(promise);
}
}
foo();
REPL을 통해 수행시키면 비동기 작업이 순차적으로 동작합니다. 병렬로 한꺼번에 실행하지 않습니다.
비동기 작업을 병렬로 한꺼번에 수행시킨다고 했갈릴 수 있습니다. 다수의 프로미스를 병렬로 동작시키고 싶을땐 Promise.all()
을 사용합니다.
Map/Set
ES6 전에는 자바스크립트에서 Map, Set 을 객체와 리스트로 대체하여 구현했습니다. 하지만 이후 Map, Set 이 나오면서 보다 효율적으로 코드를 작성할 수 있게 되었습니다.
Map
Map은 데이터를 키-값 형태로 가질 수 있는 자료구조입니다. 키는 중복될 수 없으며 이미 존재하는 키에 값을 할당할 경우 새로운 값이 저장됩니다.
Map 사용법
1
2
3
const map = new Map();
map.set("key","value");
Set
Set은 배열과 비슷한 구조를 가지는 자료구조입니다. 중복된 값을 허용하지 않는다는 특징이 있습니다.
Set 사용법
1
2
3
const set = new Set();
set.add(1);
널 병합/옵셔널 체이닝
생각해보면 ES6에서 추가된 기능 중에 제가 가장 자주 쓰고 있는 기능인 것 같네요 🤔
널 병합
null
, undefined
인지를 확인하는 병합 연산자 입니다. 일일이 null 체크 할 필요 없이 간단한 문법으로 손쉽게 처리할 수 있습니다. ||
구문과 유사한 기능을 합니다. 하지만 조금 다릅니다.
||
에서는 0
, ""
, Nan
, null
, undefined
를 falsy
한 값으로 보고 처리합니다. 반면 ??
은 null
, undefined
만을 처리합니다.
널 병합 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const a = 0;
const b = null;
const c = undefined;
const d = '';
const e = NaN;
const foo = a || 10; // foo=10;
console.log("0 || 10 => "+foo);
const bar = a ?? 100; // bar=100;
console.log("0 ?? 100 => "+bar);
const baz = b ?? 1; // baz=1;
console.log("null ?? 1 => "+baz);
const foobar = c ?? 7; // foobar=7;
console.log("undefined ?? 7 => "+foobar);
const qux = d ?? 100; // qux='';
console.log("'' ?? 100 => "+qux);
const corge = d || 100; // corge=100;
console.log("'' || 100 => "+corge)
const waldo = e ?? 5; // waldo=NaN;
console.log("NaN ?? 5 => "+waldo);
const thud = e || 5; // thud=5;
console.log("NaN || 5 => "+thud);
로그 결과
1
2
3
4
5
6
7
8
0 || 10 => 10
0 ?? 100 => 0
null ?? 1 => 1
undefined ?? 7 => 7
'' ?? 100 =>
'' || 100 => 100
NaN ?? 5 => NaN
NaN || 5 => 5
??
연산자는 null
, undefined
의 경우에만 연산 처리가 되는 것을 확인할 수 있습니다.
옵셔널 체이닝
nullish한 데이터의 속성을 조회할 때 에러 발생을 막습니다. 이 경우 에러를 발생시키지 않고 undefined
를 리턴합니다.
?.
구문을 통해 사용할 수 있습니다. 일반적으로 데이터의 속성에 접근할 때 데이터.속성
으로 .
을 통해 접근합니다. 이 구문을 ?.
으로 대체하여 사용 합니다.
아래와 같이 사용할 수 있습니다.
1
2
3
4
obj?.name;
obj?.[age];
arr?.[i];
func?.();
속성 명, 특정 인덱스, 함수 실행 등에서도 사용할 수 있습니다. 함수의 경우 이름에 맞는 함수가 없다면 undefined
를 반환합니다. 만약 같은 이름의 변수는 있지만 함수가 없는 경우에도 undefined
를 반환합니다.
옵셔널 체이닝을 통해 undefined
가 반환 되는 경우에 널 병합을 사용해 처리하면 코드가 깔끔합니다.