[Node.js] #2 알아둬야 할 자바스크립트
#2 알아둬야 할 자바스크립트
ES2015(ES6) 이후를 기준으로 합니다.
변수를 선언하는 방법
if (true) {
var x = 3;
}
console.log(x); // 3
if (true) {
const y = 3;
}
console.log(y); // Uncaught ReferenceError: y is not defined
var는 함수 스코프를 가져서 if문의 블록과 관계없이 접근 가능.
const와 let은 블록 스코프를 가져서 블록 밖에서는 변수에 접근 불가.
const a = 0;
a = 1; // Uncaught TypeError: Assignment to constant variable.
let b = 0;
b = 1; // 1
const c; // Uncaught SyntaxError: Missing initializer in const declaration
const는 초기값 주어야하며 값 변경 불가. (swift로 치면 static let)
let은 값 변경 가능
# 템플릿 문자열
아래 예시가 있는데 백틱으로 스트링을 묶는거 외에는 크게 swift랑 다른건 안보임
// 기존 ES5 문법
var num1 = 1;
var num2 = 2;
var result = 3;
var string1 = num1 + ' 더하기 ' + num2 + '는 \'' + result + '\'';
console.log(string1); // 1 더하기 2는 '3'
// ES2015(ES6) 문법
const num3 = 1;
const num4 = 2;
const result2 = 3;
const string2 = `${num3} 더하기 ${num4}는 '${result2}'`;
console.log(string2); // 1 더하기 2는 '3'
# 객체 리터럴
sayJs 같은 객체의 메서드에 함수를 연결할 때 더는 콜론과 function을 붙이지 않아도 된다.
// 기존
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
# 화살표 함수
화살표 함수에서는 function대신 => 기호로 함수를 선언.
화살표함수에서는 내부에 return밖에 없는 경우 코드를 줄일 수 있음.
// add1, 2, 3, 4는 모두 같은 기능
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);
// not도 같은 기능
function not1(x) {
return !x;
}
const not2 = x => !x;
기존 functionrhk 다른 this의 바인딩 박식을 보자.
- this는 자바에서 많이 보았고, 스위프트의 self랑 닮았으며 forEach에 => 는 in으로 느껴지고 { } 는 있어야 하나 스위프트랑 달리 값을 밖에서 꺼내서 정의하는듯.
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();
# 구조 분해 할당
객체와 배열로부터 속성이나 요소를 쉽게 꺼낼 수 있음.
구조 분해 할당을 사용하면 함수의 this가 달라질 수 있음. 달라진 this를 원래대로 되돌려주려면 getCandy함수를 사용해야 한다.
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;
배열에 대한 구조 분해 할당 방법도 존재하는데,
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;
# 클래스
다른 언어처럼 클래스 기반으로 동작하는 것이 아니라 여전히 프로토타입 기반으로 동작.
// 기존의 프로토타입 상속 예제 코드
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
// 아래는 클래스 기반
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
프로토타입 기반 예제는 잘 와닿지 않음. 하지만 클래스 기반 코드는 constructor가 스위프트의 init + 변수선언을 담당하고 sayName을 func 그리고 super를 통해 상속받은 것을 사용하며 $ { } 에서는 constructor에서 정의한 걸 사용하는 것 같음.
타입을 string으로 정의하는게 좀 특이해보임.
# 프로미스
자바스크립트와 노드의 API들이 콜백 대시 프로미스 기반으로 재구성.
악명 높은 콜백지옥 현상 극복했다는 평가.
다만 프로미스는 규칙이 있음. 먼저 프로미스 객체를 생성해야 함.
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('무조건');
});
프로미스의 내부에서 resolve가 호출되면 then으로 그렇지 않으면 catch로
finally는 무조건 실행.
new Promise는 바로 실행되나 결괏값은 then을 붙였을 경우에만 받을 수 있음.
아래 코드는 여러번 호출하는 예제
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에서 resolve를 하면 다음 then에서 message2로 받을 수 있음.
다시 message2가 resolve한 것을 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);
});
}
위의 예제에서는 코드의 깊이가 더이상 깊어지지 않음. 각각 error를 처리하는 것도 catch 한번으로 처리 가능.
프로미스 여러개를 한번에 실행할 수 있는 방법도 존재. (Pomise.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);
});
근데 all의 경우에는 배열에 들어는 것중 하나만 reject여도 catch로 넘어가서 어떤 친구가 reject되었는지 모름.
allSettled를 사용하면 아래처럼 사용 가능.
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('실패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);
});
어떤 프로미스가 reject가 되었는지 확인할 수 있음.
참고로 Node 16부터는 reject된 promise에 catch를 달지 않으면 UnhandledPromiseRejection 에러 발생.
에러 발생 시, 다음 코드가 실행되지 않음.
# async/await
Promise가 자바스크립트 콜백지옥 해결했어도 여전히 코드가 장황함.
ES2017부터 사용할 수 있음.
// 이 코드를 async/awiat를 통해 교체해볼 예정
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);
});
}
// async/await 예제
async function findAndSvaeUser(Users) {
let user = await Users.findOne({});
user.name = 'zero';
user = await Users.save();
user = await Users.findOne({ gender: 'm' });
// 생략
}
와 이걸 보니까 Swift에서 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);
}
}
어떤 프로미스가 reject된 경우.
아래는 화살표 함수도 async와 같이 사용하는 예제.
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을 통해서 프로미스 하나씩 순차처리 가능
ES2018부터 가능
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
(async () => {
for await (promise of [promise1, promise2]) {
console.log(promise);
}
})();
뭔가 스위프트가 자스를 많이 따라간다는 기분이다.
async 함수의 반환값은 항상 Promise로 감싸진다. 따라서 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여서 처리할 수 있다.
async function findAndSaveUser(Users) {
// 생략
}
findAndSaveUser().then(() => { /* 생략 */ });
// 또는
async function other() {
const result = await findAndSaveUser();
}
# Map/Set
맵은 객체와 유사 Set은 배열과 유사
맵은 C++에서 본거같고 Set은 스위프트에서도 잘보임.
- Map
속성들 간의 순서를 보장하고 반복문을 사용할 수 있으며, 문자열이 아닌 값도 사용할 수 있고, size도 쉽게 찾을 수 있음.
(개인의견) 그냥 자료구조라고 생각
const m = new Map();
m.set('a', 'b'); // set(키, 값)으로 Map에 속성 추가
m.set(3, 'C'); // 문자열이 아닌 값을 키로 사용 가능합니다
const d = {};
m.set(d, 'e'); // 객체도 됩니다
m.get(a); // 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', 0, 'e'
} // 속성 간의 순서도 보장됩니다
m.forEach((v, k) => { // forEach도 사용 가능합니다
console.log(k, V); // 결과는 위와 동일
});
m.has(d); // has(키)로 속성 존재 여부 확인
console.log(d); // true
m.delete(d); // delete(키)로 속성을 삭제
m.clear(); // clear()로 전부 제거
console.log(m.size); // 0
- Set
Set은 중복을 허용하지 않음.
const arr = [1, 3, 2, 7, 2, 6, 3, 5];
const s = new Set(arr); // 중복제거
const result = Array.from(s); // 다시 set을 배열로 되돌리기
console.log(result); // 1, 3, 2, 7, 6, 5
# 널 병합/옵셔널 체이닝
ES2020에서 나옴
falsy값: (0, '', false, NaN, null, undefined)
널 병합 연산자(||) 는 falsy값중 null과 undefined만 따로 구분.
const a = 0;
const b = a || 3; // || 연산자는 falsy 값이면 뒤로 넘어감
console.log(b); // 3
const c = 0;
const d = c ?? 3; // ?? 연산자는 null일때랑 undetined일 때만 뒤로 넘어감
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;
옵셔널 체이닝
아래 예시를 보자. 근데 이건 뭐 스위프트에서도 이해했던 개념이므로 당연하다 싶음
{}를 객체로 표현하는데만 주목하자.
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')
}
# AJAX
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// 여기에 예제 코드를 넣으세요.
</script>
get 요청 보내보기
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get('https://www.zerocho.com/api/get')
.then((result) => {
console.log(result);
console.log(result.data); // {}
})
.catch((error) => {
console.error(error);
});
</script>
async await 사용
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
(async () => {
try {
const result = await axios.get('https://www.zerocho.com/api/get');
console.log(result);
console.log(result.data); // {}
} catch (error) {
console.error(error);
}
})();
</script>
이번에는 post 연습
- 전체적인 구조는 비슷하나 바디 부분이 추가되었음.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
(async () => {
try {
const result = await axios.post('https://www.zerocho.com/api/post/json', {
name: 'zerocho',
birth: 1994,
});
console.log(result);
console.log(result.data);
} catch (error) {
console.error(error);
}
})();
</script>
# FormData
이게 뭐냐면 HTML 태그의 form 태그의 데이터를 동적으로 제어할 수 있는 기능.
주로 AJAX와 함께 사용된다.
먼저 FormData 생성자로 formdata 객체를 만든다.
다음 코드를 한 줄씩 console 탭에 입력.
<script>
const formData = new FormData();
formData.append('name', 'zerocho');
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('test', ['hi', 'zero']);
formData.get('test'); // hi, zero
formData.delete('test');
formData.get('test'); // null
formData.set('item', 'apple');
formData.getAll('item'); // ['apple'];
</script>
생성된 객체의 append를 통해 키-밸류로 데이터 저장 가능.
이제 폼 데이터를 axios로 서버로 보내면된다.
그냥 바디를 더 깔끔하게 처리한 느낌.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
(async () => {
try {
const formData = new FormData();
formData.append('name', 'zerocho');
formData.append('birth', 1994);
const result = await axios.post('https://www.zerocho.com/api/post/formdata', formData);
console.log(result);
console.log(result.data);
} catch (error) {
console.error(error);
}
})();
</script>
# encodeURIComponent, decodeURIComponent
AJAX에 요청을 보낼 때 주소에 한글이 들어간 경우 서버에 따라 한글 주소를 이해하지 못하는 경우가 있어서 window객체의 메서드인 encodeURIComponent 메서드를 사용
해당 메서드는 노드에서 사용 가능
한글 부분만 감싸기.
<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);
console.log(result.data); // {}
} catch (error) {
console.error(error);
}
})();
</script>
받는쪽도 인코딩되어 받는데
decodeURIComnponent('%EB....'); // 노드
받는쪽에서 디코드 처리가필요하다.
# 데이터 속성과 dataset
노드를 웹 서버로 사용하는 경우, 클라이언트와 빈번하게 데이터를 주고받게 된다.
이때 서버에서 보내준 데이터를 프런트엔드 어디에 넣어야 할지 고민.
가장 먼저 고민해야할 점은 보안!
민감한 데이터를 절대 보내지말고, 비밀번호 같은것도 내려보내지 말기.
보안과 관련이 없는 데이터는 자유롭게 프런트트엔드로 보내도된다.
<ul>
<li data-id="1" data-user-job="programmer">Zero</li>
<li data-id="2" data-user-job="designer">Nero</li>
<li data-id="3" data-user-job="programmer">Hero</li>
<li data-id="4" data-user-job="ceo">Kero</li>
</ul>
<script>
console.log(document.querySelector('li').dataset);
// { id: '1', userJob: 'programmer' }
</script>
위와같이 HTML5 태그의 속성으로 data-로 시작하는 것들을넣는다.
주석을 보면 조금씩 변형 되었는데, data- 접두어는 사라지고 - 뒤에 위치한 글자는 대문자가 된다. data-id는 id, data-user-job은 userJob이 되었다.
반대로 dataset에 데이터를 넣어도 HTML에 반영된다.
dataset.monthSalary = 10000;을 넣으면 data-moth-salary = "10000" 이라는 속성이 생김