[동기적 처리와 비동기적 처리] Callback, Promise

김태훈·2022년 9월 25일
0

1. 동기적 처리와 비동기적 처리란?

  • 동기(Syncronous) : 요청을 보내고, 요청의 응답을 받은 후 다음 요청을 처리하는 방식
  • 비동기(Asyncronous) : 요청을 보내고, 해당 요청의 응답을 받지 않음에도 다음 요청을 처리하는 방식

2. 자바스크립트의 방식

대부분의 프로그래밍 언어는 동기적 처리의 방식을 따른다.
하지만 자바스크립트는, 비동기적인 처리'도' 가능하게끔 설정되어 있다.
그러니까, '필요'시에 비동기적 처리 방식을 따른다고 이해하자.

3. 자바스크립트가 비동기적 처리 방식을 필요로 할 때

  • Ajax Web API 요청 (서버 쪽에서 데이터를 받아올 때)

  • 파일,데이터베이스 읽기 (서버 쪽에서 파일을 읽어와야 할 때)

  • 암호화/복호화

  • 작업 예약 (몇 초 후에 해야 되는 작업이 필요할 때)

<대표적인 비동기적 처리 방식의 예>

  • setTimeout() 함수
    이 함수는 작업을 예약하는 함수다. 다음의 코드를 보고 이해해보자.
console.log("바로 나오는 로그");
setTimeout(() => {
  console.log("setTimeout시 나오는 로그");
}, 5000);
console.log("바로 나오는 로그2");

다음의 코드를 보면, setTimeout에 인자에 함수가 들어가 있어서 저렇게 생겨먹었다.
첫 번째 인자로 'setTimeout시 나오는 로그'를 콘솔창에 출력하는 함수를 주었고,
두 번째 인자로, 몇 초 뒤에 실행할 것인지에 해당하는 시간을 milliseconds 단위로 표시하였다. 따라서, 함수는 5초 뒤에 콘솔창에 찍힌다.
원래대로 라면, '바로 나오는 로그' -> 'setTimeout시 나오는 로그' -> '바로 나오는 로그2'가 순서대로 나와야 하지만, 'setTImeout시 나오는 로그' 가 마지막으로 나왔으므로 비동기적 처리 방식을 사용했다고 할 수 있다.

4. 비동기 처리 방식의 문제

비동기 처리 방식을 사용하면, 백엔드(서버,데이터베이스)에서 정보를 가져올 때, 이는 비동기적 처리 방식을 따르기 때문에, 아직 서버에서 정보를 가져오지 않았음에도 관련된 코드가 실행되는 경우가 있을 수도 있다.

5. 비동기 처리 방식의 문제의 해결 방법

- 1. callback 함수

console.log("바로 나오는 로그");
function solveFunc(callback) {
  setTimeout(() => {
    console.log("setTimeout시 나오는 로그");
    callback();
  }, 3000);
}
function solveFunc2() {
  console.log("바로 나오는 로그2");
}

solveFunc(solveFunc2);

callback함수가 무엇인지부터 알아보자.

[stackoverflow] 에서 정의한 callback 함수

  • passed as an argument to another function
  • invoked after some kind of event.
    즉, 다른 함수의 인자로 사용되거나, 이벤트가 진행된 뒤 (예를 들어 네트워크 연결) 에 실행되는 함수이다.

<코드 분석>
이제 비동기적 처리 방식을 순서대로 만들어 보자.
이 코드는 1번정의로 사용되었다고 할 수 있다.

solveFunc(solveFunc2)

여기서 주의할 점은 solveFunc2()로 쓰면 안된다. 이는 solveFun2를 먼저 실행시키기 때문이다.
setTimeout은 두번쨰 인자로 주어진 시간만큼(milliseconds) 뒤에, 첫 번째 인자의 함수를 호출하는데, 첫 번째 함수는 console.log("setTimeout시 나오는 로그") 를 호출하고 callback함수를 호출한다. 이때 callback함수는 동기화적으로 처리하고 싶은 함수를 불러오므로, 결과적으로 비동기적 처리방식을 동기적으로 바꿨다고 할 수 있다.

하지만 callback함수는 콜백지옥 (무분별한 콜백) 을 야기한다. 이를 해결하는 것이 Promise 문법이다.

- 2. Promise

이 문법은 두 단계로 구현이 되는데,
1. new 로 해당 Promise의 객체를 선언하고,
2. 선언한 객체에서 then 메서드를 활용하여 실행한다.

이 때, 객체를 선언할 때, 비동기적으로 실행하고 싶은 코드를 넣는다.
그리고, 선언한 객체는 비동기작업이 성공할 때와, 실패했을 때를 분리하여 두가지 파라미터를 갖는 함수이며, 각 상황에 따라 비동기적인 코드를 동기적으로 만드는 과정을 거친다. 자세한건 코드를 보자.

/*const testSyn = new Promise(
  function(resolve,reject){
  }); */
const testSyn = new Promise((resolve, reject) => {
  setTimeout(() => {
    let num = 10;
    if (num > 11) {
      resolve(num);
    } else {
      reject("error");
    }
  }, 2000);
});

// testSyn.then(callback_success,callback_failure) 이 꼴이 실제 꼴.
testSyn.then(
  (value) => {
    console.log(value, "success");
  },
  (message) => {
    console.log(message);
  }
);

<코드 분석>
testSyn이라는 객체함수를 만들어서, 인자로 resolve, reject를 주었다.
이 때 Promise 객체 선언시 인자로 함수 하나가 들어간다.
실제 객체에서 실행되는 메서드인 then에는, 두가지 파라미터에 두 함수(실행 코드)가 들어가게 된다.
첫번째 실행 코드(인자)는, 비동기 함수 실행이 성공이 되었을 때이고, 두번째 실행 코드(인자)는 비동기 함수 실행이 실패했을 때이다. 이때 객체를 선언할 때 넘겨줬던 파라미터 num은 곧 testSyn에서 then메서드를 실행했을 때의 실행 코드의 인자 (console.log(value)) 로 들어가게 된다.
이 때 중요한점은 무엇을 동기적으로 만드느냐 인데,
** setTimeout 함수를 거치고 난 후에, resolve 혹은 reject가 실행된다는 점에서 setTimeout과 resolve/reject 간에 동기화가 일어나는 것이다.

Promise - 1. chaining 연쇄적 동기화 적용 (then 메서드를 계속 붙인다)

이 때, testSyn.then 이후에도 동기화를 적용하고 싶은 코드가 있을지 모른다. 그 때는 다음과 같이 작성하면 된다.

/*const testSyn = new Promise(
  function(resolve,reject){
  }); */
const testSyn = new Promise((resolve, reject) => {
  setTimeout(() => {
    let num = 10;
    if (num > 11) {
      resolve(num);
    } else {
      reject("error");
    }
  }, 2000);
});

// testSyn.then(callback_success,callback_failure) 이 꼴이 실제 꼴.
testSyn
  .then(
    (value) => {
      console.log(value, "success");
    },
    (message) => {
      console.log(message);
    }
  )
  .then(
    () => {
      console.log("success2");
    },
    () => {
      console.log("failure2");
    }
  );

이렇게 작성하면, num이 10인데 11보다 크므로, 먼저 reject('error')가 호출이 될텐데,
Promise 함수를 통해 만든 객체 testSyn에서는 reject('error')를 호출 시에, console.log(message)를 호출하게 되어있으므로, 콘솔창에는 error가 적혀있을 것이고, 그 후에 또다시 then이 호출되면서 success2 가 적히게 될것이다.
정리하자면 다음의 코드 결과의 콘솔창은 "error" 다음 줄에 "success2"가 적히게 된다.
이 때, 왜 failure2가 아니고 success2인가 하는 궁금증이 생기지만, tesySyn.then(callback_success,callback_failure)을 이미 하고난 뒤 객체에 then이 붙은 것이기 때문에, 이미 실행된 tesySyn.then(callback_success,callback_failure) 이 함수는 비동기화함수가 성공적으로 실행된 함수이므로 success2가 적히게 되는 것이다.

<기억해야할 사항>

  • then함수에 인자를 하나만 주게 되면, 비동기화 작업이 success할 때만 호출하게 된다.
  • chaining 기법을 사용할 시, return을 이용해서 다음 chaining 호출 메서드에 return값을 인자로 넘겨줄 수 있다.
const testSyn = new Promise((resolve, reject) => {
  setTimeout(() => {
    let num = 10;
    if (num > 9) {
      resolve(num);
    } else {
      reject("error");
    }
  }, 2000);
});

testSyn
  .then(
    (value) => {
      console.log(value, "success");
      return 100;
    },
    (message) => {
      console.log(message);
    }
  )
  .then(
    (num) => {
      console.log(num);
    },
    () => {
      console.log("failure2");
    }
  );

다음의 코드는 결국 console차에 10과 100을 순서대로 띄우게 된다.

Promise - 2. catch 메서드

예외 상황을 처리하는 메서드이다.
만일, then 메서드를 사용할 때, failure 의 상황을 설정하지 않았다면 (then 메서드의 인자를 하나(성공한 경우)만 줬다면) catch메서드를 실행하게 된다. 만일 success 와 failure의 상황 두가지 모두 인자로 들어갔다면 catch 메서드는 실행되지 않는다.

promise.catch((error)=>{console.log(error);});
> 예외 상황을 억지로 설정할 수 있는데 **throw** 문법을 이용해 설정할 수 있다.
```java script
throw new Error('error message');

중간에 이러한 throw 구문을 던지면, catch 메서드를 실행시키게 된다.

이 때, chaining으로 연결한 비동기처리를 하다가 throw문을 만나면 chaining을 끊고 바로 catch메서드를 실행한다.

Promise - 3. finally 메서드

finally 함수는 비동기화 작업이 성공하든, 실패하든간에 마지막에 실행되는 함수이다.

promise.finally(()=>{console.log('finally done')});

이 때, finally와 catch가 겹치게 되면, 만일 비동기화 작업이 처리되는 중간에, 예외 상황을 만나게 됐을 때, 느낌 상 catch로 건너 뛸 것 같지만, 그렇지 않고, finally를 거치고 catch로 간다. 왜냐하면 결국 예외상황을 만났다는 것은 작업이 종료(finally)가 됐다는 뜻이기 때문이다.

Promise - 4. Promise.all 메서드

동기화할 모든 함수들이 종료됐을 때 실행하기 위한 메서드이다.

Promise - 5. Promise.race 메서드

여러 함수중 가장 빠른 메서드가 끝났을 때 then구문을 실행하는 메서드이다.

profile
기록하고, 공유합시다

0개의 댓글