본문 바로가기

BackEnd/Node

[노드교과서] 섹션 1. 알아두어야 할 자바스크립트

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