JavaScript 비동기, 이벤트 루프, Promise

Younngg·2023년 1월 4일

JavaScript

목록 보기
8/11
post-thumbnail

비동기

자바스크립트 제어 흐름

자바스크립트는 다른 멀티스레드 프로그래밍 언어와 다른 방식으로 비동기 동작을 처리함

내부의 비동기 동작을 이해하기 위해서는 이벤트 루프 등의 개념을 알아야 한다!

자바스크립트 엔진

  • 자바스크립트 엔진은 하나의 메인 스레드로 구성된다.
  • 메인 스레드는 코드를 읽어 한 줄씩 실행한다.
  • 브라우저 환경에서는 유저 이벤트를 처리하고 화면을 그린다.

동기적 제어 흐름

현재 실행 중인 코드가 종료되기 전까지 다음 줄의 코드를 실행하지 않는 것

  • 자바스크립트에서는 분기문, 반복문, 함수 호출 등이 동기적으로 실행된다.
  • 코드의 흐름과 실제 제어 흐름이 동일하다.
  • 싱글 스레드 환경에서 메인 스레드를 긴 시간 점유하면, 프로그램을 멈추게 한다.
let a = 10;
console.log('a : ', a);

function foo(num) {
  for (let i = 0; i < 10; ++i) {
    console.log(num);
  }
}

foo('num');

// a : 10
// num * 10

비동기적 제어 흐름

현재 실행 중인 코드가 종료되기 전에 다음 라인의 코드를 실행하는 것

  • 프로미스, 콜백 함수를 호출하는 함수 등
  • 코드 흐름과 실제 제어 흐름이 다르다.
  • 비동기 작업을 기다리는 동안 메인 스레드는 다른 작업을 처리
let a = 10;

setTimeout(function callback() {
  console.log('a: ', a);
}, 3000);

console.log('Finished');

// Finished
// a: 10

이벤트 루프

자바스크립트 엔진은 비동기 처리를 제공하지 않는다. 대신, 비동기 코드는 정해진 함수를 제공하여 활용할 수 있다. 이 함수들을 API라 한다.

비동기 API의 예시) setTimeout, XMLHttpRequest, fetch

node.js의 경우 파일 처리 API, 암호화 API 등을 제공한다

// 타이머 비동기 처리
setTimeout(() => console.log('타이머 끝'), 1000);
setInterval(() => console.log('인터벌 타이머'), 1000);

// 네트워크 처리
fetch('https://google.com')
  .then(() => console.log('네트워크 요청 성공'))
  .catch(() => console.log('네트워크 요청 실패'));

비동기 처리 모델

비동기 코드가 불러와지고, 브라우저의 경우 Web API 모듈에서 setTimeout의 delay 시간이 만료되면 Task queue에 콜백함수를 넣고, 코드가 실행된다. Job queue는 Promise나 애니메이션 프레임 같은 경우에 사용된다.

  1. 이벤트 루프가 하는 일은 비동기 코드가 끝났을 때 메인스레드가 Call stack을 비웠다고 가정하면, Task queue에 task가 남아있는지 확인한다.
  2. 남아있다면 이벤트 루프를 통해 task를 Call stack으로 넘겨 코드를 실행시킨다.
  3. Call stack이 비워지면 또 다시 Task queue를 체크한다.

setTimeout(fn, 0)을 실행하면 어떻게 될까?

  1. setTimeout 함수의 delay 시간이 4ms 이하인 경우 최소 지연 시간인 4ms가 지정된다.
  2. setTimeout시 실행되면서 call stack에서 setTimeout이 제거
  3. 타이머가 만료되면 콜백함수 fn이 task queue에 들어간다.
  4. 이벤트루프가 call stack이 비었는지 확인하고, 비었으면 콜백 함수를 call stack에 넣는다.
  5. fn이 실행되고 콜스택에서 제거된다.

이때 task queue에 담긴 callback이 많거나 CPU-intensive한 작업을 수행하게 되면 병목현상이 일어나 queue 뒷단에 있는 함수들은 실행이 늦어지게 된다. 실행 시점이 중요한 콜백 함수들엔 문제가 될 수 있다.
그렇기 때문에 Node.js는 시간에 민감한 어플리케이션을 개발하기에는 적합하지 않다.

👩🏻‍💻정리!

  • 비동기 코드를 처리하는 모듈은 자바스크립트 엔진 외부에 있다
  • 이벤트 루프, 태스크 큐, 잡 큐 등으로 구성된다.
  • API 모듈은 비동기 요청을 처리 후 태스크 큐에 콜백 함수를 넣는다.
  • 자바스크립트 엔진은 콜 스택이 비워지면, 태스크 큐의 콜백 함수를 실행한다.

Promise

각 함수에 대한 데이터를 사용하기 위해 비동기적으로 데이터를 가져와야 한다고 가정

function getName(cb) {
    setTimeout(() => {
        cb("Elice");
    }, 2000);
}

function getAge(cb) {
    setTimeout(() => {
        cb(6);
    }, 2000);
}

function getAddress(cb) {
    setTimeout(() => {
        cb("Seoul");
    }, 2000);
}

console.log를 한 번만 사용해야 한다면 어떻게 해야할까?

getName((name) => {
    getAge((age) => {
        getAddress((address) => {
            console.log(name, age, address)
        })
    })
})

이렇게 콜백함수 안에 콜백함수를 반복 호출하면 name, age, address를 한꺼번에 접근할 수 있다. 비동기 함수가 3개 쓰이고 각 2초씩 걸리기 때문에 6초 뒤에 Elice 6 Seoul이라는 log가 나온다.

이런 식으로 비동기 함수가 많아지면, 이를 콜백 지옥이라고 부른다.

콜백 함수가 많아지면 함수가 복잡해지고 가독성이 떨어지며, 콜스택의 아래 방향으로 에러가 전파되기 때문에 에러처리가 곤란하다.

function getName() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Elice");
        }, 2000);
    })
}

function getAge() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(6);
        }, 2000);
    })
}

function getAddress() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Seoul");
        }, 2000);
    })
}

// promise
Promise
    .all([getName(), getAge(), getAddress()])
    .then((res) => {
        const [name, age, address] = res;
        console.log(name, age, address)
    })

// async/await
(async () => {
    const name = await getName();
    const age = await getAge();
    const address = await getAddress();

    console.log(name, age, address);
})();

Promise

  • Promise API는 비동기 API 중 하나이다.
  • 태스크 큐가 아닌 잡 큐(Job queue 혹은 microtask queue)를 사용한다.
  • 잡 큐는 태스크 큐보다 우선순위가 높다.
setTimeout(() => {
  console.log('타임아웃1');
}, 0);

Promise.resolve().then(() => console.log('프로미스1'));

setTimeout(() => {
  console.log('타임아웃2');
}, 0);

Promise.resolve().then(() => console.log('프로미스2'));

// 프로미스 1 프로미스 2
// 타임아웃 1 타임아웃 2
  • 비동기 작업의 진행(pending), 성공(resolved, fulfilled), 실패(rejected) 상태를 표현
    • 성공과 실패는 작업이 끝났다는 의미에서 settled라고 한다.
  • 비동기 처리의 순서를 표현할 수 있음

Promise 생성자

let promise = new Promise((resolve, reject) => {
  if (Math.random() < 0.5) {
    return reject('실패');
  }
  resolve(10);
});
  • new Promise(callback) 으로 생성
  • 콜백 함수는 resolve, reject 두 인자를 받는다.
  • Promise가 성공했을 땐 resolve 호출, 실패 시엔 reject 호출
let promise = new Promise((resolve, reject) => {
  if (Math.random() < 0.5) {
    return reject('실패');
  }
  resolve(10);
});

promise
  .then((data) => console.log('성공 : ', data))
  .catch((e) => console.log('실패 : ', e))
  .finally(() => console.log('promise 종료'));
  • 성공의 경우, .then 메서드가 호출이 되고, 실패는 .catch 메서드가 호출된다. 또는 .then(성공fn, 실패fn) 으로 작성할 수 있다.
  • finally() 메서드는 성공/실패 여부와 상관 없이 실행할 콜백함수를 넘긴다.

Promise 메서드 체인

promise
  .then((data) => {
    return fetchUser(data);
  })
  .then((user) => console.log('User : ', user))
  .catch((e) => console.log('실패 : '), e);
  • then/catch 메서드가 또 다른 promise를 리턴하여, 비동기 코드에 순서를 부여한다.
  • 이렇게 동일한 객체에 메서드를 연결할 수 있는 것을 체이닝(chaining)이라 한다.
  • 함수를 호출한 주체가 함수를 끝낸 뒤 자기 자신을 리턴하도록 하여 구현

Promise.resolve, Promise.reject

Promise
	.resolve(10)
	.then(console.log)

Promise
	.reject("Error")
	.catch(console.log)
  • Promise.resolve는 성공한 Promise 바로 반환
  • Promise.reject는 실패한 Promise 바로 반환
  • 인위적으로 Promise 메서드 체인을 만들 수 있음
  • 비동기 코드로 진행해야 하는 상황 등에서 유용하게 사용 가능

Promise.all

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log('모두 성공 : ', values);
  })
  .catch((e) => {
    console.log('하나라도 실패 : ', e);
  });
  • promise의 배열을 받아 모두 성공시 각 Promise의 resolved값을 배열로 반환
  • 하나라도 실패할 시, 가장 먼저 실패한 Promise의 실패 이유 반환


promise의 꼬리에 꼬리를 무는 코드 작성 방식 또한 함수가 많아지면 가독성이 떨어지게 된다.
이를 보완하기 위해 async await이 등장했다.
async 함수 안에서 await을 통해 반환 값을 받아 올 수 있으며, 코드를 더 동기적으로 보이게 작성할 수 있다.

https://velog.io/@younngg1012/asyncawait









모든 이미지의 출처는 (주)엘리스에 있습니다.
profile
8533283@naver.com

0개의 댓글