[ 자바스크립트 ] 비동기 Promise

김수연·2022년 10월 1일
0
post-thumbnail

함수와 콜백

function str(){
    console.log('I \'m string');
}

console.log(1);
str(); 
console.log(2);
console.log(3);

본래 코드는 위에서 아래 순서로 실행된다.

함수 str은 코드 가장 위에 정의되어 있지만 실행되지 않고 있다가
1이 출력된 이후 호출되어 문자열을 뱉어낸다.

1
I'm string
2
3

다음 예제도 살펴보자

function b() {
  console.log("b called!");
}

function a(another) {
  console.log("a started!");
  another();
  console.log("a ended!");
}

console.log(1);
a(b);
console.log(3);
1
a started!
b called!
a ended!
3

a함수는 another 인자를 받아서 함수처럼 호출하고 있다
이처럼 a함수를 호출할 때 b함수를 전달하는 것을 '콜백함수를 전달한다' 고 한다.

종합해보자면 함수는

1. 선언과 호출의 시점을 다르게 해서 코드의 실행 흐름을 조작하고
2. 인자로 함수를 받아 실행 할 수 있다


let i = 0;
function decryptData(callback, wait, age) {
  let start = new Date().getTime();
  while (new Date().getTime() < start + wait); 
  callback(`${age}세 은행원`);
}

fetchData(console.log, 1000, 20);
fetchData(console.log, 1000, 37);
fetchData(console.log, 1000, 59);

자 이제부터 익명의 직원 3명의 개인정보를 가져오는 작업을 한다고 가정해보자
정보는 암호로 되어있어 하나의 정보를 해독 후에 가져오는데 시간 1초가 꼭 걸린다고 한다면
한명의 직원 정보를 가져오는 동안 다른 직원의 정보를 동시에 가져오는 일은 할 수 없다. 즉 3초를 꽉 채워 기다려야 한다는 뜻이다.

하지만 해독을 다른 누군가가 대신 해주고 나는 답변을 기다리기만 한다면,
또 3대의 컴퓨터로 3명의 정보를 동시에 받아오면 어떨까?


비동기 - 동시에 여러개 작업

function finishJob(num) {
  console.log(`${num}번 요원의 정보를 받아왔습니다.`);
}

setTimeout(finishJob, 2000, 1);
setTimeout(finishJob, 1500, 2);
setTimeout(finishJob, 1000, 3);
console.log("정보 요청을 모두 보냈습니다.");
정보 요청을 모두 보냈습니다.
3번 요원의 정보를 받아왔습니다.
2번 요원의 정보를 받아왔습니다.
1번 요원의 정보를 받아왔습니다.

위 코드는 3명의 정보를 동시에 받아서 정해진 시간 뒤에 출력하고 있다.
그런데 결과값이 조금 이상하다.
마지막 console.log 함수 결과가 가장 먼저 출력되었다. 어떻게 된 일일까?

setTimeout ( ) 때문이지

setTimeout(기다린 후 호출할 콜백함수, 기다릴 밀리초, 콜백함수에 넘길 인자(옵션))
  1. setTimeout 은 기다리는 함수로 일정한 ms 뒤에 콜백함수를 호출한다.
  2. 그러나 setTimeout 함수 자체는 '~~ms 뒤에 이 함수를 호출해'하고 예약만 한 후에 바로 함수를 끝내버린다.
  3. 이때 기다리지 않는 console.log는 즉식 실행되어 가장 상단에 출력되고
  4. 콜백 함수들은 예약된 시간이 되면 finishJob을 호출해서 문자열을 출력하기 때문에 위와 같은 결과가 나오는 것이다.

위를 아래와 같이 표현해보면 setTimeout이 비동기 방식임을 알 수 있다.
-> 예약이 끝나자마자 동시에 각자의 예약 시간을 기다리고, 이때 하나의 콜백함수가 다른 콜백함수의 기다림에 영향을 주지 않기 때문이다.

setTimeout(callback1, 1000ms, arg (옵션)) -> 함수 끝 , 예약됨
setTimeout(callback2, 1500ms, arg (옵션)) -> 함수 끝 , 예약됨
setTimeout(callback3, 2500ms, arg (옵션)) -> 함수 끝 , 예약됨
 	|			  |	 	  	     |				|
 'hi' 출력	   	  |         	 |	 	  	    |
 			   callback1		 |	 	  	    |	
 			    1000ms		  callback2 		|	
                              	1500ms			|
                                             callback3
                                              2500ms

📌 비동기 - 문제점

위에서 본 것처럼 비동기 작업은 한번에 여러 작업을 동시에 수행할 수 있는 장점이 있지만, 의존성이 길게 이어져 있는 경우 일이 복잡해진다

왜냐하면 비동기 작업은 함수가 호출되는 시점에 시작되고 이때 다음 작업(콜백함수)도 넘겨줘야 하기 때문이다.


Promise

기본적인 형태부터 알아본 후 사용 예제까지 살펴보자.

const promise1 = new Promise((resolve,reject) => {
	// 비동기 작업
}) 
  • const로 선언해서 재할당이 되지 않도록 하면 하나의 변수로 끝까지 관리하면 가독성이 좋고 유지보수에 용이하다.
  • new Promise 생성자 함수로 새로운 promise1 객체를 생성한다.
  • 특별한 함수(화살표 함수) 하나를 인자로 받는다.

📌 executor

  • promise 생성자 함수가 받는 특별한 함수를 executor 라고 한다.
  • executor는 인자 2개를 받는데 하나는 resolve, 다른 하나는 reject이다.
  • 이 두개의 인자 역시 함수이고 비동기 작업의 성공 / 실패를 표현한다.
  • resolve -> 비동기 작업 성공
    reject -> 비동기 작업 실패

비동기 작업은 언제 끝날 지 알 수 없기 때문에 성공, 실패에 따른 후작업까지
해 주어야 한다.

  • resolve -> then 으로 성공 후 작업 연결
  • reject -> catch 로 오류나 실패를 잡아냄

📌 then, catch

Promise 작업이 끝난 뒤의 후속작업을 처리하는 메소드이다.

then : 비동기 작업이 성공하면 실행할 동작 지정, 함수를 인자로 받음
catch : 비동기 작업이 실패하면 실행할 동작, 함수를 인자로 받음

const promise1 = new Promise((resolve, reject) => {
  resolve(); // reject();
});

promise1
  .then(() => {
    console.log("then!"); 
  })
  .catch(() => {
    console.log("catch!");
  });

결과

then! //catch!

위에서 설명한 것처럼 resolve는 then으로 처리, reject는 catch로
처리되는 것을 볼 수 있다.

📌 재사용

비동기 작업을 수행할 때마다 new Promise 를 이용해 객체를 만들기는 번거롭다.
이럴땐 new Promise 의 결과를 바로 리턴하는 함수를 만들어 재사용하면 된다.

function startAsync(age) {
  return new Promise((resolve, reject) => {
    if (age > 20) resolve();
    else reject();
  });
}

setTimeout(() => {
  const promise1 = startAsync(25);
  
  promise1
    .then(() => {
      console.log("1 then!");
    })
    .catch(() => {
      console.log("1 catch!");
    });
  
  const promise2 = startAsync(15);
  
  promise2
    .then(() => {
      console.log("2 then!");
    })
    .catch(() => {
      console.log("2 catch!");
    });
  
}, 1000);
  • setTimeout() 은 1000밀리초 뒤에 { } 안의 내용을 실행한다.
  • 2개의 변수는 25, 15 인자로 보내 startAsync 함수를 호출한다.
  • 호출하는 순간 new Promise 가 실행되어 비동기 작업이 시작된다.
  • then 혹은 catch 로 후속 작업을 처리한다.

결과

promise1 -> resolve ( ) -> then -> // 1 then!
promise2 -> reject ( ) -> catch -> // 2 catch!

📌 작업 결과 전달

위에서 startAsync를 호출하여 변수를 만들 때 인자를 전달했었다.
이 인자를 new Promise 에서 resolve, reject 일 때 각각 다르게
처리해 then, catch 에 전달할 수 있다.

function startAsync(age) {
  return new Promise((resolve, reject) => {
    if (age > 20) resolve(`${age} success`);    
    else reject(new Error(`${age} is not over 20`));
  });
}

setTimeout(() => {
  const promise1 = startAsync(25);
  
  promise1
    .then((value) => {
      console.log(value);
    })
    .catch((error) => {
      console.error(error);
    });
  
  const promise2 = startAsync(15);
  
  promise2
    .then((value) => {
      console.log(value);
    })
    .catch((error) => {
      console.error(error);
    });
  
}, 1000);

결과

25 success
Error: 15 is not over 20
    at /home/taehoon/Desktop/playground-nodejs/index.js:4:17
    at new Promise (<anonymous>)
    at startAsync (/home/taehoon/Desktop/playground-nodejs/index.js:2:10)
    at Timeout._onTimeout (/home/taehoon/Desktop/playground-nodejs/index.js:17:20)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

📌 기타사항

executor 를 만들 때 조금 더 알아두면 좋은 점들을 살펴보자


  1. executor 내부에서 에러가 throw 되면 reject가 수행됨
const throwError = new Promise((resolve, reject) => {
  throw Error("error");
});
throwError
  .then(() => console.log("throwError success"))
  .catch(() => console.log("throwError catched"));
  1. executor 의 리턴 값은 무시된다.
// 아무런 영향이 없습니다.
const ret = new Promise((resolve, reject) => {
  return "returned";
});
ret
  .then(() => console.log("ret success"))
  .catch(() => console.log("ret catched"));
Promise {<pending>} // 아직 아무 값도 받지 못함, (성공 / 실패) 여부 불확실
  1. 첫번째 reject 혹은 resolve 만 유효하다.(throw 역시 이전 함수가 호출되면 무시됨)
// resolve 만 실행
const several1 = new Promise((resolve, reject) => {
  resolve();
  reject();
});
several1
  .then(() => console.log("several1 success"))
  .catch(() => console.log("several1 catched"));

// reject 만 실행
const several2 = new Promise((resolve, reject) => {
  reject();
  resolve();
});
several2
  .then(() => console.log("several2 success"))
  .catch(() => console.log("several2 catched"));

// resolve 만 실행
const several3 = new Promise((resolve, reject) => {
  resolve();
  throw new Error("error");
});
several3
  .then(() => console.log("several3 success"))
  .catch(() => console.log("several3 catched"));

결과

// 위의 코드들 결과(순서대로)

throwError catched
several1 success
several2 catched
several3 success

어차피 첫번째 resolve, reject만 영향을 주기 때문에 해당 함수가 호출되면 리턴해서 비동기 작업을 빠져나가는 것이 좋다.

위에서 봤던 startAsync를 조금 바꿔보자

function startAsync(age) {
  return new Promise((resolve, reject) => {
    
    if (age > 20) {
      return resolve(`${age} success`);
    }
    
    return reject(new Error(`${age} is not over 20`));
    
    // 이 아래의 코드들은 이제 실행되지 않는다!
  });
}

(위의 코드는 무언가를 기다리는 작업은 하고 있지 않다 단순예제로만 보기를!)

  • 비동기 작업이 성공하면 resolve가 실행되면서 then으로 정보를 보낸 후 리턴되고 다음 코드가 실행되지 않는다.
  • 비동기 작업이 실패하면 reject를 실행, catch로 정보를 보낸 후 리턴되고 역시 이 다음의 코드들은 실행되지 않는다.

정리해보자면

  • 함수는 코드가 적힌 순서를 떠나 내가 원할때 코드 조각을 불러와 실행 할 수 있다.
  • 콜백은 이런 함수를 인자로 보내 함수 실행의 권한을 다른 함수에 넘기는 것을 말한다.
  • 비동기 작업은 이런 함수 여러개가 동시에 실행되고 언제 결과가 나올 지 알 수 없고
  • 비동기 작업이라도 서로 의존성이 있는 경우 곤혹을 치를 수 있다.
  • 이런 비동기 작업의 문제를 해결하기 위해 Promise 를 이용하고
  • 언제끝날지 모르는 비동기 작업의 후속처리를 then,catch로 처리할 수 있다.

출처:

https://elvanov.com/2597 - [Javascript] 비동기, Promise, async, await 확실하게 이해하기

profile
길을 찾고 싶은 코린이 of 코린이

0개의 댓글