ES2015+
- ES6 – 역사적인 변화
- const, let
- 템플릿 문자열
- 객체 리터럴
- Arrow Function
- 구조 분해 할당
- Class 추가
- Promise 기반
- Async/Await
- Map/Set
- Null 병합/Optional chaining) 연산자
1. ES6 – 역사적인 변화
- 2015년 자바스크립트 문법에 매우 큰 변화가 있었다.
- 2015년을 기점으로 매년 분법 변경 사항이 발표되고 있다.
- 최신 버전은 ES2023.
인터넷 익스플로러와 같은 구형 브라우저에서 최신 문법을 사용할 수 없지만, 요즘에는 바벨(babel)처럼 구형 브라우저에
맞게 문법을 변환해주는 도구가 널리 쓰이므로 큰 문제가 되지 않는다.
2. const, let
보통 자바스크립트를 배울 때는 var로 변수를 선언하는 방법부터 배운다.
하지만 var은 이제 const와 let이 대체 한다.
const와 let이 공통적으로 갖는 특징인 블록 스코프(범위)
// 정상 출력되는 코드
if(true) {
var x = 3;
}
console.log(x); //3
// 에러가 발생하는 코드
if(true) {
const y = 3;
}
console.log(y);
x는 정상적으로 출련된다. 하지만 y는 에러가 발생하는데 var을 const로 바꿧을 뿐인데 차이가 생겼다. var은 함수 스코프를 가지므로 if문의 블록과 관계없이 접근할 수 있다.
하지만 const와 let은 블록 스코프를 가지므로 블록 밖에서는 변수에 접근할 수 없다.
블록의 범위
- if
- while
- for
- function
등등에서 볼 수 있는 중괄호 { 와 } 사이 이다.
함수 스코프 대신 블록 스코프를 사용함으로써 호이스팅 같은 문제도 해결되고 코드 관리도 수월해졌다.
const, let과 var의 차이
- const는 한번 값을 할당하면 다른 값을 할당할 수 없다.
- 다른 값을 할당하려고 하면 에러가 발생한다.
- 초기화할 때 값을 할당하지 않으면 에러가 발생한다.
- const로 선언한 변수를 상수라고 부른다.
const a = 0;
a = 1; // Type Error : Assignment to constant variable
let b = 0;
b = 1; // 1
💡 const와 let 중에서 어느 것을 써야 할까? 자바스크립트를 사용할 때 한 번 초기화했던 변수에 다른 값을 할당하는 경우는 의외로 적다. 따라서 변수 선언 시에는 기본적으로 const를 사용하고, 다른 값을 할당해야 하는 상황이 생겼을 때 let을 사용하면 된다.
3. 템플릿 문자열
새로운 문자열이 생겼다. 이 문자열은 큰따옴표나, 작음따옴표로 감싸는 기존 문자열과 달리 백틱(`)으로 감싼다. 특이한 점은 문자열 안에 변수를 넣을 수 있다는 것이다.
// 기존 ES 문법
var num1 = 1;
var num2 = 2;
var result = 3;
var string1 = num1 + ' 더하기 ' + num2 + '는 \\'' + result + '\\'';
console.log(string1) // 1 더하기 2는 '3'
문자열 string1은 띄어쓰기와 변수, 더하기 기호 때문에 가독성이 좋지 않다.
또한, 작은 따옴표를 이스케이프하느라 코드가 지저분하다.
// 바뀐 문법
const num3 = 1;
const num4 = 2;
const result = 3;
const string2 = `${num3} 더하기 ${num4}는 ' ${result2}'`
console.log(string1) // 1 더하기 2는 '3'
${변수} 형식으로 변수를 더하기 기호 없이 문자 열에 넣을 수 있다.
기존 따옴표 대신 백틱을 사용하므로 큰따옴표나 작은따옴표와 함께 사용할 수도 있습니다.
4. 객체 리터럴
객체 리터럴에 편리한 기능들이 추가되었다.
// oldObject 객체에 동적으로 속성을 추가하는 예제
// ES5 이전
var sayNode = function() {
console.log('Node');
}
var es = 'ES';
var oldObject = {
sayJS: function() {
console.log('JS');
},
sayNode: sayNode,
};
oldObject[es+6] = 'Fatastic';
oldObject.sayNode(); // Node
oldObject.sayJs(); // JS
console.log(oldObject.ES6) // Fantastic
// ES6 이후
const newObject = {
sayJS() {
console.log('JS');
},
sayNode,
[es + 6]: 'Fantastic'
}
newObject.sayNode(); // Node
newObejct.sayJS(); // JS
console.log(newObject.ES6) // Fantastic
- sayJS 같은 객체의 메서드에 함수를 연결할 때 더는 콜론(:)과 function을 붙이지 않아도 된다.
- sayNode: sayNode처럼 속성명과 변수명이 동일한 경우에는 한 번만 써도 되게 바뀌었다. 이때 코드의 중복을 피할 수 있어 편리하다.
객체의 속성명은 동적으로 생성할 수 있다. 예전 문법에서는 ES6 속성명을 만들려면 객체 리터럴 바깥에서 생성해야 했지만, ES2015 문법에서는 객체 리터럴 안에 동적 속성을 선언해도 된다.
5. Arrow Function (화살표 함수)
화살표 함수라는 새로운 함수가 추가되었으며, 기존 function() {}도 그대로 사용할 수 있다.
function add1(x,y) {
return x + y;
}
const add2 = (x, y) => {
return x + y;
}
const add3 = (x, y) => x + y;
const add4 = (x,y) => (x+y);
function not1(x) {
return !x;
}
const not2 = x => !x;
- add1, add2, add3, add4 모두 같은 기능을 하는 함수 이다. 마찬가지로 not1, not2도 같은 기능을 한다.
- 화살표 함수는 function ⇒ 기호로 사용한다.
- 변수에 대입하면 나중에 재사용할 수 있다.
- 내부에 return문 밖에 없는 경우, return문을 줄일 수 있다. 중괄호 대신 add3과 add4처럼 return할 식을 바로 적으면 된다. 또는 add4처럼 보기 좋게 소괄호로 감쌀 수도 있다. not2처럼 매개변수가 한 개이면 매개변수를 소괄호로 묶어주지 않아도 된다.
- return문을 줄이는 문법은 자주 사용하므로 눈여겨 보자.
기존 Function과 다른점은 무엇일까? this 바인드 방식
var relationship1 = {
name: 'zero',
friends: ['nero', 'hero', 'xero'],
logFriends: function () {
var that = this; // relationship1을 가리키는 this를 that에 저장 this.friends.forEach(function (friend) {
console.log(that.name, friend);
});
}, };
relationship1.logFriends();
const relationship2 = {
name: 'zero',
friends: ['nero', 'hero', 'xero'],
logFriends() {
this.friends.forEach(friend => {
console.log(this.name, friend);
}); },
};
relationship2.logFriends();
- relationship1.logFriends() 안의 forEach문에서는 function 선언문을 사용했는데 각자 다른 함수 스코프의 this를 가지므로 that이라는 변수를 사용해서 relationship1에 간접적으로 접근하고 있다.
- relationship2.logFriends() 안의 forEach문에서는 화살표 함수를 사용했다. 따라서 바깥 스코프인 logFriends()의 this를 그대로 사용할 수 있다. 상위 스코프의 this를 그대로 물려받은것이다.
기본적으로 화살표 함수를 쓰되, this를 사용해야 하는 경우에는 화살표 함수와 함수 선언문(function) 둘 중 하나를 고르면 된다.
6. 구조 분해 할당
구조 분해 할당을 사용하면 객체와 배열로부터 속성이나 요소를 쉽게 꺼낼 수 있다.
구조분해 할당 이전
var candyMachine = {
status: {
name: 'node',
count: 5, },
getCandy: function () {
this.status.count--;
return this.status.count;
}, };
var getCandy = candyMachine.getCandy;
var count = candyMachine.status.count;
구조 분해 할당 이후
const candyMachine = {
status: {
name: 'node',
count: 5, },
getCandy() {
this.status.count--;
return this.status.count;
}, };
const { getCandy, status: { count } } = candyMachine;
- 해당 문번은 유효한 문법으로 candyMachine 객체 안의 속성을 찾아서 변수와 매칭한다.
- count 처럼 여러 뎁스안에 있는 속성도 찾을 수 있다. 결과적으로 getCandy와 count 변수가 초기화 된것이다.
- 구조 분해 할당을 사용하면 함수의 this가 달라질 수 있다. 달라진 this를 원래대로 바꿔주려면 bind 함수를 따로 사용해야한다.
배열 구조분해 할당
// 할당 이전
var array = [‘nodejs’, {}, 10, true];
var node = array[0];
var obj = array[1];
var bool = array[3];
// 할당 이후
const array = [‘nodejs’, {}, 10, true];
const [node, obj, , bool] = array;
- 어색하거나 좀 잘못된거 같지만 나름대로 규칙이 있다.
- node, obj와 bool의 위치를 보면 배열의 첫 번째 요소, obj는 두 번째 요소, bool은 네번 째 요소라는 것을 알 수 있다. obj와 bool의 사이의 요소인 10에는 변수명을 지어주지 않았으므로 10은 무시하도록 처리된다.
7. Class의 추가
- 다른 언어 처럼 클래스 기반으로 동작하는 것이 아니라 여전히 프로토타입 기반으로 동작한다. 프로토타입 기반 문법을 보기 좋게 클래스로 바꾼 것으로 이해하면 된다.
프로토타입 상속 예제 ( 클래스 이전 문법
// 클래스 이전 문법
var Human = function(type) {
this.type = type || 'human';
};
Human.isHuman = function(human) {
return human instanceof Human;
}
Human.prototype.breathe = function() {
alert('h-a-a-a-m');
};
var Zero = function(type, firstName, lastName) {
Human.apply(this, arguments);
this.firstName = firstName;
this.lastName = lastName;
};
Zero.prototype = Object.create(Human.prototype); Zero.prototype.constructor = Zero; // 상속하는 부분 Zero.prototype.sayName = function() {
alert(this.firstName + ' ' + this.lastName);
};
var oldZero = new Zero('human', 'Zero', 'Cho');
Human.isHuman(oldZero); // true
- Human 생성자 함수가 있고, 그 함수를 Zero 생성자 함수가 상속한다. Zero 생성자 함수를 보면 상속 받기 위한 코드가 상당히 난해한걸 볼 수 있다.
- Human.apply와 Object.create 부분이 상속받는 부분이다.
Class 기반 코드로 변경
class Human {
constructor(type = 'human') {
this.type = type;
}
static isHuman(human) {
return human instanceof Human;
}
breathe() {
alert('h-a-a-a-m');
}
}
class Zero extends Human {
constructor(type, firstName, lastName) {
super(type);
this.firstName = firstName;
this.lastName = lastName;
}
sayName() {
super.breathe();
alert(`${this.firstName} ${this.lastName}`);
}
}
const newZero = new Zero('human', 'Zero', 'Cho');
Human.isHuman(newZero); // true
- 전반적으로 코드가 class 안으로 그룹화 되어있다.
- 생성자 함수는 constructor로 사용한다.
- Human.isHuman 같은 클래스 함수는 static 키워드로 전환되었다.
- 프로토 타입 함수들도 모두 class 블록 안에 포함돼서 어떤 함수가 어떤 클래스 소속인지 확인하기 쉽다.
- 상속도 간단해져서 extends 키워드로 쉽게 상속할 수 있다.
하지만 클래스 문법으로 바뀌었더라도 본질은 프로토타입 기반으로 작동한다는걸 잊으며 안된다.
8. Promise 기반
자바스크립트와 노드에서는 주로 비동기를 접한다. 특히 이벤스 리스너를 사용할 때 콜백 함수를 자주 사용한다. ES2015부터는 자바스크립트와 노드의 API들이 콜백 대신 프로미스(Promise) 기반으로 재구성되며, 악명 높은 콜백 지옥(CallBack Hell) 현상을 극복했다라고 평가 받고 있다.
Promise는 반드시 알아둬야할 객체뿐만 아니라 다른 자료를 참고해서라도 꼭 숙지해야 한다.
프로미스의 규칙
- 프로미스 객체 생성
const condition = true; // true이면 resolve, false이면 reject
const promise = new Promise((resolve, reject) => {
if (condition) {
resolve('성공');
}else{
reject('실패');
}
});
// 다른 코드가 들어갈 수 있음
promise
.then((message) => {
console.log(message); // 성공(resolve)한 경우 실행
})
.catch((error) => {
console.error(error); // 실패(reject)한 경우 실행
})
.finally(() => { // 끝나고 무조건 실행
console.log('무조건');
});
new Promise로 프로미스를 생성할 수 있으며, 안에 resolve와 reject를 매개변수로 갖은 콜백 함수를 넣는다. 이렇게 생성하면 promise 변수에 then과 catch 메서드를 붙일 수 있다. Promise 내부에서 resolve가 호출되면 then이 실행되고, reject가 호출되면 catch가 실행된다. finally 부분은 성공/실패 여부와 상관없이 실행된다.
- resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.
- then은 message가 성공, catch는 error가 실패로 처리된다.
그래서 프로미스가 뭔데?
프로미스를 쉽게 설명하면, 실행은 바로 하되 결과로 나온 값은 나중에 받는 객체다. 결과값은 실행이 완료된 후 then이나 catch 메서드를 통해 받는다. 예제에 new Promise와 promise.then 사이에 다른 코드가 들어갈 수도 있다. 그 결과 new Promise는 바로 실행되지만, 결과값은 then을 붙였을 때 받게 된다.
- then이나 catch에서 다시 다른 then이나 catch를 붙일 수 있는데, 이전 then의 return 값을 다음 then의 매개변수로 넘긴다. return한 경우 프로미스가 수행된 후 다음 then이나 catch가 호출된다.
promise
.then((message) => {
return new Promise((resolve, reject) => {
resolve(message);
});
})
.then((message2) => {
console.log(message2);
return new Promise((resolve, reject) => {
resolve(message2);
});
})
.then((message3) => {
console.log(message3);
})
.catch((error) => {
console.error(error);
});
- 처음 then에서 message를 resolve하면 다음 then에서 message2로 받을 수 있다. 여기서 다시 message2를 resolve한 것을 다음 then에서 message3로 받았다.
- 단, then에서 new Promise를 return해야 다음 then에서 받을 수 있다는 것을 기억해야한다.
- 이것을 활용해서 콜백을 프로미스로 바꿀 수 있다.
콜백을 쓰는 패턴중 하나의 방법 ( 프로미스로 변경해보기
// 3중 콜백으로 프로미스로 바꾸기 전
function findAndSaveUser(Users) {
Users.findOne({}, (err, user) => { // 첫 번째 콜백
if (err) {
return console.error(err);
}
user.name = 'zero';
user.save((err) => { // 두 번째 콜백
if (err) {
return console.error(err);
}
Users.findOne({ gender: 'm' }, (err, user) => { // 세 번째 콜백
// 생략
});
});
});
}
- 콜백 함수가 세 번 중첩되어 콜백 함수가 나올 때마다 코드의 깊이가 깊어져서 각 콜백 함수마다 에러도 따로 처리해줘야 한다.
변경 후 코드
function findAndSaveUser(Users) {
Users.findOne({})
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne({ gender: 'm' });
})
.then((user) => {
// 생략
})
.catch(err => {
console.error(err);
});
}
- 코드의 깊이가 세 단계 이상 깊어지지 않는다.
- then 메서드들이 순차적으로 실행된다.
- 콜백에서 매번 따로 처리해야 했던 에러도 마지막 catch에서 한 번에 처리할 수 있다.
모든 콜백 함수를 위와 같이 바꿀 수 없다. 메서드가 프로미스 방식을 지원해야 한다.
Promise 한번에 실행하기
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
.then((result) => {
console.log(result); // ['성공1', '성공2'];
})
.catch((error) => {
console.error(error);
});
- Promise.all에 넣으면 모두 resolve가 될 때까지 즉, 실행이 완료될 때 까지 기다렸다가 then으로 넘어간다.
- 결과값은 배열로 각각 들어가 있다.
- 하나라도 reject가 되면 catch로 넘어간다.
Promise reject 체크 하기
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.reject('실패2');
const promise3 = Promise.resolve('성공3');
Promise.allSettled([promise1, promise2, promise3])
.then((result) => {
console.log(result);
/*
{ status: 'fulfilled', value: '성공1' },
{ status: 'rejected', reason: '실패2' },
{ status: 'fulfilled', value: '성공3' }
*/
})
.catch((error) => {
console.error(error);
});
}
- Promise.allSettled를 사용하면 결과값이 좀 더 자세해져서 어떤 프로미스가 reject되었는지 status를 통해 알 수 있습니다.
- 실패 이유는 reason에 들어있다. 따라서 Promise.all 대신 Promise.allSettled를 사용하는 것을 좀 더 권장한다.
Node 16 버전부터는 reject된 Promise에 catch를 달지 않으면
unhandledPromiseRejection 에러가 발생한다. 에러가 발생하면 다음 코드가 실행되지 않으니 반드시 프로미스에 catch 메서드를 붙이는걸 권장한다.
try {
Promise.reject('에러');
} catch (e) {
console.error(e);
// UnhandledPromiseRejection: This error originated either
// by throwing inside...
}
Promise.reject('에러').catch(() => {
// catch 메서드를 붙이면 에러가 발생하지 않음
})
9. async/await
노드7.6 버전부터 지원되는 기능으로, ES2017에 추가되었다. 노드처럼 비동기 위주로 프로그래밍을 해야 할 때 도움이 많이 된다. 프로미스가 콜백 지옥을 해결했지만, 여전히 코드는 장황하다. then과 catch가 계속 반복되기 때문이다. async/await 문법은 프로미스를 사용한 코드를 한 번 더 깔끔하게 줄여준다.
async/await 사용 전
function findAndSaveUser(Users) {
Users.findOne({}, (err, user) => { // 첫 번째 콜백
if (err) {
return console.error(err);
}
user.name = 'zero';
user.save((err) => { // 두 번째 콜백
if (err) {
return console.error(err);
}
Users.findOne({ gender: 'm' }, (err, user) => { // 세 번째 콜백
// 생략
});
});
});
}
async/await 사용 후
async function findAndSaveUser(Users) {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' });
// 생략
}
함수 선언부를 일반 함수 대신 async function으로 교체한 후, 프로미스 앞에 await을 붙였다. 함수는 해당 프로미스가 resolve될 때까지 기다린 뒤 다음 로직으로 넘어간다.
- await User.findOne({})이 resolve될 떄까지 기다린 다음 user 변수를 초기화 시킨다.
async/await Reject 추가
async function findAndSaveUser(Users) {
try {
let user = await Users.findOne({}); user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' }); // 생략
} catch (error) {
console.error(error);
}
}
Arrow Function에서 async/await 사용하기
const findAndSaveUser = async (Users) => {
try {
let user = await Users.findOne({}); user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' }); // 생략
} catch (error) {
console.error(error);
}
};
for문과 async/await 같이 사용해 순차적으로 실행하기
for문과 함께 쓰는 것은 노드 10 버전부터 지원하는 ES2018 문법이다.
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
(async () => {
for await (promise of [promise1, promise2]) {
console.log(promise);
}
})();
- for await of 문을 사용해서 프로미스 배열을 순회하는 모습이다. async 함수의 반환값은 항상 Promise로 감싸진다. 따라서 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여서 처리할 수 있다.
async function findAndSaveUser(Users) {
// 생략
}
findAndSaveUser().then(() => { /* 생략 */ });
// 또는
async function other() {
const result = await findAndSaveUser();
}
- 중첩되는 콜백 함수가 있다면 프로미스를 거쳐 async/await 문법으로 바꾸도록 하자. 그리고 적극 활용하는 것이 좋다.
10. Map/Set
- ES2015에는 새로운 자료구조들이 추가되었다. 그중 자주 쓰이는 것은 Map과 Set인데, Map은 객체와 유사하고 Set은 배열과 유사하다.
Map 사용해보기
const m = new Map();
m.set('a', 'b'); // set(키, 값)으로 Map에 속성 추가
m.set(3, 'c'); // 문자열이 아닌 값을 키로 사용 가능합니다 const d = {};
m.set(d, 'e'); // 객체도 됩니다
m.get(d); // get(키)로 속성값 조회
console.log(m.get(d)); // e
m.size; // size로 속성 개수 조회
console.log(m.size) // 3
for (const [k, v] of m) { // 반복문에 바로 넣어 사용 가능합니다
console.log(k, v); // 'a', 'b', 3, 'c', {}, 'e'
} // 속성 간의 순서도 보장됩니다
m.forEach((v, k) => { // forEach도 사용 가능합니다
console.log(k, v); // 결과는 위와 동일
});
m.has(d); // has(키)로 속성 존재 여부를 확인합니다
console.log(m.has(d)); // true
m.delete(d); // delete(키)로 속성을 삭제합니다 m.clear(); // clear()로 전부 제거합니다 console.log(m.size); // 0
- Map은 속성들 간의 순서를 보장하고 반복문을 사용할 수 있다. 속성 명으로 문자열이 아닌 값도 사용할 수 있고, size 메서드를 통해 속성의 수를 쉽게 알 수 있다는 점에서 일반 객체와 다르다.
Set 사용해보기
const s = new Set();
s.add(false); // add(요소)로 Set에 추가합니다
s.add(1);
s.add('1');
s.add(1); // 중복이므로 무시됩니다
s.add(2);
console.log(s.size); // 중복이 제거되어 4
s.has(1); // has(요소)로 요소 존재 여부를 확인합니다
console.log(s.has(1)); // true
for (const a of s) {
console.log(a); // false 1 '1' 2
}
s.forEach((a) => {
console.log(a); // false 1 '1' 2
})
s.delete(2); // delete(요소)로 요소를 제거합니다
s.clear(); // clear()로 전부 제거합니다
- Set은 중복을 허용하지 않는다는 것이 가장 큰 특징이다. 따라서 배열 자료구조를 사용하고 싶으나 중복은 허용하고 싶지 않을때 Set을 대신 사용하면 된다.
- 기존 배열에서 중복을 제거할 때도 사용한다.
Set으로 기존 배열 중복 제거하기
const arr=[1,3,2,7,2,6,3,5];
const s = new Set(arr);
const result = Array.from(s);
console.log(result); // 1, 3, 2, 7, , 5
- new Set(배열) 하는 순간 배열의 중복딘 요소들이 제거된다.
- Set을 배열로 되돌리려면 Array.from(Set)을 하면 된다.
11. Null 병합/Optional chaining) 연산자
ES2020에서 추가된
- ??(널 병합 연산자)
- 널 병합 연산자는 주로 || 연산자 대용으로 사용되며, false 값 (0, ‘’, false, NaN, null, undefined)중 null과 undifined만 따로 구분한다.
const a = 0;
const b = a || 3; // || 연산자는 falsy 값이면 뒤로 넘어감
console.log(b); // 3
const c = 0;
const d = c ?? 3; // ?? 연산자는 null과 undefined일 때만 뒤로 넘어감
console.log(d); // 0;
const e = null;
const f = e ?? 3;
console.log(f); // 3;
const g = undefined;
const h = g ?? 3;
console.log(h); // 3;
- ?.(옵셔널 체이닝 연산자)
- 옵셔널 체이닝 연산자는 null이나 undefined의 속성을 조회하는 경우 에러가 발생하는 것을 막는다.
const a={}
a.b; // a가 객체이므로 문제없음
const c = null;
try {
c.d;
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null (reading 'd')
}
c?.d; // 문제없음
try { c.f();
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null (reading 'f')
}
c?.f(); // 문제없음
try { c[0];
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null (reading '0')
}
c?.[0]; // 문제없음
- 위 코드처럼 일반적은 속성뿐만 아니라 함수 호출이나 배열 요소 접근에 대해서도 에러가 발생하는 것을 방지할 수 있다.
- c?.d와 c?.f(), c?.[0]의 값은 undefined가 된다는 것을 알아둬야 한다.
- 옵셔널 체이닝 연산자는 자바스크립트 프로그래밍을 할 때 발생하는 TypeError : Cannot read properties of undefined null 에러의 발생 빈도륵 획기적으로 낮출 수 있기 때문에 자주 사용한다.