javascript- 비동기적 처리&promise

현우.·2024년 6월 19일

JavaScript

목록 보기
21/31
post-thumbnail

동기적 처리

동기적 처리란 코드를 순서에 맞게 절차적으로 처리하는 것으로
특정 부분에서 시간이 오래걸리는 일을 수행하면 수행속도가 느려지는 단점이 있다.

  • 자바스크립트 런타임 환경에는 자바스크립트 엔진이 있고 그 안에는 memeory heap, call stack, queue, WEB APIS등 다양한 것이 존재한다.
  • call stack에는 호출된 함수들이 순차적으로 쌓인다.
  • call stack은 한번에 하나의 작업만 처리하는 싱글 컨텍스트 스택(싱글 스레드)이다.

코드 📄

function a(){
    return 1;
}

function b(){
  for(let i=0;i<100000000000000000;i++) // 시간이 굉장히 오래걸린다.
    return a();
}
function c(){
    return b();
}

const result =c();
console.log(result); // 1

call stack: c() -> b() -> a()
b 함수의 처리가 끝날 때까지 a는 호출되지 못해 시간이 굉장히 오래 걸린다.


비동기적 처리

비동기적으로 처리란 일을 병렬적으로 처리한다는 것이다.

  • 어떤 일이 종료되지 않았더라도 대기하지 않고 바로 다음 일을 수행한다.
  • 비동기적으로 처리하기 위해 호스트 환경에서 제공해주는 다양한 API(Web APIs,Node APIs)들을 사용할 수 있다.
    • Web APIs에는 DOM APIS, setTimeout, setInterval, fetch, event listener등이 있고 전부 비동기적으로 처리된다.
      - Ajax요청 또한 비동기적으로 처리된다.
  • setTimeout의 수행이 끝나면 setTimeOut의 콜백함수가 task queue에 들어가고 event loop에 감시되다 call stack이 비워지면 그때 callback함수가 들어간다.

코드 📄

 function run(){
        console.log("start");
        setTimeout(()=>{    
            console.log("stop");        
        },3000);
        console.log("running");
    }

 run();

알고리즘(비동기적 수행)
1. run()이 호출되면 'start'가 출력된다.
2. setTimeout함수는 node API에게 3초뒤에 콜백함수를 던져달라고 한다음 넘어간다.
3. 'running'이 출력된다.
4. Node API는 3초뒤에 콜백함수를 task queue에 전달한다. event loop는 call stack이 비워져있는것을 확인하고 그때 콜백함수는 call stack으로 들어와 출력된다.

비동기적 처리의 일반적인 접근

자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다

비동기적으로 처리해야 하는 함수가 있을 때 그 함수는 콜백함수를 인수로 반드시 줘야 한다.

콜백 기반 비동기적 처리

코드 📄

function run(){
    try{
        console.log("start!");
        delaySeconds(()=>console.log("finished!"),2)
        console.log("not finished");
    }catch(error){
        console.log(error.message);
    }
}

function delaySeconds(callback,seconds){
    if(!seconds|| seconds<0|| isNaN(seconds)){
        throw new Error('시간 설정이 이상해');
    }
   return  setTimeout(callback,seconds*1000);
}

run();
  • delaySeconds는 setTimeout 함수와 유사하지만 지연 시간을 seconds로만 받는 함수, 비동기 처리 함수
  • run 함수가 호출되어 수행되다 delaySeconds 함수를 만난다.
  • 에러가 발생하지 않으면 setTimeout을 return. 비동기적 처리 시작하고 다음 코드로 넘어간다.
  • 에러가 발생하면 try문은 멈추고 catch문으로 이동해서 코드 수행

콜백 패턴의 단점

전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리중 발생한 에러를 처리하기 곤란해 여러 비동기처리를 한번에 처리하기가 어렵다.

콜백 헬

비동기적 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러개의 콜백 함수가 중첩되어 복잡해지는 것을 말한다.

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
        
          }
        });
      }
    })
  }
});

코드 동작
1. 1.js를 로드한다. 그 후 에러가 없으면
2. 2.js를 로드한다. 그 후 에러가 없으면
3. 3.js를 로드한다. 그 후 또 다른 작업을 수행한다.

에러 처리의 한계

콜백 방식으로 비동기 처리를 했을 때 에러처리를 할 수 없다.

아래 코드를 보자

try {
    setTimeout(() => { throw new Error('Error!'); }, 1000);
  } catch (e) {
    console.log('에러를 캐치하지 못한다..');
    console.log(e);
  }

해석

  • 비동기처리 방식의 콜백함수는 timer함수같은 이벤트가 발생하면 task queue로 이동하고 호출 스택이 비워지면 호출 스택으로 이동되어 실행한다.

  • setTimeout은 비동기 처리 함수이므로 콜백함수가 실행되기까지 기다리지않고 실행했다 종료되어 호출스택에서 제거된다.

  • 즉 콜백함수가 호출스택으로 이동할때는 이미 setTimeout은 호출스택에서 제거된 상태인데 이는 setTimeout의 콜백함수의 호출자가 setTimeout이 아니라는 뜻이다.

  • 콜백함수에서 에러가 발생하면 예외처리는 콜백함수의 호출자 방향으로 전파되는데
    setTimeout의 콜백함수의 호출자가 setTimeout이 아니므로 예외처리를 할 수가 없다.

이러한 문제들을 해결하기위해 Promise가 나오게 되었다.

promise

promise는 비동기적으로 수행한 결과(성공 or 실패)를 알려주는 객체이다.

  • promise는 생성자 함수를 통해 인스턴스화한다.
  • promise 생성자함수는 비동기작업을 수행할 콜백함수(resolve,reject)를 인자로 받는다.

promise의 상태

promise의 상태에는 pending, fulfilled, rejected가 있다.

  • pending: 이제 막 promise가 생성되어 일이 끝나지 않은 상태
  • fulfilled: 비동기적 코드가 성공적으로 끝난 상태
  • rejected: 비동기적 코드가 실패로 끝난 상태

promise 기반 비동기식 처리

제작 코드 📄


function delaySeconds(seconds){	
   return  new Promise(function(resolve, reject) { 
        // 제작 코드
         if(!seconds|| seconds<0|| isNaN(seconds)){
            reject(new Error('시간 설정이 이상해'))
        }
        setTimeout(()=>resolve('완료!'),seconds*1000);

      });
}

promise 제작 코드의 특징

  • promise를 반환하는 함수는 콜백함수를 인자로 받지 않아 간결하다는 특징이 있다.
  • promise 객체는 보통 new 연산자와 함께 만들고 생성자 안에는 콜백함수를 만들어야 한다.
  • 콜백함수는 보통 무언가 비동기적으로 수행하고, 시간이 지난후에 resolve,reject를 호출한다.
  • resolve, reject는 promise의 콜백 함수의 인자이며 제작 코드안에서 둘중하나는 반드시 호출되어야 한다!
  • resolve,reject의 인자로 비동기 처리 결과를 전달한다.

소비 코드 📄

delaySeconds(2)
// 소비 코드
.then(result => console.log(result))
.catch(console.log)
.finally(()=>console.log('finished!'));

promise 소비 코드의 특징

  • then은 promise가 성공적으로 수행되었을 때 resolve()에 있는 값을 인자로 전달 받는다.
  • catch는 promise가 실패 했을 때 reject() 에 있는 값을 인자로 전달받는다.
  • finally는 에러 발생과 상관없이 호출된다.
  • 성공하는 케이스에 대해서만 처리하려면 catch,finally 생략가능하다.

Promise chaining

순차적으로 처리해야 하는 비동기 작업이 여러 개 있을 때 프로미스 체이닝을 이용하면 해결할 수 있다.

코드 📄

function printNumber(ms){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>resolve(1),ms);
    })
}

printNumber(1000) // 1초 기다렸다가 promise 반환값 then에 전달
.then(result => result*2)
.then(result2 => result2*2)
.then(result3 =>result3*2)
.then(finalResult => console.log(finalResult));

promise chaining이 가능한 이유는 then, catch, finally를 호출하면 promise가 반환되기 때문이다.

핸들러가 promise를 return하는 경우

function printNumber(ms){
    return new Promise((resolve)=>{
        setTimeout(()=>resolve(1),ms);
    })
}

function multiply2(result,ms){
    return new Promise((resolve)=>{
        setTimeout(()=>resolve(result*2),ms);
    })
}

printNumber(1000) // 1초 기다렸다가 promise 반환값 then에 전달
.then(result => multiply2(result,1000)) 
.then(result2 =>multiply2(result2,1000))
.then(result3 =>multiply2(result3,1000))
.then(finalResult => console.log(finalResult)); //8 

then 핸들러가 promise를 반환하는 함수(multiply2)를 반환하고 다음 then 핸들러가 반환 값을 받기 때문에 promise chaining된다.


Error handling

코드📄

const kid = new Promise((resolve,reject)=>{
        setTimeout(()=>resolve('👲'),1000);
})
const student = (kid)=>new Promise((resolve,reject)=>{
        setTimeout(()=>reject(new Error(`${kid}=>👦`)),1000);
    })

const adult= (student)=> new Promise((resolve,reject)=>{
        setTimeout(()=>resolve(`${student}=>👨`),2000);
    })


kid
.then(kid=>student(kid))
.catch(error=>{
    return '👶';
})
.then(adult)
.then(console.log)

출력📌


Promise의 정적 메소드

Promise 클래스에는 5개의 정적 메소드가 있다.

Promise.resolve(), reject()

resolve(), reject() 메소드를 사용하면 Promise 객체를 생성할 필요 없이
즉각적으로 promise가 성공 ,실패했을 때 결과를 반환할 수 있다.

코드 📄

function getApple(){
    return new Promise((resolve)=>{
        setTimeout(()=>resolve('🍎'),3000)
    })
}
function getBanana(){
    return new Promise((resolve)=>{
        setTimeout(()=>resolve('🍌'),2000)
   })

}
function getStrawberry(){
    return Promise.reject(new Error('딸기는 없습니다.'));

}

getApple()
.then(apple => getBanana()
.then(banana => console.log(apple + banana)));  
//  코드 실행후 기본 promise가 5초뒤에 출력 🍎🍌

Promise.all([])

병렬적으로 한번에 모든 Promise들을 수행한다.

코드 📄

Promise.all([getApple(),getBanana()])
.then(result => console.log('all',result));  
//  코드 실행후 all 메소드가 3초뒤에 출력  all [ '🍎', '🍌' ]

Promise.all은 요소 전체가 프라미스인 배열을 받는다.

코드 📄

Promise.all([getApple(),getBanana(),getStrawberry()])
.then(result => console.log('all-error',result))  // 실행 x
.catch(result => console.log('all-error',result));
// all-error Error: 딸기는 없습니다.

promise를 받아올 함수들중 에러가 발생하는 함수가 있을경우 catch를 써줘야한다.
그렇지 않으면 Promise.all이 반환하는 프라미스들은 에러와 함께 바로 거부된다.

Promise.race()

주어진 promise중에서 제일 빨리 수행된것이 처리된다.

코드 📄

Promise.race([getApple(),getBanana()])
.then(result => console.log('race',result));  
//  코드 실행후 race 메소드가 2초뒤에 출력  race 🍌

Promise.allSettle([])

성공하든 실패하든 결과를 출력

코드 📄

Promise.allSettled([getApple(),getBanana(),getStrawberry()])
.then(result => console.log('settle',result));  
/*
  settle [
  { status: 'fulfilled', value: '🍎' },
  { status: 'fulfilled', value: '🍌' },
  {
    status: 'rejected',
    reason: Error: 딸기는 없습니다.
        at getStrawberry (C:\Users\82105\Desktop\khw\projects\javascript\practice.js:18:27)
        at Object.<anonymous> (C:\Users\82105\Desktop\khw\projects\javascript\practice.js:35:44)
        at Module._compile (node:internal/modules/cjs/loader:1358:14)
        at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
        at Module.load (node:internal/modules/cjs/loader:1208:32)
        at Module._load (node:internal/modules/cjs/loader:1024:12)
        at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12)
        at node:internal/main/run_main_module:28:49
  }
]
  */
profile
학습 기록.

0개의 댓글