본문 바로가기
카테고리 없음

Node.js - 호출스택, 이벤트 루프 / ES2015+ 문법

by stayby 2024. 4. 28.

1. 호출 스택과 이벤트 루프 ⭐️⭐️⭐️⭐️⭐️

1-1. 호출 스택

function first(){
  second();
  console.log('첫 번째')
}

function second(){
  third();
  console.log('두 번째')
}

function third(){
  console.log('세 번째')
}

first()

 

위 코드의 순서를 예측해보면 콘솔 창에 "세번째"→"두번째"→"첫번째" 순으로 결과가 출력될 텐데 이를 쉽게 파악하기 위해선 호출스택을 그려보면 된다.

[개정3판]Node.js 교과서 호출스택

위 표에서 Anonymous는 가상의 전역 컨텍스트이며, 파일이 실행될 때 생기며, 파일이 끝나면 사라진다. 이 Anonymous까지 사라지면 자바스크립트 실행이 완료되었다고 볼 수 있다.

 

그리고 보이는 것처럼 함수 호출 순서대로 쌓이고 그의 역순으로 함수가 실행된다.

이후 함수 실행이 완료되면 스택에서 빠진다.

이렇게 Last in First out 구조를 LIFO 구조라 하고 스택이라고 불린다.

 

이번엔 아래 예시 코드의 순서를 예측해보면 "시작"→"끝"→ "3초 후 실행" 일 것이다.

function run(){
  console.log('3초 후 실행')
}

console.log('시작');
setTimeout(run, 3000);
console.log('끝')

 

이런 비동기 코드는 호출 스택만으로는 설명이 되지 않는다.

 

그래서 호출 스택 + 이벤트 루프 까지 같이 해야 설명할 수 있다.

 

1-2. 이벤트 루프

이벤트 루프의 구조까지 생각하면서 위 코드를 분석해보면 아래와 같다.

[개정3판]Node.js 교과서 이벤트 루프1

1. 예시에서 setTimeout이 호출 될 때 콜백함수 run은 백그라운드로 보내지며, 백그라운드에서 3초를 보낸다.

3초가 다 지난 후엔 태스크 큐로 보내지며, 

[개정3판]Node.js 교과서 이벤트 루프2

 

2. setTimeout과 anonymous가 실행 완료된 후 호출 스택이 완전히 비워지면 이벤트 루프가 태스크 큐의 콜백을 호출 스택으로 올린다.

(이때, 호출 스택이 비워져야만 올려지며, 만약 호출 스택에 함수가 많이 차 있으면 그것들을 처리하느라 3초가 지난 후에도 run 함수가 태스크 큐에서 대기하게 된다. → 타이머가 정확하지 않을 수 있는 이유이기도 하다.)

[개정3판]Node.js 교과서 이벤트 루프3

 

3. run이 호출 스택에서 실행되고 완료 후 호출 스택에서 빠지게된다. 이벤트 루프는 태스크 큐에 다음 함수가 들어올 때까지 계속 대기한다.

태스크 큐는 실제로 여러 개이며, 태스크 큐들과 함수들 간의 순서를 이벤트 루프가 결정한다.

1. 이벤트 루프 : 이벤트 발생 (setTimeout 등) 시 호출할 콜백 함수들 (위 예제에선 run)을 관리하고 호출할 순서를 결정하는 역할
2. 태스크 큐 : 이벤트 발생 후 호출되어야 할 콜백 함수들이 순서대로 기다리는 공간
3. 백그라운드 : 타이머나 I/O 작업 콜백, 이벤트 리스너들이 대기하는 공간. 여러 작업이 동시에 실행될 수 있다.

 

또다른 예시로 setTimeout과 Promise가 같이 사용되는 코드는 어떤 식으로 진행이 되는지 알아보자!

function oneMore(){
  console.log('one more')
}

function run(){
  console.log('run run')
  setTimeout(()=>{
      console.log('wow')
  }, 0)
  new Promise((resolve)=>{
    resolve('hi')
  }).then(console.log)
  oneMore()
}

setTimeout(run, 5000)

 

1. 호출 스택에 anonymous가 쌓이게 되고 setTimeout(run, 5000) 이 그 다음으로 쌓이게 된다.

2. 그리고 setTimeout이 실행되고 끝나면서 백그라운드에 "타이머(run, 5초)" 를 보내준다.

3. 코드가 다 끝났기 때문에 anonymous도 호출스택에서 사라지며, 백그라운드에서 5초 후에 run함수를 태스크 큐로 보낸다.

4. 호출스택이 비어있을 때 태스크 큐에 있는 run 함수가 이벤트 루프에 의해 호출스택으로 이동한다.

5. 그리고 run이 실행되면서 "run run" 이 콘솔 창에 입력되고 setTimeout(익명함수, 0)이 실행된다.

(0초라고 해서 바로 실행되고 끝나는 것이 아니라 setTimeout은 반드시 백그라운드로 가야한다.)

6. setTimeout 이 호출 스택에서 빠지면서 "타이머(익명함수, 0초)"가 백그라운드로 가게 된다.

7. 그리고 호출스택에서 Promise가 실행되는데 Promise는 .then 전까지는 동기이기 때문에 resolve('hi')도 같이 실행된다.

8. promise는 then을 만나는 순간 비동기가 되기 때문에 then console.log(hi)가 백그라운드로 가게 된다.

9. 호출스택엔 oneMore 함수가 실행되고 그로 인해 콘솔 창엔 "one more"가 입력된다.

10. 현재 호출스택은 비어있는 상태, 백그라운드엔 타이머와 then 이 남아있는 상태가 된다.

11. 백그라운드에서 누가 먼저 실행될지 모른다. 이번 경우 타이머가 먼저 끝난다 치더라도 타이머와 promise중에서 항상 promise.then의 우선순위가 높기 때문에 console.log('hi'), console.log('wow') 순으로 태스크 큐로 이동, 호출 스택에서 실행된다.

12. 결론! "run run" → "one more" → "hi" → "wow" 순으로 콘솔 창에 입력될 것이다.


2. ES2015+ 문법

2-1. const, let

var와 const / let의 가장 큰 차이점은 스코프 이다.

var는 함수 스코프인 반면, const와 let은 블록 스코프({  })를 가진다.

if(true){
  var x = 3;
}

console.log(x);  // 3

if(true){
  const y = 3;
}

console.log(y)  // Uncaught Reference Error : y is not defined


function a(){
  var y = 3;
}
console.log(y) // 에러!

 

물론, 재선언 / 재할당 에서도 var와 const, let은 차이가 있다!

 

2-2. 객체 리터럴

ES6 버전 들어와서 ES5 시절보다 훨씬 간결한 문법으로 객체 리터럴 표현이 가능해졌다.

1. 객체의 메서드에  :function을 붙이지 않아도 됨
2. {sayNode : sayNode} 와 같은 것을 {sayNode} 로 축약이 가능하다
3. [변수 + 값] 등의 동적 속성명을 객체 속성 명으로 사용 가능하다.
// 전

const 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

 

2-3. 화살표 함수

아래 코드에서 add1, add2, add3, add4는 모두 같은 기능을 하는 함수이다.

add2 : add1을 화살표 함수로 나타낸 것,

add3 : 함수의 본문이 return만 있는 경우엔 return을 생략할 수 있다.

add4 : return이 생략된 함수의 본문을 소괄호로 감싸줄 수 있다.

 

not1과 not2도 같은 기능을 하는데 not2처럼 매개변수가 하나일 경우 괄호를 생략할 수 있다.

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;

// 그런데 만약 객체를 리턴하는 함수인 경우, 예를 들어 아래와 같이 {x + y}를 리턴하는 함수에서
// return과 {}를 생략한다면 
const obj = (x,y) => {
  return {x + y}
}

// const obj = (x,y) => {x + y} 처럼 되어서 헷갈릴 수 있기 때문에
// 객체를 리턴하는 경우엔 소괄호는 필수!이다.

// const obj = (x,y) => ({x + y})

 

또한, 화살표 함수가 기존 function(){}을 완전히 대체하지 못한 이유는 this 바인딩에서 차이가 있기 때문이다.

function은 자신만의 this를 가지는 반면, 화살표 함수는 부모의 this를 물려 받는다.

그래서 this를 사용할거라면 function(){}을 사용하는 것이 좋다.

 

2-4. 구조분해 할당

const example = {a : 123, b : {c : 135, d : 146}}

const a = example.a
const d = example.b.d

// 이를 구조분해할당을 사용하게 되면
// const {변수} = 객체; 

const {a, b : {c,d}} = example;

console.log(a) // 123
console.log(d) // 146

// 배열에도 구조분해할당을 사용할 수 있는데

const arr = [1,2,3,4,5]
const x = arr[0]
const y = arr[1]
const z = arr[4]

// 위처럼 표현하는 것이 아닌 구조분해할당을 사용해서 표현하면 다음과 같으며, 순서를 잘 지켜주기만 하면 된다.
// const [변수] = 배열;  형식이며, 각 배열 인덱스와 변수가 대응된다.

const [x,y,,,,z] = arr;
console.log(x)  // 1
console.log(y)  // 2

 

단, this를 사용하고 있는 경우 구조분해할당을 사용하면 문제가 생길 수 있으니 주의!

 

2-5. 클래스

프로토타입 문법을 깔끔하게 작성할 수 있는 Class 문법이 도입되었다.

(클래스도 프로토타입이다. 다만, 프로토타입 문법을 가독성 좋게 표현하는 것이 클래스!)

Constructor(생성자), Extends(상속) 등을 깔끔하게 처리할 수 있다.

 

Super로 부모 Class를 호출하거나 Static 키워드로 클래스 메서드를 생성하는 등의 장점을 가지고 있다.

class Human {
  constructor(type = 'human') {
    this.type = type
  }
  
  static isHuman(human){
    return human instanceof Human;
  }
  
  breathe(){
    alert('h-a-a')
  }
}


class One extends Human {
  constructor(type, firstName, lastName){
    super(type)
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  sayName(){
    super.breathe();
    alert(`${this.firstName}${this.lastName}`)
  }
}

const newOne = new One('human', 'one', 'ji')
Human.isHuman(newOne) // true

 

2-6. 프로미스, async/await

프로미스는 내용이 실행은 되었지만 결과를 아직 반환하지 않은 객체이며,

Then을 붙이면 결과를 반환한다.

실행이 완료되지 않았으면 완료된 후 Then 내부 함수가 실행 된다.

 

Resolve(성공 리턴 값) -> then으로 연결,

Reject(실패 리턴 값) -> catch로 연결

Finally 부분은 무조건 실행된다.

 

프로미스의 then 연달아 사용 가능하며, then 안에서 return 한 값이 다음 then으로 넘어간다.

return 값이 프로미스면 resolve 후 넘어가며, 에러가 난 경우 바로 catch로 이동해서 한번에 처리된다.

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('무조건')
})

 

Promise.resolve(성공 리턴 값) : 바로 resolve 하는 프로미스

Promise.reject(실패 리턴 값) : 바로 reject 하는 프로미스

 

Promise.all(배열) : 여러 개의 프로미스를 동시에 실행, 하나라도 실패하면 catch로 간다.

Promise.allSettled(배열) : 여러 개의 프로미스를 동시에 실행해서 실패한 것만 추려낼 수 있음.

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)
})

이전의 프로미스 패턴의 코드들은 모두 Async / await으로 한번 더 축약이 가능하다.

 

async function의 도입으로

변수 = await 프로미스; 인 경우 프로미스가 resolve된 값이 변수에 저장되며,

변수 await 값; 인 경우 그 값이 변수에 저장된다.

async function findAndSaveUser(Users){
  let user = await Users.findOne({})
  user.name = 'One'
  user = await user.save()
  user = await Users.findOne({gender : 'm'})
}

 

화살표 함수도 async / await이 가능하다.

 

Async 함수에서 return한 것들은 무조건 then이나 await을 붙여줘야 한다. 왜냐하면 async도 결국 프로미스이기 때문이다.

그리고 async가 프로미스이기 때문에 따지고 보면 resolve만 있고 reject 시 처리가 없기 때문에 반드시 try catch로 에러처리를 해주어야 한다!

const promise = new Promise(...)

async function main(){
  try{
    const result = await promise
    return 'Oneji'
  }catch(e){
    console.error(e)
  }
}

main().then((name)=>{ ... })
// 또는
const name = await main()

 

2-7. Map / Set

Map은 객체와 유사한 자료 구조이다.

const m = new Map();

m.set('a','b) // set(키, 값)으로 Map에 속성 추가
m.set(3,'c') // 문자열이 아닌 값을 키로 사용 가능!!!

const d = {};
m.set(d,'e') // 객체도 된다는 점!!

m.get(d) // 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', {}, 'e'
}  // 속성 간의 순서도 보장 된다.

m.has(d) // has(키)로 속성 존재 여부 확인 (true or false)

m.delete(d)  // delete(키)로 속성을 삭제
m.clear() // clear()로 전부 제거

 

Set은 배열과 유사한 자료구조로서 차이점은 중복된 값은 무시된다는 점이기 때문에 기존 배열의 중복을 제거할 때도 사용된다.

const s = new Set();

s.add(false) // add(요소)로 Set에 추가
s.add(1)
s.add('1')
s.add(1)  // 중복이므로 무시된다.
s.add(2)

console.log(size) // 중복이 제거되어 4

s.has(1) // true

for(const a of s){
  console.log(a) // false 1 '1' 2
}

s.delete(2) // delete(요소)로 요소를 제거
s.clear()  // clear()로 전부 제거


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

 

2-7. Null 병합 / 옵셔널 체이닝

?? (null 병합, nullish coalescing) 연산자는 || 대용으로 사용되며, falsy 값 중 null과 undefined만 따로 구분한다.

 

?? 연산자는 null과 undefined일 때만 뒤로 넘어간다.

const a = 0;
const b = a || 3  // || 연산자는 falsy 값이면 뒤로 넘어간다.

const c = 0;
const d = c ?? 3  // ?? 연산자는 null과 undefined일 때만 뒤로 넘어간다.
console.log(d)  // 0;

const e = null
const f = e ?? 3
console.log(f)  // 3

 

?. (옵셔널 체이닝) 연산자는 null이나 undefined의 속성을 조회하는 경우 에러가 발생하는 것을 막아준다.

const a = {}
a.b;  // a가 객체이므로 문제 없음

const c = null;
try {
  c.d;
} catch(e){
  console.error(e)  // TypeError:Cannot read properties of null
}

c?.d  // 문제 없음

3. 프론트엔드 자바스크립트

3-1. Ajax(Asynchronous Javascript And XML)

자바스크립트를 이용해서 서버와 브라우저가 비동기 방식으로 데이터를 교환할 수 있는 통신 기능이다.

브라우저가 가지고 있는 XMLHttpRequest 객체를 이용해서 전체 페이지를 새로 고치지 않고도 페이지의 일부만을 위한 데이터를 로드하는 기법이다.

 

즉, 자바스크립트를 통해서 서버에 데이터를 비동기 방식으로 요청하는 것이다.

axios.get('https://www.oneji.com/api/get').then((result)=>{
  console.log(result);
  console.log(result.data); // {}
}).catch(()=>{
  console.error(error)
})

async ()=>{
  try{
    const result = await axios.get('https://www.oneji.com/api/get');
    console.log(result)
    console.log(result.data) // {}
  } catch (e){
    console.error(e)
  }
}


// ===========================================================

async ()=>{
  try{
    const result = await axios.post('https://www.oneji.com/api/get', {
      name : 'oneji',
      birth : 1994,
    });
    console.log(result)
    console.log(result.data) // {}
  } catch(e){
    console.error(e)
  }
}

 

3-2. FormData

HTML form 태그에 담긴 데이터를 AJAX 요청으로 보내고 싶은 경우 사용하며,

formData 객체를 이용한다.

FormData 메서드

1. Append로 데이터를 하나씩 추가
2. Has로 데이터 존재 여부 확인
3. Get으로 데이터 조회
4. getAll로 데이터 모두 조회
5. delete로 데이터 삭제
6. set으로 데이터 수정

 

그래서 만약 FormData POST 요청을 보낸다면 Axios의 data자리에 formData를 넣어서 보내면 된다.

async ()=>{
  try{
    const formData = new FormData();
    formData.append('name', 'oneji')
    formData.appen('birth', 1994);
    const result = await axios.post('https://www.oneji.com/api/formdata', formData)
    console.log(result)
    console.log(result.data)
  } catch(e){
    console.error(e)
  }
}

3-3. encodeURIComponent, decodeURIComponent

가끔 주소창에 한글을 입력하면 서버가 처리하지 못하는 경우가 발생하는데 이때, encodeURIComponent로 한글을 감싸줘서 처리하면 된다.

const result = await axios.get(`https://www.oneji.com/api/search/${encodeURIComponent('노드')}`)

 

이제 이 '노드'를 서버에선 %ㄷEB%85%B8...이런 식으로 인코딩이 되어서 오는데 이를 decodeURIComponent로 서버에서 한글로 해석해주어야 한다.

3-4. data attribute와 dataset

HTML 태그에 데이터를 저장하는 방법이다.

서버의 데이터를 프론트엔드 단으로 내려줄 때 사용되며, 태그 속성으로 data-속성명.

 

자바스크립트에서 태그.dataset.속성명으로 접근이 가능하다.

예를들어 data-user-job 이라면 자바스크립트에선 dataset.userjob

 

반대로 자바스크립트 dataset에 값을 넣으면 data-속성 이 생긴다.

dataset.monthSalary = 10000 -> data-month-salary = '10000'

<ul>
  <li data-id="1" data-user-job="programmer">one</li>
  <li data-id="2" data-user-job="designer">two</li>
  <li data-id="3" data-user-job="programmer">three</li>
  <li data-id="4" data-user-job="student">four</li>
</ul>
<script>
  console.log(document.querySelector('li').dataset);
  //{id : '1', userJob : 'programmer'}
</script>