WeniVooks

검색

JavaScript 베이스캠프

비동기 프로그래밍과 Promise

1. 동기와 비동기

비동기 처리 방식이 왜 필요한지 아래 예시를 들어 살펴보도록 하겠습니다. 먼저 동기 방식에 대해 살펴보겠습니다. 동기는 순차적으로 처리되는 방식입니다.

  1. (10시) licat : 로봇 청소기를 돌립니다.
  2. (11시) licat : 빨래를 합니다.
  3. (12시) licat : 설거지를 합니다.
  4. (01시) licat : 요리를 합니다.

위의 예시는 동기 방식으로 처리되는 방식입니다. 순차적으로 처리되기 때문에 로봇 청소기를 돌리고 나서 빨래를 하고, 빨래를 하고 나서 설거지를 하고, 설거지를 하고 나서 요리를 합니다.

반면 비동기 방식은 순차적으로 처리되지 않습니다. 아래 예시를 살펴보겠습니다.

  1. (10시) licat : 로봇 청소기를 돌리면서
  2. (10시) licat : 빨래를 합니다.
  3. (10시) licat : 설거지를 합니다.
  4. (10시) licat : 요리를 하려고 물도 끓입니다.

우리는 당연히 비동기 처리가 효율적이라는 것을 알고 있습니다. 다만 컴퓨터는 항상 모든 일을 순차적으로 진행하려 합니다. 앞에 일이 다 끝나야만 뒤에 일을 하는 것이죠.

카페에서 주문을 받는 아래 이미지를 예로 들어보겠습니다. 만약 주문을 받는 점원이 주문을 받고 모든 음료를 만들어야만 다음 주문을 받는다면 어떻게 될까요? 이런 경우가 없을 것 같지만 code에서는 이런 경우가 많이 발생합니다.

좀 더 구체적으로 코드 예제를 들어보도록 하겠습니다.

console.log('첫 번째 작업 시작');
for (let i = 0; i < 1000000000; i++) {} // 오래 걸리는 작업
console.log('두 번째 작업 시작'); // 중요도 높음
console.log('첫 번째 작업 시작');
for (let i = 0; i < 1000000000; i++) {} // 오래 걸리는 작업
console.log('두 번째 작업 시작'); // 중요도 높음

중요도가 높은 코드가 가장 마지막에 있습니다. 이러한 코드는 아래와 같이 순서를 변경할 수 있습니다.

console.log('첫 번째 작업 시작'); setTimeout(() => { this.querySelector('.codeblock-result').innerText = '시간이 오래 걸리는 작업'; }, 2000); // 2초 후 실행 this.querySelector('.codeblock-result').innerText = '두 번째 작업 시작(중요한 작업)';

이 코드는 2000ms(2초) 후에 '시간이 오래 걸리는 작업'이 출력됩니다. 이것이 바로 비동기 처리 방식입니다. 중요한 작업을 먼저 처리하고, 시간이 오래 걸리는 작업은 뒤에서 처리하는 것이죠. 마치 세탁기를 돌려놓고 요리를 하는 것처럼요. 여기서 2000ms(2초)를 0ms(즉시)로 변경하여 실행해보세요. 그렇게 되어도 시간이 오래 걸리는 작업이 나중에 출력된 것을 확인할 수 있습니다. 작업의 우선순위가 뒤로 밀렸기 때문이죠.

이번에는 실제 프로젝트에서 사용하는 예시를 살펴보도록 하겠습니다. 아래 이미지는 위니브월드라는 서비스입니다. 캐릭터를 움직이며 프로그래밍을 배울 수 있는 툴이죠. 여기서 왼쪽 소스코드와 오른쪽 캐릭터는 비동기로 움직이고, 오른쪽 소스코드는 동기로 움직입니다. 만약 이것이 동기로 움직인다면 어떤 일이 발생될까요? 부드러운 애니메이션은 적용시킬 수 없을겁니다. 코드의 실행 속도는 워낙 빠르니까요.

위니브 월드
위니브월드 Beta

2. Promise

Promise는 비동기 작업을 처리하기 위한 방식입니다. Promise는 비동기 작업의 완료 또는 실패를 나타냅니다. 완료나 실패에 따라 코드를 분기할 수 있어요. 마치 if문처럼요.

Promise는 성공과 실패만을 다룹니다. 중립은 없습니다. Promise는 대기(pending) 상태, 성공(fulfilled) 상태, 실패(rejected) 상태를 가집니다.

이름은 왜 Promise일까요? Promise는 약속을 의미합니다. 약속을 한다는 것은 무엇인가를 약속하고, 그 약속을 지키겠다는 뜻입니다. 어떤 것을 약속하냐면 성공과 실패에 따라 실행할 함수를 약속하는 것이죠. 실행할 함수는 콜백함수라고 합니다. Promise의 상태는 아래와 같습니다.

  • promise의 상태
    • pending(대기상태) - resolve(해결) - fulfilled(성공)
    • pending(대기상태) - reject(거부) - rejected(실패)

Promise는 아래와 같은 형태입니다.

let p = new Promise((resolve, reject) => {
  // 실행코드
});
let p = new Promise((resolve, reject) => {
  // 실행코드
});

그러면 p는 작업이 성공할 수도 있고, 실패할 수도 있습니다. 이에 따라 코드를 분기할 수 있습니다. 성공하면 then을 연달아 실행하고, 실패하면 catch를 사용하여 실패에 대한 코드를 작성할 수 있습니다. 여기서 then으로 그 다음 코드를 실행할 때에는 이전 코드의 결과값(return)을 아규먼트로 넘겨받습니다. 또한 catch로 에러를 잡을 때에는 catch로 넘어가기 전 코드의 에러를 넘겨받습니다.

자, 그러면 코드를 성공시키거나 일부러 실패하게 해보겠습니다. 아래 코드를 한 번 실행시키고 결과를 확인한 다음 resolve를 주석처리하고 reject를 주석 해제하고 실행해보세요.

여기서 주의 깊게 보아야 할 포인트는 단순 코드의 분기가 아니라 앞에 함수가 성공하고 다음 함수로 넘어갈 때 앞에 함수의 return 값이 다음 함수의 인자로 넘어간다는 것입니다.

let p = new Promise((resolve, reject) => { resolve('hello world'); // 성공 // reject('hello world'); // 실패 }) .then((메시지) => { alert(메시지); return 메시지.split(' ')[0]; }) .then((메시지) => { alert(메시지); return 메시지[0]; }) .then((메시지) => { alert(메시지); }) .catch((메시지) => { alert('catch 실행!! :' + 메시지); });

이번에는 then 도중에 애러를 발생시켜 보도록 하겠습니다. 아래 코드를 실행시키고 결과를 확인해보세요.

let p = new Promise(function (resolve, reject) { resolve('hello world'); }) .then((메시지) => { alert(메시지); throw Error('에러 발생!'); return 메시지.split(' ')[0]; }) .then((메시지) => { alert(메시지); return 메시지[0]; }) .then((메시지) => { alert(메시지); }) .catch((메시지) => { alert('catch 실행!! :' + 메시지); });

위 코드는 then에서 에러를 발생시키고 catch에서 에러를 잡아서 처리하는 코드입니다. then에서 에러가 발생하면 중간에 catch로 넘어가게 됩니다. 애러를 만나기 이전 코드는 실행하고 넘어간다는 사실을 기억해주세요. 이 Promise는 ES6에서 추가된 비동기 처리를 위한 방법 중 하나입니다.

콜백함수, 콜백지옥이라는 단어를 많이 만날 것입니다.

ES6이전은 비동기 프로그래밍을 할 때 콜백함수를 사용했었습니다. 콜백함수는 함수의 아규먼트로 함수를 넘겨주는 것을 말합니다. 그리고 나서 나중에(back) 호출(call)하는 것이죠. 콜백함수를 이번장에서 설명하려는 것은 아닙니다. 다만 초급자가 콜백함수나 콜백 지옥을 이해하는 것은 매우 어려운 일이므로 Promise를 이해하기 위해서 콜백함수를 이해하려 노력하는 것보다는 Promise 자체를 이해하려고 노력하는 편이 더 좋습니다. 다만 중급자가 되기 위해 클로저 등의 개념을 이해하기 위해서는 꼭 배워야 하는 개념입니다. 이번장에서는 Promise만을 중점적으로 다루도록 하겠습니다.

10장 비동기 프로그래밍10.2 fetch와 async, await