p.s javascript에서 가장 중요한 3가지
⇒ 이벤트루프, 프로토타입, 실행컨테스트(this, scope)
2. 호출 스택 알아보기
2.1. 호출 스택, 이벤트 루트
1. 호출 스택
function first() {
second();
console.log('첫 번째');
}
function second() {
third();
console.log('두 번째');
}
function third() {
console.log('세 번째);
}
first(); // console.log의 출력 순서 : '세 번째' → '두 번째' → '첫 번째'
⇒ Anonymousm는 가상의 전역 컨테스트이다. ( 항상 있다고 생각함이 이해하기 쉽다)
- 함수는 호출하는 순서대로 스택에 쌓이고, 역순으로 실행된다.
- 함수 실행이 완료되면 스택에서 빠진다.
- LIFO(Last In First Out : 후입선출) 구조라서 스택이라고 부른다.
ex) 호출 스택 + 이벤트 루프의 예시
function run() {
console.log("3초후 실행");
}
console.log('시작');
setTimeout(run, 3000);
console.log('끝');
// console.log의 순서예측 시작 → 끝 → 3초 후 실행
⇒ 호출 스택만으로는 run함수를 설명할 수 없다. ( run을 직접 호출하지 않았기 때문)
: run 함수는 백그라운드에 진입하여 타이머가 끝나길 기다리기 때문이다.
그러므로 호출 스택 + 이벤트 루프(run 함수)로 설명할 수 있다.
* 도표(호출스택 + 이벤트루프 실행순서 예시)
: 백 가운드와, 태스트 큐는 다른언어로 만들어져 있고 호출 스택만 자바스크립트로 되어있다.
- run 함수는 백그라운드 타이머 시간이 되면 태스크 큐로 이동 후 이벤트 루프가 호출 스택으로 옴겨준다.
- 백그라운드에서 발생한 타이머는 태스크 큐로 run 함수가 내려갈 때 삭제된다.
- 태스크 큐에 있는 함수는 호출 스택이 비어 있을때만 호출 스택으로 이동이 가능하다.
호출 스택(this, scopre) | ⑭ run 삭제 | 백 그라운드 | ⑥ 타이머(run, 3초) |
⑫ run | ⑫ 타이머(run, 3초) 삭제 | ||
⑩ console 삭제 | |||
⑧ console | 태스크 큐 | ⑪ run | |
⑦ settimeout 삭제 | |||
⑤ settimeout(비동기함수) | |||
④ console 삭제 | 콘솔 창 | ③ "시작" | |
② console | ⑨ "끝" | ||
① anonymous |
ex2) 호출 스택과 이벤트루프의 예시
function oneMore() {
console.log('one more');
}
function run() {
console.log('run run');
// Promise가 setTimeout보다 우선순위가 높다.
// 태스크 큐에 동시 진입 시 setTimeout보다 우선순위가 높은 것들
// - Promise.then/catch
// - process.nextTick
setTimeout(() => {
console.log('wow');
},0);
new Promise((resolve) => {
}).then(console.log);
oneMore();
}
setTimeout(run, 5000); // console의 실행예측 : runrun → one more → wow
2.2 ES2015+
1. const, let
⇒ ES2015 이전에는 var 변수만을 사용하였다.
- ES2015부터는 const(상수), let(변수)로 var가 대체 되었다.
- var와 가장 큰 차이점은 const, let은 블록 스코프라는 것이다.(var는 함수 스코프)
- const : 상수, 값을 선언한 후 변경할 수 없다.
- let : 변수, 값을 선언한 후 변경이 가능하다.
// var는 함수 스코프이기 때문에 블록 스코프를 무시한다.
// 그러므로 스코프 밖에서도 스코프 안의 변수에 진입이 가능핟.
// but, 함수로 감쌀 경우에는 외부에서 var를 참조할 수 없다.
if(true) {
var x = 3;
}
console.log(x) // 3 출력
// const는 블록스코프를 무시하지 않으므로 y의 접근에 오류가 발생한다.
if(true) {
const y = 123;
}
console.log(y); // VM41:5 Uncaught ReferenceError: y is not defined
2. 템플릿 문자열
⇒ 문자열을 합칠 때 + 기호 때문에 지저분하고 가독성이 떨어진다.
- ES2015부터는 `(백틱) 특수기호가 사용 가능하다.
- 백틱 문자열 안에 ${변수}처럼 사용이 가능하다.
var won = 1000;
var result = '이 과자는 ' + + won + '원입니다.';
// 이 과자는 1000원입니다.
const result = `이 과자는 ${won}원입니다`;
function a() {}
a();
a\\; 태그드 탬플릿리터럴..?
3. 객체 리터럴
⇒ ES5 시절의 객체 표현 방법이다.
- 속성 표현 방식에 주목하며 소스 코드를 확인하라.
var sayNode = function () {
console.log('Node');
};
var es = 'ES';
var oldObject = {
sayJs: function() {
console.log('JS');
},
sayNode : sayNode,
}
oldObject[es + 6] = 'Fantastic';
oldObject.sayNode();
oldObject.sayJs();
console.log(oldObject.ES6);
// console.log의 출력순서 예측 // Node → JS → Fatastic
⇒ 현재는 훨신 간결한 문법으로 객체 리터럴 표현이 가능하다.
- 객체의 메서드에 :function을 생략해도 됨.
- {sayNode : sayNode}와 같은 중복표기된 표기법을 {sayNode}로 축약이 가능하다.
- [변수+값] 등으로 동적 속성명을 객체에서 바로 선언해 사용할 수 있다.
const sayNode = function() {
console.log('Node');
}
const es = 'ES';
const newObject = {
// before sayJs: function() {}
sayJs() {
console.log('NS');
}
// before sayNode : sayNode
sayNode,
[es+6] : 'Fantastic',
}
newObject.sayNode(); // Node
newObject.sayJs(); // NS
console.log(newObject.ES6); // Fantastic
4. 화살표 함수
⇒ 화살표 함수(Arrow function)는 function 키워드 대신 화살표(=>)를 사용한다.
// function을 이용한 기존 함수
function add1(x, y) {
return x + y;
}
// ex1) 화살표 함수 예시
const add2(x, y) => {
return x+ + y;
}
// ex2) 함수 본문에 return만 존재할 경우 return 생략 가능
const add3(x, y) => x + y;
// ex3) return이 생략된 함수의 본문을 소과로로 감싸줄 수 있다.
// 소괄호로 감싸야 하는 경우는 중괄호가 소스 코드에 포함이 되어 있음.
const add4(x, y) => (x + y);
// before
function objRtn(x, y) {
return {x,y};
}
// after
const objRtn(x, y) => ({x, y});
⇒ 하지만 화살표 함수가 기존 function() {} 을 완전히 대체할 수는 없다.
왜냐하면 화살표 함수(=>)와 기존 함수(function)의 this는 차이가 존재하기 때문이다.
// function을 통해 함수를 생성했을 경우
var relationship1 = {
name : 'zero',
friends : ['zero', 'hero', 'xero'],
logFriends : function() {
var _this = this;
// 로그 예측
this.friends.forEach(function(friend) {
console.log(this,name); // , ,
console.log(this.name, friend); // zero, hero, xero
console.log(_this.name, friend); // zero, zero, zero, hero, zero, xero
});
},
};
relationship1.logFriends();
- 기존함수(function)을 사용 시 foreach this와 logFriends의 this는 다르다.
- _this라는 중간 변수를 선언하여 logFriends에 전달이 가능하다.
- 일반 fucntion에서 Foreach 스코프 안에서 this는 Window 객체를 참조한다. (그러므로 Window.name은 값이 없다)
// 화살표 함수(arrow function) 통해 함수를 생성했을 경우
var relationship2 = {
name : 'zero',
friends : ['zero', 'hero', 'xero'],
logFriends : function() {
var _this = this;
// 로그 예측
this.friends.forEach(friend => {
console.log(this.name, friend); // zero, zero, zero, hero, zero, xero
});
},
};
relationship2.logFriends();
- forEach의 화살표 함수(arrow function)의 this와 logFriends의 this가 동일하다.
- 화살표 함수는 자신을 포함하는 함수의 this를 상속 받는다.
물려받고 싶지 않을 경우에는 :function()을 사용해 주면 된다.
// ex) 화살표 함수(arrow function)를 사용 시 동작할 수 없는 예시
// - 정상적인 동작을 하는 기존 함수
button.addEventListener('click',function() {
console.log(this.textContext); // 자신을 포함하는 this를 상속
});
// - 오류 발생하는 화살표 함수
button.addEventListener('click' () => {
console.log(this.textContext);
});
// - 오류를 해결한 화살표 함수
button.addEventListener('click' (e) => {
console.log(e.target.textContext);
});
- 위와 같이 화살표 함수와 기존 함수의 가장 큰 차이점은 this라고 볼 수 있다.
5. 구조분해 문법
⇒ const { 변수 } = 객체;로 객체 안에 속성을 변수명으로 사용이 가능하다.
const처럼 속성 안의 속성도 변수명으로 사용이 가능하다.
// 객체 구조분해 할당
const example = {
a : 123,
b : {
c : 135,
d : 146,
},
};
// example의 const a, b.d를 선언
const a = example.a, d = example.b.d;
// 구조분해 할당을 통해 example의 a와 b.d를 객체화
const { a, b:{ d } = example;
// 배열 구조분해 할당
const arr = [1, 2, 3, 4, 5];
// 배열의 index를 넘겨 const x, y, z를 선언
const x = arr[0], y = arr[1], z = arr[4]; // x = 1, y = 2, z = 5
// 배열 구조분해 할당을 통해 const명을 선언하여 값을 얻어올 수 있다.
const[x, y, , , z] = arr; // x = 1, y = 2, z = 5
========================================= 해당 부분에 대한 재 확인 필요 ===========================
⇒ 구조분해 할당에서 this를 사용 시 기존 문법이든 과거문법이든 문제가 발생할 수 있다.
var candyMachine = {
status : {
name : 'node',
count : 5,
},
getCandy : function() {
this.status.count++; // count는 6으로 증가
return this.status.count;
},
};
var getCandy = candyMachine.getCandy; // error
var count = candyMachine.status.count;
console.log(getCandy); // ƒ () {
// this.status.count++; // count는 6으로 증가
// return this.status.count;
// }
console.log(count) // 5
var candyMachine = {
status : {
name : 'node',
count : 5,
},
getCandy() {
this.status.count++;
return this.status.count;
},
};
const {getCandy, status: {count} } = candyMachine;
// 오류 발생(Identifier 'getCandy' has already been declared)
========================================= 해당 부분에 대한 재 확인 필요 ===========================
6. 클래스
⇒ 프로토타입 문법을 깔끔하게 작성할 수 있는 Class 문법을 도입.
- constructor(생성자), extends(상속) 등을 쉽고 깔끔하게 처리할 수 있다.
- 특히, 코드가 그룹화 되기 때문에 가독성이 향상된다.
⇒ 기존의 class없이 상속을 받기 위한 소스 코드이다.
// 생성자 함수(생성자 함수는 첫 문자를 대문자로 작성한다.)
var Human = function(type) {
this.type = type || 'human';
};
// static 함수
Human.isHuman = function(human) {
return human instanceof Human;
};
// instance 함수
Human.prototype.breathe = function() {
alert('h-a-a-a-m');
};
//////////////////////////////// 보충학습 필요 //////////////////////////////
var Zero = function(type, firstName, lastName) {
// 부모로부터 apply해준다.
Human.apply(this, arguments); // 상속을 받기 위해 사용된다.
this.firstName = firstName;
this.lastName = lastName;
};
Zero.prototype = Object.create(Human.protytype);
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 사용 시 생성자, static, instance 함수를 하나로 합칠 수 있다.
class Human {
// 생성자 함수
constructor(type = 'human') {
this.type = type;
};
// static 함수
static isHuman(human) {
return human instanceof Human;
};
// instance 함수
breathe() {
alert("h-a-a-a-m");
};
};
class zero extends Human {
constructor(type, firstName, lastName) {
// super는 부모를 참조할 수 있다.
super(type);
this.firstName = firstName;
this.lastName = lastName;
}
sayName() {
// 부모의 instance함수인 breathe 함수를 호출한다.
super.breathe();
alert(`${this.firstName} ${this.lastName}`);
}
};
7. 프로미스( 현재 노드의 생태계가 callback에서 promise로 전환이 되가고 있기 때문에 숙지해야 한다.)
⇒ 콜백 헬이라고 불리는 지저분한 자바스크립트 코드의 해결책이다.
- 프로미스(Promise)는 내용이 실행은 되었지만 결과를 아직 반환하지 않은 객체이다.
당연히 Promise의 then과 catch는 Promise가 실행 완료된 이후에 실행된다.
결과를 원하는 시점에 Promise의 then을 이용하여 결과를 반환시킬 수 있어 매우 유용하다.
// Resolve(성공리턴값) -> then으로 연결
// Reject(실패리턴값) -> catch로 연결
// Finally 부분은 무조건 실행된다.
const condition = true;
const promise = new Promise((resolve, reject) => {
if(condition) {
resolve('성공');
} else {
reject('실패');
}
});
// 다른 코드가 들어갈 수 있음
promise.then((message) => {
console.log(message); // 성공(resolve)한 경우 실행
}).catch((error) => {
console.error(error); // 실패(reject)한 경우 실행
});
8. async/await
⇒ async function의 도입
- 변수 = await 프로미스;인 경우 프로미스가 resolve(성공)된 값이 변수에 저장된다.
- 변수 await 값;인 경우 그 값이 변수에 저장된다.
- await가 then의 역할을 한다고 볼 수 있다.
// Async/await으로 한번 더 축약이 가능하다.
// before
function findAndSaveUser(Users) {
// 왼쪽에서 오른쪽으로 실행이 되는 개념으로 이해하면 좋다.
Users.findOne({})
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne({ gender: 'm'});
})
.then((user) => {
// 생략
})....
}
// after
// 위의 then들을 생략하되 function에 async를 꼭 붙여야 한다.
async function findAndSaveUser(Users) {
// await는 오른쪽에서 왼쪽으로 실행이 된다고 이해하면 좋다.
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await Users.findOne({gender : 'm'});
...
}
⇒ 과거에는 await을 사용하기 위해서는 무조건 async 함수 안에서만 사용이 가능했다.
하지만 현재는 javascript top level에 await이 생겨서 async 함수가 없더라고 수행이 가능하다.
// Promise를 async로 변환하려고 한다.
// Promise 소스코드
const promise = new Promise(...)
promise.then((result) => ...)
// 과거에는 async function으로 감싸야만 await가 오류가 발생하지 않았다.
// before
async function main() {
// async/await은 catch가 없다.
// 고로 try catch로 감싸줘야 한다.
try {
const result = await Promise;
return 'zerocho';
} catch (error) {
console.error(error);
}
}
// async는 Promise라서 Promise의 성질을 그대로 갖고있다.
// 그러므로 return을 받기 위해서는 then or await를 사용해야 한다.
main().then((name => ...);
const name = await main();
// after
// 현재는 javascript top level에 await가 생겨 async 함수로 감싸지 않아도 된다.
const result = await Promise;
⇒ 화살표 함수도 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.log(error);
}
};
8. for await of
⇒ 노드 10부터 지원
⇒ for await(변수 of 프로미스배열)
- resolve(성공)된 프로미스가 반복을 통해 변수에 담겨 나온다.
- await을 사용하기 때문에 async 함수 안에서 해야한다.
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
(async () => {
for await ( promise of [promise1, promise2]) {
console.log(promise);
}
})();
9. Map/Set
⇒ ES2015에서 추가된 자료구조 중 Map은 객체와 유사하고 Set은 배열과 유사하다.
Map과 Set에 대해서 좀더 자세히 공부하고 싶다면 MDN을 참고하자.
// Map은 key와 value로 1:1로 묶여있다.
const m = new Map();
m.set('a','b'); // set (키, 값)으로 Map 속성에 추가
m.set(3, 'c'); // 문자열이 아닌 숫자도 키로 사용 가능하다.
// 객체도 키로 사용될 수 있다.
// 하지만 객체가 선언되어 있지 않은 형태로는 값이 일치하지 않는다.
// 객체를 선언 후 key로 사용해야 한다.
const d = {a : 'b'};
m.set(d, 'e');
// size로 현재 속성의 갯수를 확인 가능하다.
console.log(m.size); // 3
// 반복문에 바로 넣어서 사용이 가능하며 속성 간 순서도 보장된다.
for(const[k,v] of m) {
console.log(k, v); // 'a', 'b', 3, 'c', {a : 'b'}, 'e'
}
m.forEach((k,v) => {
console.log(k, v); // 'a', 'b', 3, 'c', {a : 'b'}, 'e'
});
m.has(d); // has(키)로 속성 존재 여부를 확인이 가능하다.
console.log(m.has(d)); // true
m.delete(d); // delete(키)로 속성을 제거할 수 있다.
m.clear(); // clear()로 모든 속성을 삭제할 수도 있다.
console.log(m.size); // 0
⇒ Set은 중복이 없는 배열 or 중복을 제거하고 싶은 배열을 생성할 때 사용한다.
const s = new Set();
s.add(false); // add(요소)로 Set에 추가한다.
s.add(1);
s.add('1');
s.add(1); // 중복이므로 무시된다.
s.add(2);
console.log(s.zise); // 중복제거로 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); // flase, 1, '1', 2
});
s.delete(2); // delete(요소)로 요소를 제거한다.
s.clear(); // clear()로 요소를 전부 삭제한다.
// WeakMap(WeakSet도 있다)
const wm = new WeakMap();
let obj = {};
wm.set(obj, '123');
console.log(wm.get(obj)); // '123'
// 일반 Map과 다른 점은 선언해둔 object가 비워졌을 경우
// WeakMap안의 키와 값도 가비지컬렉팅되어 함께 삭제된다.
obj = null;
console.log(wm.get(obj)); // undefined
// 가비지 컬렉팅의 가능성 있는 예시
// 기 사용중인 사용자 정보에 파라미터를 추가할 수 없는 경우
// WeakMap을 이용하면 추가적인 프로퍼티를 생성해서 사용할 수 있다.
let user = { name : 'zerocho', age : 29 };
wm.set(user, {married : false});
user = null;
10. 널 병합/옵셔널 체이닝
⇒ ES2020에서 추가된 ??(널 병합(nullish coalescing)) 연산자와 ?.(옵셔널 체이닝(optional chaining)) 연산자이다.
- 널 병함 연산자는 주로 || 연산자 대용으로 사용된다.
falsy 값(0, '', false, NaN, null, undefined) 중 undefined만 따로 구분한다.
const a = 0;
const b = a || 3; // || 연산자는 falsy 값이면 뒤로 넘어간다.
console.log(b); // 3
// 널 병합 연산자(nullish coalescing) 예시
const c = 0;
const d = c ?? 3; // ?? 널 병합 연산자는 null or undefined일때만 뒤로 넘어간다.
console.log(d); // 0
const e = null, g = undefined;
console.log(e ?? 3, g ?? 3); // 3 3
// null이나 undefined 속성 조회하여 오류가 나는 예시
const a = {};
a.b;
const c = null;
try {
c.d;
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null(reading 'd')
}
// 옵셔널 체이닝 사용 예시
console.log(c?.d); // undefined(문제없음)
// 함수의 옵셔널 체이닝 예시
try {
c.f();
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null(reading 'f')
}
console.log(c?.f()); // undefined(문제없음)
// 배열의 옵셔널 체이닝 예시
try {
c[0];
} catch (e) {
console.error(e); // TypeError: Cannot read properties of null(reading '0')
}
console.log(c?.[0]); // undefined(문제없음)
⇒ 옵셔널 체이닝 연산자는 위 코드처럼 일반적인 속성, 함수 호출, 배열 요소 접근에 대하여 에러 발생을 방지할 수 있다.
- TypeError: cannot read properties of undefined 또는 null에러 발생 빈도를 낮추기 위해 사용
// 널 병합/ 옵셔널 체이닝의 동시 사용 예시
const c = null;
console.log(c?.[0] ?? '123') // 123
const d = [1,2,3];
console.log(d?.[0] ?? '123') // 1
'BackEnd > Node' 카테고리의 다른 글
[노드교과서] 섹션 3. http 모듈로 서버 만들기. 섹션 4. 패키지 매니저 (0) | 2023.12.19 |
---|---|
[노드교과서] 섹션 2. 노드 기본 기능 익히기 (0) | 2023.12.17 |
[노드교과서] 프런트엔드 자바 스크립트 (0) | 2023.12.13 |
[노드교과서] 챕터0. Node.js 설치하기 (0) | 2023.12.01 |
[노드교과서] 챕터0. 노드의 정의 (0) | 2023.11.30 |