ES2015+ (ES6+) 기본
이 포스트는 ES2015(ES6) 이후 적용된 기본적인 새로운 문법에 대해 간략히 기술한다.
소스는 assu10/nodejs.git 에 있습니다.
1. ES2015+ (ES6+, ESNext) 신규 문법
1.1. const, let
if (true) {
var x = 1;
}
console.log(x);
if (true) {
const y = 2;
}
console.log(y); // ReferenceError: y is not defined
var
: 함수 스코프, if 문 등의 블록과 관계없이 접근 가능const
: 블록 스코프, 블록 밖에서는 변수에 접근 불가, 한 번 값을 할당하면 다른 값 할당 불가 (상수)let
: 블록 스코프, 블록 밖에서는 변수에 접근 불가
1.2. 템플릿 문자열
기존 js 는 문자열을 “ (큰따옴표) 나 ‘ (작은따옴표) 로 감싸는 데에 비해 템플릿 문자열은 ` (백틱) 으로 감쌀 수 있다.
아래 코드를 보면 ${변수}
의 형태로 선언을 하고, escape 처리없이 큰따옴표와 작은따옴표를 내부에 사용할 수 있어 훨씬 높은 가독성을 보여준다.
// AS-IS
var a1 = 1;
var b1 = 2;
var rst1 = a1 + b1;
var str1 = a1 + ' 더하기 ' + b1 + '은 \'' + rst1 + '\'' + ' 이다.';
console.log(str1);
// TO-DO
const a2 = 1;
const b2 = 2;
const rst2 = a2 + b2;
const str2 = `${a2} 더하기 ${b2}는 '${rst2}' 이다.`;
console.log(str2);
1.3. 객체 리터럴
// AS-IS
var oldFunc = function() {
console.log('Old Function');
}
var oldEs = 'ES';
var oldObj = {
sayJs : function() {
console.log('JS');
},
oldFunc: oldFunc
};
oldObj[oldEs+6] = 'Wow';
oldObj.oldFunc(); // Old Function
oldObj.sayJs(); // JS
console.log(oldObj.ES6); // Wow
// TO-DO
const newFunc = function() {
console.log('New Function');
}
const newEs = 'ES';
const newObj = {
sayJs() { // sayJs 객체의 메서드에 함수 연결 시 콜론과 function를 붙이지 않음
console.log('JS');
},
newFunc, // newFunc: newFunc 처럼 속명명과 변수명이 동일하면 한 번만 써도 됨
[newEs+6]: 'Wow' // 객체의 속성명을 동적으로 생성
};
newObj.newFunc(); // New Function
newObj.sayJs(); // JS
console.log(newObj.ES6); // Wow
속성명과 변수명이 동일한 경우 한 번만 써도 되도록 변경되었는데 코드의 중복을 피할 수 있어 매우 편리하다.
{ a: a, b: b} // ES5
{ a, b } // ES6
1.4. Arrow Function (화살표 함수)
기존의 함수 선언문 function () { }
를 사용할 수도 있지만 화살표 함수를 사용하면 코드의 양이 줄어들게 된다.
// AS-IS
function old1(a, b) {
return a+b;
}
// TO-BE
const new1 = (a, b) => { // function 선언 대신 => 기호로 함수 선언
return a+b;
}
const new2 = (a, b) => a+b; // 함수 내부에 return 문밖에 없는 경우 return 생략 가능
const new3 = (a, b) => (a+b);
// AS-IS
function old4(a) {
return !a;
}
// TO-BE
const new4 = a => !a; // 매개변수가 하나만 있으면 매개변수 소괄호 생략 가능
기존의 함수 선언문 (function () { }
) 과 다른 점은 this
바인딩 방식이다.
// AS-IS
var oldBinding = {
name : 'old name',
arr : ['oldArr1', 'oldArr2'],
printArr: function() {
var that = this; // oldBinding 을 가리키는 this 를 that 에 할당
this.arr.forEach(function (v) {
console.log(that.name, v);
});
}
};
oldBinding.printArr(); // old name oldArr1, old name oldArr2
// TO-BE
const newBinding = {
name : 'new name',
arr : ['newArr1', 'newArr2'],
printArr() {
this.arr.forEach(v => { // 화살표 함수를 사용했기 때문에 상위 스코프의 this 를 그대로 사용
console.log(this.name, v);
});
}
};
newBinding.printArr(); // new name newArr1, new name newArr2
기본적으로 화살표 함수(=>
)를 쓰되 this 를 사용하는 경우 그 스코프에 따라 화살표 함수(=>
) 와 함수 선언문 (function () { }
) 중 알맞는 것을 사용한다.
1.5. 구조분해 할당 (중요)
구조분해 할당 사용 시 객체/배열에서 속성이나 요소를 쉽게 꺼낼 수 있다.
객체에 대한 구조분해 할당 (객체의 속성을 같은 이름의 변수에 대입)
https://ideone.com/7IiR3i
// AS-IS
let beforeObj = {
status: {
bfName: 'node',
bfCount: 5
},
getBeforeObj: function () {
this.status.bfCount--;
return this.status.bfCount;
}
};
let getBeforeObj = beforeObj.getBeforeObj;
let bfCount = beforeObj.status.bfCount;
// TO-BE : 객체의 속성을 같은 이름의 변수에 대입
let afterObj = {
status: {
afName: 'node',
afCount: 5
},
getAfterObj() {
this.status.afCount--;
return this.status.afCount;
}
};
let { getAfterObj, status: { afCount } } = afterObj;
console.log('getBeforeObj', getBeforeObj);
console.log('bfCount', bfCount); // 5
console.log('getAfterObj', getAfterObj);
console.log('afCount', afCount); // 5
let person = {name: 'assu', age: 20};
let {name, age} = person; // name='assu', age=20
배열에 대한 구조분해 할당
https://ideone.com/20lJyN
// AS-IS
let bfArr = ['node.js', {}, 10, true];
let bfNode = bfArr[0];
let bfObj = bfArr[1];
let bfBool = bfArr[3];
// TO-BE : 배열에 대한 구조분해 할당
let afArr = ['node.js', {}, 10, true];
let [afNode, afObj, , afBool] = afArr; // 1, 2, 4번째 요소를 변수에 대입 / 노드에서 굉장히 자주쓰이는 방식
console.log('bfNode', bfNode);
console.log('bfObj', bfObj);
console.log('bfBool', bfBool);
console.log('afNode', afNode);
console.log('afObj', afObj);
console.log('afBool', afBool);
let array = [1, 2, 3, 4];
let [head, ...rest] = array; // head=1, rest=[2,3,4]
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1
1.6. Class
클래스 문법도 포함되어 있긴 하지만 실제로 클래스 기반으로 동작하는 것이 아닌 여전히 프로토타입 기반으로 동작한다.
다만, 프로토타입 기반 문법을 보기 좋게 클래스로 변경한 것으로 이해하면 된다.
AS-IS
// AS-IS
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
console.log(Human.isHuman(oldZero));
TO-BE
class Human {
constructor(type = 'human') {
this.type = type;
}
static isHuman(human) { // 클래스 함수는 static 으로 전환
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}`);
}
}
let newZero = new Zero('human', 'jh', 'lee');
Human.isHuman(newZero); // true
console.log(Human.isHuman(newZero));
1.7. Promise (프로미스) (중요)
노드에서는 주로 비동기로 통신이 일어나는데 ES2015 부터는 콜백 대신 Promise
기반으로 동작한다.
Promise
는 실행은 바로 하되 결과값은 나중에 받는 객체이다.
겨로가값은 실행이 완료된 후 then
이나 catch
를 통해 받는다.
// Promise 기본 구조
const condition = true; // true 면 resolve, false 면 reject
const promise = new Promise((resolve, reject) => { // 프로미스 생성
if (condition) {
resolve('성공');
} else {
reject('실패');
}
});
// 다른 코드가 들어갈 수 있음 (즉, new Promise 는 바로 실행되지만 결과값은 then 을 붙였을 때 받는다.)
promise
.then((message) => {
console.log(message); // 성공 출력, resolve 한 후에 실행
})
.catch((error) => {
console.log(error); // 실패 출력, reject 한 후에 실행
})
.finally(() => {
console.log('무조건');
});
then 이나 catch 에서 다른 then, catch 를 붙여서 이전 then 의 return 값을 다음 then 의 매개변수로 넘길 수 있다.
// then 이나 catch 에서 다른 then, catch 를 붙여서 이전 then 의 return 값을 다음 then 의 매개변수로 넘길 수 있다.
const condition = true; // true 면 resolve, false 면 reject
const promise = new Promise((resolve, reject) => { // 프로미스 생성
if (condition) {
resolve('성공');
} else {
reject('실패');
}
});
// 다른 코드가 들어갈 수 있음 (즉, new Promise 는 바로 실행되지만 결과값은 then 을 붙였을 때 받는다.)
promise
.then((message) => {
return new Promise(((resolve, reject) => {
resolve(message);
}));
})
.then((message2) => { // message 를 message2 가 받음, 단 then 에서 new Promise 를 return 해야 다음 then 에서 받을 수 있음
console.log('message2', message2);
return new Promise(((resolve, reject) => {
resolve(message2);
}))
})
.then((message3) => {
console.log('message3', message3);
})
.catch((error) => {
console.error('error', error);
});
// 결과
// message2 성공
// message3 성공
위 코드처럼 then 이나 catch 에서 다른 then, catch 를 붙여서 이전 then 의 return 값을 다음 then 의 매개변수로 넘길 수 있는 점을 활용하여 콜백을 Promise 로 바꿀 수 있다.
아래 콜백 함수를 Promise 로 변경해보자.
// 콜백 함수를 프로미스로 변경
// 콜백함수가 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) => { // 세 번째 콜백
// 생략
});
});
});
}
// then 메서드들은 순차적으로 실행되며, 콜백에서 매번 따로 처리해주던 에러도 마지막에 한번에 처리 가능
// 단, 메서드가 프로미스 방식을 지원해야 한다.
function findAndSaveUser(Users) {
Users.findOne({}) // findOne(), save() 가 내부적으로 프로미스 객체를 가지고 있어야 함 (=new Promise 가 내부에 구현되어 있어야 함)
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne( { gender: 'm'});
})
.then((user) => {
// 생략
})
.catch((error) => {
console.error(error);
})
}
프로미스 여러 개를 한 번에 실행할 수도 있다. 기존의 콜백 패턴이라면 콜백을 여러 번 중첩해서 사용해야 하지만 Promise.all
을 사용하여 간단히 실행 가능하다.
// 프로미스 여러개 한꺼번에 사용하기 (Promise.all)
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); // 프로미스 중 하나라도 reject 되면 호출
});
이 외 Promise.resolve
와 Promise.reject
는 각각 즉시 resolve, reject 하는 프로미스를 만든다.
1.8. async / await
async
, await
는 노드 7.6 버전부터 지원되는 기능으로 ES2017 에서 추가되었다.
프로미스가 콜백 지옥을 해결했지만 여전히 then() 과 catch() 가 반복되면서 코드가 장황하다.
이 부분을 async/await
문법을 통해 더 깔끔하게 정리할 수 있다.
// async/await 기본 사용
function findAndSaveUser(Users) {
Users.findOne({}) // findOne(), save() 가 내부적으로 프로미스 객체를 가지고 있어야 함 (=new Promise 가 내부에 구현되어 있어야 함)
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne( { gender: 'm'});
})
.then((user) => {
// 생략
})
.catch((error) => {
console.error(error);
})
}
// 프로미스로 구성되어 있는 위 코드를 async/await 문법을 사용하여 바꿔보자.
async function findAndSaveUser(Users) {
try {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await User.findOne( { gender: 'm' });
// 생략
} catch (error) {
console.error(error);
}
}
위 코드를 보면 함수 선언부를 async function
으로 교체한 후 프로미스 앞에 await
를 붙였다.
이제 함수는 해당 프로미스가 resolve 될 때까지 기다린 후 다음 로직으로 넘어간다.
이번엔 화살표 함수와 함께 async/await
를 사용하여 findAndSaveUser() 를 더 간결하게 개선해보자.
// 화살표 함수와 함께 `async/await` 를 사용
async function findAndSaveUser(Users) {
try {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await User.findOne( { gender: 'm' });
// 생략
} catch (error) {
console.error(error);
}
}
// 위 함수를 화살표 함수와 함께 사용
const findAndSaveUser = async (Users) => {
try {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await User.findOne({ gender: 'm' });
// 생략
} catch (error) {
console.error(error);
}
};
for 문과 async/await 를 같이 사용하여 프로미스를 순차적으로 실행할 수도 있다. (노드 버전 10부터 지원하는 ES2018 문법)
// for 문과 async/await 를 같이 사용하여 프로미스를 순차적으로 실행
const promise1 = Promise.resolve('success 1');
const promise2 = Promise.resolve('success 2');
(async () => {
for await (promise of [promise1, promise2]) {
console.log(promise);
}
})();
// 결과
// success 1
// success 2
1.9. 모듈
변수나 함수, 클래스 등에 export
키워드를 사용해 모듈로 만들고, 다른 곳에서는 import
키워드를 사용해 재사용이 가능하다.
import * as fs from 'fs'
export function writeFile(filepath: string, content: any) {
fs.writeFile(filepath, content, (err) => {
err && console.error('error', err);
});
}
1.10. 생성기 (generator) function*
파이썬이나 PHP 도 yield
라는 키워드를 제공한다.
yield
는 iterator
를 생성할 때 사용하는데 이 iterator
은 iterable
를 통해 얻는다.
이렇게 yield
문을 이용해 iterator
를 만들어내는 iterable
를 생성기(generator)
이라고 한다.
이 생성기는 function 에 *
를 결합하여 function*
과 yield
키워드를 이용해 만들고, 타입스크립트에서 yield
는 반드시 function*
으로 만들어진 함수 내부에서만 사용 가능하다.
function* gen() {
yield* [1,2]; // yield 가 호출되면 일시 정지 후 for 문을 수행한다. for 문이 끝나면 다시 이 부분으로 돌아와 배열 [1,2] 요소를 모두 순회할 때까지 반복
}
for (let value of gen()) {
console.log(value); // 1, 2
}
2. Front-End Javascript
2.1. ajax (asynchronous javascript and xml)
ajax 는 페이지 이동없이 서버에 요청을 보내고 응답을 받는 기술이다.
ajax 요청은 jQuery 나 axios 와 같은 라이브러리를 이용하여 보낸다. 본 포스트에선 브라우저에서 기본적으로 제공하는 XMLHttpRequest 객체가 아닌 axios 사용할 것이다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// axios.get 내부에 new Promise 가 들어있기 때문에 then(), catch() 사용이 가능
axios.get('https://www.zerocho.com/api/get')
.then((result) => {
console.log(result);
console.log(result.data); // {}
console.log(result.status); // 200
})
.catch((error) => {
console.error(error);
});
// 위 함수를 async/await 방식으로 변경
(async () => {
try {
const result = await axios.get('https://www.zerocho.com/api/get')
console.log(result);
console.log(result.data); // {}
console.log(result.status); // 200
} catch(error) {
console.error(error);
}
})();
</script>
axios.get() 이 프로미스이므로 async/await 방식으로 변경할 수 있다.
POST 요청 방식은 아래와 같다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// POST 방식
(async () => {
try {
const result = await axios.post('https://www.zerocho.com/api/post/json', {
name: 'assu',
birth: 1984
});
console.log(result);
} catch (error) {
console.error(error);
}
})();
</script>
2.2. FormData
FormData
html form 데이터를 동적으로 제어하는 기능으로 서버에 폼데이터를 전송할 수 있게 해준다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
const formData = new FormData();
formData.append('name', 'assu');
formData.append('item', 'orange');
formData.append('item', 'melon');
formData.has('item'); // true
formData.has('money'); // false;
formData.get('item'); // orange
formData.getAll('item'); // ['orange', 'melon'];
formData.append('family', ['mom', 'brother']);
formData.get('family'); // mom brother
formData.delete('family');
formData.get('family'); // null
formData.set('item', 'apple');
formData.getAll('item'); // ['apple']
(async () => {
try {
const formData2 = new FormData();
formData2.append('name', 'assu');
formData2.append('birth', 1984);
const result = await axios.post('https://www.zerocho.com/api/post/formdata', formData2);
console.log(result);
console.log(result.data);
} catch (error) {
console.error(error);
}
})();
</script>
2.3. encodeURIComponent, decodeURIComponent
ajax 요청 시 주소에 한글이 들어가는 경우 window 객체의 메서드인 encodeURICompoenent
메서드를 사용한다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
(async () => {
try {
const result = await axios.get(`https://www.zerocho.com/api/search/${encodeURIComponent('노드')}`);
console.log(result);
} catch (error) {
console.error(error);
}
})();
</script>
2.3. 데이터 속성과 dataset
노드를 웹 서버로 사용하는 경우 클라이언트와 데이터를 주고 받을 때 자바스크립트 변수에 저장해도 되지만, 데이터 속성
이라는 데이터는 저장하는 HTML5 공식적인 방법이 있다.
HTML 태그 속성에 data-
로 시작하는 속성을 넣어주면 되는데 이러한 데이터 속성의 장점은 자바스크립트로 쉽게 접근할 수 있다는 점이다.
<ul>
<li data-id="1" data-user-job="programmer">ASSU</li>
<li data-id="1" data-user-job="ceo">ASSU</li>
</ul>
<script>
console.log(document.querySelector('li').dataset);
// { id: '1', userJob: 'programmer' }
</script>
위와 반대로 dataset 에 데이터를 넣어도 HTML 태그에 반영이 된다. 예를 들면 아래와 같다.
dataset.dailyScrum = 'good';
-->
data-daily-scrum="good"
본 포스트는 조현영 저자의 Node.js 교과서 2판을 기반으로 스터디하며 정리한 내용들입니다.