ES2015+ (ES6+) 기본


이 포스트는 ES2015(ES6) 이후 적용된 기본적인 새로운 문법에 대해 간략히 기술한다.

소스는 assu10/nodejs.git 에 있습니다.


1. ES2015+ (ES6+, ESNext) 신규 문법

1.1. const, let

https://ideone.com/tk8qIe

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 처리없이 큰따옴표와 작은따옴표를 내부에 사용할 수 있어 훨씬 높은 가독성을 보여준다.

https://ideone.com/pyqKjy

// 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. 객체 리터럴

https://ideone.com/4n6j9p

// 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 () { } 를 사용할 수도 있지만 화살표 함수를 사용하면 코드의 양이 줄어들게 된다.

https://ideone.com/m5PrsE

// 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 바인딩 방식이다.

https://ideone.com/ynVvNb

// 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.resolvePromise.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 라는 키워드를 제공한다.

yielditerator 를 생성할 때 사용하는데 이 iteratoriterable 를 통해 얻는다.
이렇게 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판을 기반으로 스터디하며 정리한 내용들입니다.

참고 사이트 & 함께 보면 좋은 사이트






© 2020.08. by assu10

Powered by assu10