Kawaii_Jordy

[JS] Event Loop와 비동기(Callback, Promise, Async & Await) 본문

취준/Node.js

[JS] Event Loop와 비동기(Callback, Promise, Async & Await)

Kawaii_Jordy 2021. 9. 7. 09:35

0. Event Loop

  • 자바스크립트는 싱글 스레드(single-thread) 프로그래밍 언어다. 즉, 작동시 하나의 콜 스택만을 가지고 있다는 얘기다.
  • 브라우저가 제공하는 비동기함수(setTimeout, setInterval, ...)는 Web APIs에 이동하게 되고 작동을 시작한다. 작동이 완료되면, 그 실행 데이터들은 콜백 큐(callback queue)에 등록된다. 콜 스택이 비게되면 콜백 큐의 데이터들이 콜 스택에 들어가 실행되는데, 이를 event loop라고 한다.

function test() { console.log(1); setTimeout(function () { console.log(2); }, 0) console.log(3); } test() // => 1, 3, 2 //setTimeout이 0초에 실행된다 해도, 콜 스택을 다 비우고 실행되기에 제일 나중에 뜬다.

  • 만약 모든 함수들이 동기적으로 실행된다면 어떻게 될까? 예를 들어 setTimeout으로 3600초를 걸어버리면, 1시간이 지나 함수가 실행될때 까지 아무런 작업도 진행하지 못하게 될 것이다(blocking).

1. 비동기 다루기

  • 비동기 함수들은 Web APIs에서 작업이 마치는 순서에 따라, 콜백 큐에 들어가게 된다. 이 순서 또한 제어하고 싶으면 callback 함수를 이용하면 된다.

1-1. Callback

  • 콜백 함수의 뜻은 간단하게 다음과 같다.
    • 어떤 함수에 인자로 들어가는 함수.
    • 어떤 이벤트에 의해 호출되는 함수(위에 문장과 크게 다르지 않다).

let printString = (somString, callback) => { setTimeout ( () => { console.log(somString) callback() }, Math.floor(Math.random() * 100) + 1 //서버와 통신하는데, 임의의 시간이 걸린다고 가정하자. ) } let printAllString = () => { printString("a", () => { printString("b", () => { printString("c", () => {}) }) }) } printAllString() // a, b, c (각자 실행되는 시간이 다른데도 순서대로 되어있는 것을 볼 수 있다.)

  • 즉, 하나의 비동기 함수에 다른 비동기 함수를 인자로 넣어서, 해당 비동기 함수가 끝나면 인자로 넣은 비동기 함수를 실행(callback)시키는 형식이다.

let callback = (err, data) => { if (err) { return throw error('something wrong'); } return data };

  • 이런 식으로 콜백함수를 통해 에러를 다룰 수도 있다. (어떤 과정이 잘 끝나면 err에는 null을 넣고 data에는 해당 과정의 data를 넣어 콜백함수를 실행)

  • 콜백은 그 과정이 많아지면, 위와 같은 형태로 될 가능성이 높은데, 이를 callback hell이라고 한다. 이를 피하기 위해 나온 것이 Promise다.

1-2. Promise

  • 프로미스는 비동기 작업을 수행하는 객체다.
  • 프로미스는 생성되고 종료될 때까지 3가지 상태를 갖는다.
    • Pending(대기): 아직 비동기 작업이 완료되지 않은 상태.
    • Fulfilled(이행): 비동기 작업이 완료되어 결과 값을 반환한 상태.
    • Rejected(실패): 비동기 작업이 실패하거나, 오류가 발생한 상태.

let promise = () => { return new Promise((resolve, reject) => { //프로미스 객체는 new Promise로 생성하고 인자로는 executor라는 함수를 인자로 받는다. //excutor 함수는 또 resolve와 reject라는 함수를 인자로 받는다. //만약 서버에서 어떤 데이터를 받아온다고 가정해보자 데이터를 가져오는 함수(주소, function(데이터) { if (데이터) {//데이터 불러오는데 성공했다면 resolve(데이터); } reject(new Error); //실패하면 }); }

  • 프로미스가 생성되자 마자, excutor 함수가 실행되기 때문에 조건 분기를 잘 나눠서 프로미스를 생성해야 한다. (서버와 불필요한 통신을 할 가능성이 있기 때문에)

promise() .then((data) => { //then은 같은 프로미스 객체를 리턴한다. //then의 매개변수는 resolve 함수의 인자다. return 해당 데이터로 할 다른 작업들() //여기서의 리턴은 프로미스 객체를 생성할때의 resolve 안에 들어있는 인자라고 생각해도 될 것 같다. //그렇기에 다음 then의 매개변수에 인자로 들어갈 수 있다. } .then((data2) => { //콜백 함수처럼 then을 계속 이어 데이터들을 사용할 수 있다. //data2는 위의 리턴된 함수의 결과값이 들어간다. //단순히 변수가 리턴되었다면 변수가 then의 인자로 간다. return 결과 } .catch((error) => { //reject에 인자가 들어갈 경우 catch를 통해 다룰 수 있다. console.log(error) } .finally(() => { //finally는 성공했건 실패했건 마지막에 무조건 실행된다. })

Promise.all

  • 프로미스를 then으로 계속 연결 시킨 경우를 살펴보자. 해당 프로미스에서 작업이 끝나면 다음 then에 있는 작업이 시작된다.
  • 만약 이전 프로미스 작업과 then에서 하는 작업이 서로 연관성이 없다면, 굳이 따로따로 할 필요가 있을까.

let plusAllData = () => { return Promise.all([어떤 데이터1을 가져오는 함수, 어떤 데이터2를 가져오는 함수]) //당연하게도 위 함수들은 프로미스 객체를 리턴한다. //위의 인자로 들어간 함수(프로미스 객체)가 갖는 데이터들을 한꺼번에 갖는 프로미스 객체가 리턴된다. .then((allData) => { console.log(allData) } }

1-3. Async & Await

  • 위의 프로미스와 작동원리는 같으나, 좀 더 간편하고 동기적으로 보이게 쓰일 수 있는 기능이다.

let AsyncFtn = async () => { //async를 쓰면 함수 자체가 프로미스 객체를 리턴한다. let id = await 어떤 아이디를 가져오는 함수() //물론 이 함수들도 프로미스를 리턴하는 함수다. let password = await 어떤 비밀번호를 가져오는 함수() return `${id}의 비밀번호는 ${password}다.` //?! //AsyncFtn 함수는 return 값을 갖는 프로미스 객체를 리턴한다. }

  • await은 해당 함수가 작업을 다 완료할 때까지 기다렸다가 실행된다.

let AsyncFtn = async () => { try { let id = await 어떤 아이디를 가져오는 함수() ... } catch (error) { console.log(error) }

  • try ... catch를 통해 에러를 관리할 수 있다.
Comments