본문 바로가기
javascript

자바스크립트 비동기 처리 - callback 과 promise

by 윤-찬미 2020. 11. 10.

서론

자바스크립트에서 중요한 개념인 비동기처리

오늘은 자바스크립트에서 비동기처리를 왜 해야하며 비동기처리를 어떻게 하는지 알아보도록 하겠습니다.

비동기 왜 중요할까?

비동기의 중요성을 설명할때 굉장히 많이 사용되는 짤인데 사실 이게 전부입니다.

"앞전의 처리가 완료 될때까지 기다려야 한다니.."

예를 들어 서버에서 데이터를 가져와서 화면에 표시하는 작업을 수행할 때, 서버에 데이터를 요청하고 데이터가 응답될 때까지 이후 태스크들은 블로킹(blocking, 작업 중단)됩니다.

클라이언트에서 서버로 데이터를 요청 했을 때, 서버가 그 요청에 대한 응답을 언제 줄지도 모르는데 마냥 기다릴 수 없고, 기다려야 한다면 사용자는 해당 어플리케이션을 사용하지 않을 것입니다.

그래서 우리는 비동기 처리를 반드시 배워야만 합니다!

비동기처리란?

자바스크립트의 비동기 처리란 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 자바스크립트의 특성을 의미합니다.

자바스크립트에서의 비동기 처리 방법

자바스크립트에서 비동기 처리를 하는 방법은 크게 두가지로 나눌 수 있습니다.

1) callback

2) promise

1 - (1) callback

function requestData(callback){
  setTimeout(()=>{
    callback({name:'abc',age:23});
  },1000);
}

function onSuccess(data){
  console.log(data);
}

console.log('call requestData');
requestData(onSuccess);

callback 을 사용하여 1초 후에 {name:'abc',age:23} 을 출력하는 비동기 코드를 작성해 보았습니다.

1 - (2) callback의 단점

콜백 지옥은 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제입니다.

function requestData1(callback){
  //..
  callback(data);
}
function requestData2(callback){
  //..
  callback(data);
}

function onSuccess1(data){
  console.log(data);
  requestData2(onSuccess2);
}

function onSuccess2(data){
  console.log(data);
  // ...
}
requestData1(onSuccess1);

위의 코드를 한번 살펴 보겠습니다.

간단한 코드인데도 불구하고 코드의 가독성이 떨어지고 다소 복잡해 보입니다.

$.get('url', function(response) {
    parseValue(response, function(id) {
        auth(id, function(result) {
            display(result, function(text) {
                console.log(text);
            });
        });
    });
});

ajax 코드 출처 : joshua1988.github.io

2 - (1) Promise

** 이러한 비동기 코드 작성시의 복잡도와 콜백지옥을 해결하고자 할 때 Promise를 쓰시면 됩니다.**

promise의 가장 큰 특징은 비동기 처리를 동기코드를 작성하듯 작성할 수 있다는 점입니다.

위에서 복잡하게 썼던 코드를 Promise를 사용하여 처리 해보도록 하겠습니다.

requestData1()
.then(data => {
  console.log(data);
  return requestData2();
})
.then(data => {
  console.log(data);
  //..
})

위 코드에서는 우선 비동기 함수를 호출하고 있고,

그 처리가 끝나면 데이터를 받아서 필요한 처리를 합니다.

그리고 두번째 함수를 호출하고 그리고 두번째 처리가 끝나면 데이터를 받아서 또 필요한 처리를 합니다.

더 자세히 알아보도록 하겠습니다.

2 - (2) Promise의 상태

promise객체는 3개의 상태값을 가집니다.

1)Pending(대기 중)

2)Fulfilled(성공)

3)Rejected(실패)

그리고 Fulfilled 와 Rejected상태를 합쳐 Settled 라고 부릅니다.

requestData().then(onResolve,onReject)
Promise.resolve(123).then(data => console.log(data));
Promise.reject('error').then(null,data => console.log(data));

promise의 상태값에 따라 then메서드에서 실행되는 함수가 달라집니다.

첫번째로 들어간 함수는 fulfilled 상태일 때 실행이 되고,

두번째로 들어간 함수는 reject 상태일 때 실행이 됩니다.

2 - (3) then메서드는 항상 Promise객체를 반환한다.

function requestData1(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve(10);
    },1000)
  });
}
function requestData2(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve(20)
    },1000)
  })
}

requestData1()
.then(data => {
  console.log(data);
  return requestData2();
})
.then(data =>{
  console.log(data);
  return data+1;
})
.then(data => {
  console.log(data);
  throw new Error('some Error');
})
.then(null,error => {
  console.log("error");
})
.then(data => {
  console.log(data);
})

promise는 then메서드를 체인처럼 연결해 사용가능합니다.

이는 then메서드가 항상 prmise객체를 반환하기 때문입니다.

requestData를 호출하고 난 후 첫번째 then이 실행되는데요. data를 받아 console.log를 한후 두번째 함수를 실행시키고

두번째 함수에서 promise객채를 반환합니다.

그 후 data에서 받은 값에 +1 을 하고 return 하는데 이때는

data를 가진 promise객체가 반환됩니다.

세번째 then에서는 rejected상태이기 때문에 4번째 then에 두번째에 들어간 함수가 실행이 됩니다.

그리고 마지막엔 undefined를 데이터로 하여 프로미스 객체를 반환하기 때문에 undefined가 출력이 됩니다.

2 - (4) Rejected상태일 때 catch로 처리하기

Promise.reject(1).then(null,error => {
  console.log(error);
});

Promise.reject(1).catch(error => {
  console.log(error);
});

rejected상태일 때 처리하는 방법엔 아까 설명했던 대로 then의 두번째 인자로 처리하는 방법과

catch메서드를 사용하는 방법이 있습니다.

예외처리를 하는경우 then의 두번째 인자로 처리하는 방법 보다는 catch메서드를 사용하는 것을 추천 드립니다.

아래 코드는 joshua1988.github.io님이 작성해 놓으신 예제를 가져와보았습니다.

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
  console.log('then error : ', err);
});

getData()함수의 프로미스에서resolve()메서드를 호출하여 정상적으로 로직을 처리했지만,then()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못합니다. 따라서 코드를 실행하면 아래와 같은 오류가 납니다.

// catch()로 오류를 감지하는 코드
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function(err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
});

하지만 똑같은 오류를catch()로 처리하면 다른 결과가 나옵니다.

따라서, 더 많은 예외 처리 상황을 위해 프로미스의 끝에 가급적 catch()를 붙이시기 바랍니다.

2 - (5) Finally 메서드

프로미스 객체에는 finally 라는 메서드가 있습니다.

finally는 fulfiled와 rejected 상태를 모두 처리 할 수 있습니다.

특징은 데이터는 넘어오지 않는다는 것입니다.

그리고 이전에 있던 promise객체를 그대로 반환 합니다. finally내부에서 반환하는 retun 값은 상관 없이 말이죠.

2 - (6) 각각의 비동기 처리가 병렬로 처리되지 않는 단점을 Promise.all로 해결!

requestData1()
  .then(data => {
    console.log(data);
    return requestData2();
  })
  .then(data => {
    console.log(data);
  })

위의 코드같은 경우 두개가 병렬로 처리 되지 않기에 2초라는 시간을 기다려야 결과값을 얻을 수 있습니다.

두 함수간에 의존성이 없다면 병렬로 처리되는게 빠를겁니다.

이런경우에는 Promise.all 로 해결 가능합니다.

Promise.all([requestData1(),requestData2()]).then(([data,daya2])=>{
  console.log(data1,data2);
})

단, 입력된 모든 Promise객체가 fulfiled상태가 되어야 반환하는 Promise객체도 fulfiled상태가 됩니다.

만약 하나라도 rejected상태이면 Promise객체가 반환하는 promise객체 또한 rejected 상태가 됩니다.

2 - (7) 가장빨리 sattled된 Promise를 반환 하는 Promise.race

Promise.race 는 입력받은 Promise 객체중에 가장빨리 sattled된 Promise를 반환하는 함수입니다.

Promise.race([
  requestData(),
  new Promise((_,reject) =>  setTimeout(reject,3000)),
])
  .then(data => console.log('fulfilled',data))
  .catch(error => console.log('rejected'));

2-(8) then 메서드는 기존 객체를 수정하지 않고 새로운 promise 객체를 반환한다

then 메서드는 기존객체를 수정하지 않고 새로운 promise객체를 반환합니다.

이를 인지하지 못하면 다음과 같은 실수를 할 수 있습니다.

function requestData(){
  const p = Promise.resolve(10);
  p.then(data => {
    return data + 20;
  });
  return p;
}
requestData().then(v => {
  console.log(v);
})

이 코드를 작성하신 분은 아마  console.log 에 찍히는 값이 30이 되기를 원했을 겁니다.

하지만 이를 콘솔에 찍어보면 결과값은 10이 나옵니다.

then 메서드는 기존객체를 수정하지 않고 새로운 promise객체를 반환하기 때문입니다.

이를 30이 나오게끔 수정하려면

function requestData(){
  const p = Promise.resolve(10);
  return p.then(data => {
    return data + 20;
  });
}
requestData().then(v => {
  console.log(v);
})

이렇게 작성을 해주는 것이 맞습니다.