보통 자바스크립트를 배울 때는 var로 변수를 선언하는 방법. 하지만 var은 이제 const, let이 대체.
if(true) {
var x = 3;
}
console.log(x); //3
if (true) {
const y = 3;
}
console.log(y); //y is not defined
var은 함수 스코프를 가지므로 if문의 블록과 관계 없이 접근할 수 있다. 하지만 const, let은 블록 스코프를 가지므로 블록 밖에서는 변수에 접근할 수 없다. 블록의 범위는 if, while, for, function 등에서 볼 수 있는 중괄호 사이. 함수 스코프 대신 블록 스코프를 사용함으로써 호이스팅 같은 문제도 해결되고 코드도 수월.
ES6 문법에 새로운 문자열이 생겼다. 문자열은 큰따옴표나 작은따옴표로 감싸는 기준 문자열과 다르게 백틱으로 감싼다. 특이한 점은 문자열 안에 변수를 넣을 수 있다.
// 템플릿 문자열
var num1 = 1;
var num2 = 2;
var result = 3;
var string1 = num1 + '더하기' + num2 + '는 \'' + result + '\'';
1더하기2는 '3'
문자열 string1 은 띄어쓰기와 변수, 더하기 기호 때문에 가독성이 좋지 않음. 또한, 작은따옴표를 이스케이프(escape)하느라 코드가 지저분. ES6에서부터 다음과 같이 사용할 수 있다.
const num3 = 10;
const num4 = 20;
const result2 = 30;
const string2 = `${num3} 더하기 ${num4}는 ${result2}`
console.log(string2);
10 더하기 20는 30
훨씬 깔끔. ${변수} 형식으로 변수를 더하기 기호 없이 문자열에 넣을 수.
객체 리터럴에 편리한 기능이 추가
다음 코드는 oldObject 객체에 동적으로 속성을 추가.
var sayNode = function() {
console.log('Node')
};
var es = 'ES'
var oldObject = {
sayJS: function() {
console.log('JS')
},
sayNode : sayNode,
};
oldObject[es + 6] = 'Fantastic'
oldObject.sayNode(); // Node
oldObject.sayJS(); // JS
console.log(oldObject.ES6); //Fantastic
이코드를 다음과 같이 쓸 수 있다.
const newObject ={
sayJS(){
console.log('JS')
},
sayNode,
[es+6]: 'Fantastic',
};
newObject.sayNode(); // Node
newObject.sayJS(); // JS
console.log(newObject.ES6); //Fantastic
oldObject 와 newObject 비교. sayJS와 같은 객체의 메서드에 함수를 연결 시 더는 콜론(:)과 function을 붙이지 않아도 됨. sayNode : sayNode처럼 속성명과 변수명이 동일한 경우 한 번만 써도 됨.
{name : name, age: age} //es5
{name, age} //es6
객체의 속성명은 동적으로 생성할 수 있다. 예전 문법에서는 ES6라는 속성명을 만들려면 객체 리터럴(oldObject) 바깥에서 [es+6]를 해야 했다. 하지만 ES6에서는 객체 리터럴 안에 동적 속성을 선언해도 됨. newObject안에서 [es+6]가 속성명으로 바로 사용.
// 화살표 함수
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,2,3,4는 같은 기능을 하는 함수. not1,2도 같은 기능. 화살표함수에서는 function 대신 => 기호로 함수를 선언. 변수에 대입하면 나중에 재사용 가능.
내부에 return 밖에 없는 경우 return 문을 줄일 수 있다. 중괄호 대신 return할 식을 다보 적으면 됨. add4처럼 소괄호로 감쌀 수도. return 문을 줄이는 문법은 자주 사용!
기존 function과 다른 점은 this 바인드 방식.
var relationships = {
name : 'zero',
friends: ['nero', 'hero', 'xero'],
logFri : function() {
var that = this;
this.friends.forEach(function(friend){
console.log(that.name, friend);
});
},
};
relationships.logFri()
const relationship2 = {
name : 'zero',
friends: ['nero', 'hero', 'xero'],
logFri() {
this.friends.forEach(friend => {
console.log(this.name, friend);
});
},
};
relationship2.logFri();
relationship1.logFri()안의 forEach 문에서는 function 선언문을 사용. 각자 다른 함수 스코프의 this를 가지므로 that이라는 변수를 사용해서 relationship1에 간접적으로 접근. 하지만 relationship2.logFri()안의 forEach문에서는 화살표 함수를 사용. 따라서 바깥 스코프인 logFri()의 this를 그대로 사용가능.
구조분해 할당을 사용하면 객체와 배열로부터 속성이나 요소를 쉽게 꺼낼 수 있다. 다음은 객체의 속성을 같은 이름의 변수에 대입하는 코드.
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 = ['node.js', {}, 10, true]
var node = array[0];
var obj = array[1];
var bool = array[3];
다음과 같이 바꿀 수 있다.
const array = ['node.js', {}, 10, true]
const [node, obj, bool] = array
어색해보이지만, 나름의 규칙 존재. node, obj, bool의 위치를 보면 node는 배열의 첫번째, obj는 두번째, bool은 네번째요소. obj와 bool사이 요소인 10에는 변수명을 지어주지 않았으므로 10은 무시.
클래스 문법도 추가되었다. 다른 언어처럼 클래스 기반으로 동작하는 게 아니라 여전히 프로토타입 기반으로 동작. 프로토타입 기반 문법을 보기 좋게 클래스로 바꾼 것.
var Human = function(type){
this.type = type || 'human';
};
Human.isHuman = function(human){
return human instanceof Human;
}
Human.prototype.breathe = function() {
alert('haaaaaaaaaaaaam')
}
var BORAM = function(type, firstName, lastName){
Human.apply(this, arguments);
this.firstName = firstName;
this.lastName = lastName;
};
BORAM.prototype = Object.create(Human.prototype);
BORAM.prototype.constructor = BORAM; // 상속하는 부분
BORAM.prototype.sayName = function() {
alert(this.firstName + ' ' + this.lastName);
};
var OldBoram = new BORAM('human', 'BORAM', "I");
Human.isHuman(OldBoram);
human 생성자 하수가 있고, 그 함수를 생성자 함수가 상속. Zero 생성자 함수를 보면 상속받기 위한 코드가 난해. 아해는 클래스 기반 코드.
class Human {
constructor(type = 'human'){
this.type = type;
}
static isHuman(human){
return human instanceof Human;
}
breathe() {
alert("haaaaaaaaaaam")
}
}
class Boram extends Human {
constructor(type, firstName, lastName){
super(type);
this.firstName = firstName;
this.lastName = lastName;
}
sayName(){
super.breathe();
alert(`${this.firstName} ${this.lastName}`);
}
}
const newBoram = new Boram('human', 'Boram', 'chan');
Human.isHuman(newBoram)
전반적으로 class 안으로 그룹화되었다. 생성자 함수는 constructor 안으로. Human.isHuman 같은 클래스 함수는 static 키워드로 전환. 프로토타입 함수들도 모두 class 블록 안에 포함되어 어떤 함수가 어떤 클래스 소속인지 알기 쉽다. 상속도 extends 키워드로 쉽게 상속 가능. 그러나 이렇게 클래스를 사용하더라도 자바스크립트는 프로토타입 기반으로 동작.
자바스크립트와 노드에서는 주로 비동기를 접함. ES6부터는 자바스크립트와 노드의 API 들이 콜백 대신 프로미스기반으로 재구성. 콜백헬을 극복했다는 평가.
const condition = true;
const promise = new Promise((resolve, reject) => {
if(condition) {
resolve('성공');
} else {
reject('실패');
}
});
promise
.then((message) => {
console.log(message); // 성공(resolve)한 경우 실행
})
.catch((err) => {
console.log(error); // 실패(reject)한 경우 실행
})
.finally(() => {
// 끝나기 전에 무조건 한 번은 실행
console.log('무조건')
});
new Promise로 프로미스 생성. 내부에 resolve, reject를 매개변수로 갖는 콜백함수. 이렇게 만든 promise 변수와 then, catch 메서드를 붙일 수. 프로미스 내부에서
resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있음.
condition 변수를 false로 바꿔보면 catch에서 에러가 로깅된다.
프로미스를 쉽게 설명하면, 실행은 바로 하되 결과값을 나중에 받는 객체. 결과값은 실행이 완료된 후 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에서 받을 수 있다.
이것을 활용해서 콜백을 프로미스로 바꿀 수. 다음은 콜백을 쓰는 패턴 중 하나.
function findAndSaveUser(Users){
Users.findOne({}, (err, user) => {
//첫번째 콜백
if(err){
return console.error(err);
}
user.name = 'boram';
user.save((err) => {
// 두번째 콜백
if(err){
return console.error(err);
}
Users.findOne({gender:'m'}, (err, user) => {
// 세번째 콜백
// 생략
});
});
});
}
콜백 함수가 세 번 중첩됨. 코드가 계속 깊어짐. 각 콜백 함수마다 에러도 따로 처리해줘야. 이 코드를 다음과 같이 바꿀 수 있음.
function findAndSaveUser(Users){
Users.findOne({})
.then((user) => {
user.name = 'boram'
return user.save();
})
.then((user) => {
return Users.findOne({gender:'m'})
})
.then((user)=>{
//생략
})
.catch(err=>{
console.error(err);
});
}
코드 깊이가 세 단계 이상 깊어지지 x. 위 코드에서 then 메서드들은 순차적으로 실행. 매번 따로 처리했던 에러도 마지막 catch에서 한번에 처리할 수 있음. 하지만 모든 콜백함수를 위와 같이 바꿀 수 있는 것은 아님. 메서드가 프로미스 방식을 지원해야.
예제의 코드는 findOne과 save 메서드가 내부적으로 프로미스 객체를 가지고 있다고 가정했을 때 가능. 프로미스 여러 개를 한 번에 실행할 수 있는 방법이 있음. 기존의 콜백 패턴이었다면 콜백을 여러 번 중첩해서 사용해야. 하지만 Promise.all을 활용하면 간단.
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
.then((result) => {
console.log(result); //[성공1, 성공2]
})
.catch((err) => {
console.error(err);
})
Promise.resolve는 즉시 resolve 하는 프로미스를 만드는 방법. 비슷한 것으로 즉시 reject 하는 Promise.reject도 있음. 프로미스가 여러 개 있을 때 Promise.all에 넣으면 모두 resolve될 때까지 기다렸다가 then으로 넘어감. result 매개변수에 각각의 프로미스 결과값이 배열로 들어가 있음. Promise 중 하나라도 reject되면 catch로 넘어간다.
프로미스가 콜백 지옥을 해결. 여전히 코드가 장황. async/await 문법은 프로미스 사용한 코드를 한 번 더 깔끔하게 줄임.
async function findAndSaveUser(Users) {
let user = await Users.findOne({});
user.name = 'boram';
user = await user.save();
user = await Users.findOne({gender:'m'});
//생략
}
함수 선언부를 일반 함수 대신 async function 으로 교체한 후, 프로미스 앞에 await을 붙였다. 이제 함수는 해당 프로미스가 resolve 될 때까지 기다린 뒤 다음 로직으로 넘어간다. await Users.findOne({})이 resolve될 때까지 기다린 다음에 user 변수를 초기화. 위 코드는 에러 처리하는 부분(프로미스가 reject된 경우)이 없으므로 다음과 같은 추가작업 필요.
async function findAndSaveUser(Users){
try{
let user = await Users.findOne({});
user.name = 'boram';
user = await user.save();
user = await Users.findOne({ gender:'m' });
//생략
}
catch(error){
console.log(error);
}
}
try/catch 문으로 로직을 감쌌다. 프로미스의 catch 메서드처럼 try/catch 문의 catch가 에러를 처리.
화살표 함수도 async와 같이 사용할 수 있다.
const findAndSaveUser = async (Users) => {
try{
let user = await Users.findOne({});
user.name = 'boram';
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을 붙여서 처리.
AJAX(Asynchronous Javascript And XML)는 비동기적 웹 서비스를 개발 시에 사용하는 기법. 이름에 XML이 있지만 요즘 JSON을 더 많이 사용. 페이지 이동 없이 서버에 요청을 보내고 응답을 받는 기술. jQuery 나 axios 같은 라이브러리를 이용. 브라우저에서 기본적으로 XMLHttpRequest 객체를 제공하긴 하지만 사용 방법이 복잡 & 서버에서는 사용 불가.
HTML form 태그의 데이터를 동적으로 제어할 수 있는 기능. 주로 AJAX와 함께 사용. 생성된 객체의 append 메서드로 키-값 형식의 데이터를 저장할 수 있다.
AJAX 요청을 보낼 때, 주소에 한글이 들어가는 경우. 서버 종류에 따라 다르지만 서버가 한글 주소를 이해하지 못하는 경우가 있다. 이럴 때는 window 객체의 메서드인 encodeURIComponent 메서드를 사용. 노드에서도 사용할 수 있다. 한글 부분만 encodeURIComponent 메서드로 감싼다. 받는 쪽에서는 decodeURIComponent를 사용하면 된다. 이후의 예제에서 요 두개는 한글을 처리하기 위한 것.
노드를 웹 서버로 사용하는 경우, 클라이언트(프론트엔드)와 빈번하게 데이터를 주고 받게 됨. 이때 서버에서 보내준 데이터를 프론터엔드 어디에 넣어야 할까? 고민하게됨.
<ul>
<li data-id="1" data-user-job ="programmer">Borma</li>
<li data-id="2" data-user-job ="designer">Porma</li>
<li data-id="3" data-user-job ="programmer">Boram</li>
<li data-id="4" data-user-job ="ceo">choram</li>
</ul>
<script>
console.log(document.querySelector('li').dataset);
// {id : '1', userJob : programer'}
</script>
위와 같이 HTML 태그 속성으로 data- 로 시작하는 것들을 넣는다. 이들이 데이터 속성. 여기서는 data-id 와 data-user-job을 줌. 이 데이터를 사용해 서버에 요청을 보내게 됨.
데이터 속성의 장점은 자바스크립트로 쉽게 접근가능. script 태그를 보면 dataset 속성을 통해 첫 번째 li 태그의 데이터 속성에 접근하고 있다. 단, 데이터 속성의 이름이 조금 변형. 앞의 data- 접두어는 사라지고 - 뒤에 위치한 글자는 대문자. data-id는 id, data-user-job은 userJob이 되는 것.
반대로 dataset에 데이터를 넣어도 HTML 태그에 반영. dataset.monthSalary = 10000; 을 넣으면 data-month-salary = "10000" 이라는 속성이 생김. 나중에 실습 예제에서 데이터 속성을 자주 쓰게 됨.