Jay's Cookbook
Menu
  • Tags
  • Categories
  • Projects
Computer Science
OS
Network
Data Structure
Algorithm
Language
Code Architecture
Python
Javascript
Typescript
Java
Backend
Backend Theory
TypeORM
Node.js
NestJS
FastAPI
Frontend
HTML/CSS
React
Next.js
Data Engineering
DE Theory
MySQL
MongoDB
Elastic
Redis
Kafka
Spark
Airflow
AI
Basic
Pytorch
NLP
Computer Vision
Data Analytics
Statistics
Pandas
Matplotlib
DevOps
Git
Docker
Kubernetes
AWS
[Javascript]: 비동기 프로그래밍
language
javascript

[Javascript]: 비동기 프로그래밍

Jay Kim
Jay Kim 10 Feb 2024
[Javascript]: 타이머 함수 [Javascript]: 자바스크립트 이벤트 루프와 콜 스택

Table of Contents

  • 비동기 함수
  • 프로미스
    • 프로미스의 생성
    • 프로미스의 후속 처리 메서드
      • Promise.prototype.then
      • Promise.prototype.catch
      • Promise.prototype.finally
    • 프로미스의 에러 처리
    • fetch
  • async/await
    • async
    • await
    • 에러 처리

비동기 함수

  • 함수의 실행 순서는 실행 컨텍스트 스택(콜 스택)으로 관리한다
  • 자바스크립트 엔진은 하나의 실행 컨텍스트 스택만을 갖는다
  • 이는 함수를 실행할 수 있는 창구가 단 하나이며, 동시에 2개 이상의 함수를 동시에 실행할 수 없다는 것을 의미한다
  • 이처럼 자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작한다
  • 싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행할 경우 블로킹이 발생한다
  • 이처럼 현재 실행 중인 태스크가 종료할 때까지 다음 실행될 태스크가 대기하는 방식을 동기(synchronous) 처리라고 한다
  • 동기 처리 방식은 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료될 때까지 이후 태스크들이 블로킹되는 단점이 있다
  • 반면에 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기(asynchronous) 처리라고 한다
  • 비동기 처리를 수행하는 비동기 함수는 전통적으로 콜백 패턴을 사용한다. 하지만 콜백 패턴은 콜백 헬을 발생시켜 가독성을 나쁘게 하고, 비동기 처리 중 발생한 에러의 예외 처리가 곤란하며, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다
  • setTimeout, setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다
  • 자바스크립트는 이벤트 루프를 통해 동시성을 지원한다
  • 비동기 함수의 콜백 함수 또는 이벤트 핸들러는 태스크 큐에 일시적으로 보관된다
  • 이벤트 루프는 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기 중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해서 확인한다
  • 만약 콜 스택이 비어있고, 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다
  • 이때 콜 스택으로 이동한 함수는 실행된다. 즉, 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다

  • 자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작한다
  • 브라우저에 있는 태스크 큐와 이벤트 루프가 비동기 처리를 돕는 것이다

프로미스

  • ES6에서 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다
  • 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다
  • 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다
  • 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 후, 나중에 완료된다
  • 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다
  • 예를 들어, 비동기 함수인 setTimeout 함수의 콜백 함수는 setTimeout 함수가 종료된 이후에 호출된다
  • 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다
  • 따라서 비동기 함수의 처리 결과(ex. 서버의 응답)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다
  • 이때 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 것이 일반적이다
  • 하지만 콜백 함수는 콜백 헬 문제와 에러 처리를 못한다는 문제가 있다
  • 이를 극복하기 위해 ES6에서 프로미스가 도입되었다

프로미스의 생성

  • Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달 받는데 이 콜백 함수는 resolve와 reject 함수를 인수로 전달받는다
  • 비동기 처리는 Promise 생성자 함수가 인수로 전달받는 콜백 함수 내부에서 수행한다. 만약 비동기 처리가 성공하면 비동기 처리 결과를 resolve 함수에 인수로 전달하면서 호출하고, 실패하면 에러를 reject 함수에 인수로 전달하면서 호출한다
프로미스의 상태 정보 의미 상태 변경 조건
pending 비동기 처리가 아직 수행되지 않은 상태 프로미스가 생성된 직후 기본 상태
fulfilled 비동기 처리가 수행된 상태 (성공) resolve 함수 호출
rejected 비동기 처리가 수행된 상태 (실패) reject 함수 호출
  • 생성된 직후의 프로미스는 기본적으로 pending 상태다
  • 이후 비동기 처리가 수행되면 처리 결과에 따라 다음과 같이 프로미스의 상태가 변경된다
    • 성공: resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경한다
    • 실패: reject 함수를 호출해 프로미스를 rejected 상태로 변경한다
  • 이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정된다
  • 프로미스는 pending 상태에서 fulfilled 또는 rejected 상태가 되면 더는 다른 상태로 변화할 수 없다
  • 프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과도 갖는다
  • 성공하면, resolve() 함수의 첫 번째 인자로 넘겨준 값을 처리 결과 값으로 한다
  • 실패하면, reject() 함수의 첫 번째 인자로 넘겨준 에러 객체를 결과 값으로 한다
  • 즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다

프로미스의 후속 처리 메서드

  • 프로미스가 fulfilled 또는 rejected가 되면 처리 결과를 가지고 무언가를 해야한다
  • 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공한다
  • 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다
  • 모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다

Promise.prototype.then

  • then 메서드는 두 개의 콜백 함수를 인수로 전달받는다
  • 첫 번째 콜백 함수는 프로미스가 fulfilled 상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 비동기 처리 결과를 인수로 전달받는다
  • 두 번째 콜백 함수는 프로미스가 rejected 상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다
  • then 메서드는 언제나 프로미스를 반환한다. 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다

Promise.prototype.catch

  • catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다
  • catch 메서드의 콜백 함수는 프로미스가 rejected 상태인 경우에만 호출된다
  • catch 메서드는 then(undefined, onRejected)과 동일하게 동작한다
  • 마찬가지로 언제나 프로미스를 반환한다

Promise.prototype.finally

  • finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다
  • finally 메서드의 콜백 함수는 프로미스의 성공, 실패와 상관없이 무조건 한 번 호출된다
  • 프로미스의 상태와 상관없이 수행해야 할 처리 내용이 있을 때 유용하다
  • 마찬가지로 언제나 프로미스를 반환한다

프로미스의 에러 처리

  • then, catch를 이용해 에러를 처리할 수 있다
  • 단, then 메서드의 두 번째 콜백 함수가 첫 번째 콜백 함수에서 발생한 에러를 캐치하지는 않는다
  • then이 단 한번만 나온다면 두 번째 콜백함수를 사용해 그 안에서 에러 처리를 해도 괜찮다
  • 하지만 then이 여러 번에 걸쳐 체이닝 되는 경우에는 두 번째 콜백 함수를 쓰지 않고, catch 로 에러를 처리하는게 가독성이 좋다

fetch

  • fetch 함수는 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API다
  • fetch 함수는 프로미스를 지원하기 때문에 비동기 처리가 훨씬 자유롭다
  • fetch 함수는 HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환한다
  • fetch 함수는 Response 객체를 래핑한 프로미스를 반환하므로, then을 통해 프로미스가 resolve한 Response 객체를 전달 받을 수 있다
  • fetch 함수가 반환하는 프로미스는 404와 같은 HTTP 에러가 발생해도 에러를 reject 하지 않고 불리언 타입의 ok 상태를 false로 설정한 Response 객체를 resolve 한다
  • 오프라인 등의 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를 reject 한다
  • 따라서 HTTP 에러는 프로미스가 resolve한 불리언 타입의 ok 상태를 확인해 명시적으로 처리해야 한다

async/await

  • ES8 에서 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await이 도입되었다
  • async/await은 프로미스를 기반으로 동작한다
  • async/await을 사용하면 프로미스의 then/catch/finally과 같은 후속 처리 메서드를 사용하지 않고도 마치 동기 처리처럼 프로미스를 사용할 수 있다

async

  • async는 함수 정의문 앞에 붙는 키워드로, 함수가 비동기적으로 실행되도록 한다. 즉 비동기 함수를 정의할 때 사용하는 키워드이다
  • async 함수는 언제나 프로미스를 반환한다. async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다

await

  • await 키워드는 프로미스가 settled 상태(fulfilled 또는 rejected)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다
  • await 키워드는 반드시 프로미스 앞에서 사용해야 한다
  • await 키워드는 다음 실행을 일시 중지시켰다가 프로미스가 settled상태가 되면 다시 재개한다

에러 처리

  • async/await 에서 에러 처리는 try...catch 문을 사용할 수 있다
  • 콜백 함수를 인자로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다
const foo = async () => {
  try {
    const wrongUrl = 'https://wrong.url';

    const response = await fetch(wrongUrl);
    const data = await response.json();
    console.log(data)
  } catch (err) {
    console.error(err); // TypeError: Failed to fetch
  }
}

foo();
  • catch 문은 HTTP 통신에서 발생한 네트워크 에러뿐 아니라 try 코드 블록 내의 모든 문에서 발생한 에러까지 모두 캐치할 수 있다
  • async 함수 내에서 catch 문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject 하는 프로미스를 반환한다
  • 따라서 async 함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수도 있다
const foo = async () => {

  const wrongUrl = 'https://wrong.url';

  const response = await fetch(wrongUrl);
  const data = await response.json();
  return data
}

foo()
  .then(console.log)
  .catch(console.error) // TypeError: Failed to fetch
[Javascript]: 타이머 함수 [Javascript]: 자바스크립트 이벤트 루프와 콜 스택

You may also like

See all javascript
11 Feb 2024 [Javascript]: 자바스크립트 이벤트 루프와 콜 스택
language
javascript

[Javascript]: 자바스크립트 이벤트 루프와 콜 스택

05 Feb 2024 [Javascript]: 타이머 함수
language
javascript

[Javascript]: 타이머 함수

이웅모님의 모던 자바스크립트 Deep Dive 책을 읽고 정리한 내용이다

02 Feb 2024 [Javascript]: 클래스
language
javascript

[Javascript]: 클래스

Jay Kim

Jay Kim

Web development, data engineering for human for the Earth. I share posts, free resources and inspiration.

Rest
Lifestyle
Hobby
Hobby
Hobby
Hobby
2025 © Jay's Cookbook. Crafted & Designed by Artem Sheludko.