자바스크립트 비동기 처리 방식

frankiethefrank·2021년 10월 1일
0

TIL

목록 보기
4/4

동기/비동기를 처음 접한 내 심정

동기 / 비동기

  • 동기와 비동기를 나눌 수 있는 기준은 실행 순서의 차이이다.
    아래 이미지처럼 Synchronous(동기)는 요청을 보내고 요청의 응답을 받아야 다음 동작을 실행한다.
  • Asynchronous(비동기)는 요청을 보내고 해당 요청의 응답과 관계없이 다음 동작을 실행한다.

동기적 방식

  • 은행번호표 뽑기!
    은행에서 앞 사람 업무가 끝나고 내 번호가 됐을 때 내 업무를 볼 수 있는 것처럼 하나의 이벤트가 모두 끝날 때까지 다른 이벤트를 처리하지 않고 이전 이벤트가 완료된 후 다음 이벤트를 처리하는 실행 순서가 확실한 것 = 동기적 방식

비동기적 방식

  • 음식 주문!
    식당에서 순서대로 주문을 받아도 먼저 온 사람의 음식을 만들면서 다음 사람의 음식도 만들기 시작한다.
    이렇게 연속적으로 발생하는 이벤트를 다 받아 완료되는 순서대로 일을 처리하는 실행 순서가 확실하지 않은 것 = 비동기적 방식

자바스크립트는 Single Thread.

자바스크립트는 Single Thread 언어이다.
이벤트를 처리하는 Call Stack(함수 실행 호출이 쌓이는 곳. 호출이 끝나면 사라짐)이 하나밖에 없는 언어라는 뜻이다.
그래서 여러 이벤트를 처리할 때 동기적으로 처리하면, 모든 이벤트를 처리할 때까지 다른 이벤트나 업무를 수행하지 못하게 된다.

자바스크립트 구동순서 참고
따라서, 자바스크립트는 즉시 처리 불가 이벤트들을 Web API로 보내 콜스택이 비었을 때, 먼저 처리된 이벤트들을 줄세워 다시 이벤트 큐에 줄을 세운다.

Web API로 들어오는 순서보다는 어떤 이벤트가 먼저 처리되느냐가 중요.

만약 비동기 처리 이벤트들의 순서가 중요해지게 된다면 어떨까?
ex) 서버에 로그인 사용자 아이디를 요청하는 비동기 처리 후 사용자의 아이디를 이용해 프로필 사진 정보를 재요청해야하는 상황이라면?

순차적으로 진행되어야 하는 비동기를 처리하는 여러 방식有.

비동기 흐름을 처리하는 방식

1. CallBack 함수 사용

처리되어야 할 이벤트들을 순차적으로 콜백 함수로 넣어주는 방식, 일명 콜백 지옥

doItFirst(function(result) {
  doItSecond(result, function(newResult) {
    doItThird(newResult, function(finalResult) {
      console.log('What is the last result : ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

함수의 최초 반환값 result를 받아, 다음 비동기 처리를 해야하는 경우에 함수를 콜백으로 받아 비동기 처리 가능
많이 사용된다고 하나, 고전이고 지옥이라 불릴 만큼 치명적인 단점O

  1. 가독성이 떨어진다.

  2. 만약 비동기 처리가 위 예시처럼 3개가 아니라면 끝없는 지옥을 보게될 것
  3. 에러처리를 한다면 모든 콜백에서 각각 에러 핸들링 필요

  4. 콜백의 깊이만큼 에러처리도 복잡해진다. 에러처리 없이 진행해도 되지만 콜백 함수가 호출되지 않을 경우 도대체 어디서, 왜 발생했는지 알아내기 어렵다.
이런 불편함을 해소하기 위해 Javascript ES6에서 비동기 흐름 컨트롤 방법으로 Promise 객체가 등장.

2. Promise 객체

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과값을 나타냅니다.
출처 MDN

Promise를 사용하여 비동기 작업이(성공 혹은 실패) 완료된 후의 결과값을 받을 수 있다.
결과값을 돌려받을 수 있기 때문에 이후 처리를 컨트롤 할 수 있게 된다.

Promise의 상태값
Promise 객체는 new 키워드로 생성할 수 있으며 총 4개의 상태값을 가진다.

Pending: 아직 결과값이 반환되지 않은 진행중인 상태
fulfilled: 성공
Rejected: 실패
Settled: 결과값이 성공 혹은 실패로 반환된 상태

상태값은 크게 Pending과 Settled로 나눌 수 있으며, Settled는 다시 fulfilled와 Rejected로 나누어진다.
한 번 Settled된 값은 재실행 할 수 없다.

생성

Promise는 함수를 인자로 받으며 인자로 들어온 함수는 다시 resolve와 rejected 2개의 함수를 인자로 받게 된다.
resolve는 비동기 처리 성공 시 호출, rejec는 비동기 처리 실패시 호출된다.

const promise = new Promise(function(res, rej) {
  setTimeout(function() {
    res(111);
  }, 1000);
});

// 화살표 함수로 작성해도 동일
const promise = new Promise((res, rej) => {
  setTimeout(()) => {
    res(111);
  }, 1000);
});

new Promise로 생성된 인스턴스 객체는 '객체'이기 때문에 위와 같이 변수로 할당하거나 함수의 인자로 사용할 수 있다.

사용

인스턴스 호출 시에는 대표적으로 thencatch메소드를 사용한다.

resolve시 then으로
resolve되는 값은 then 메소드의 인자로 넘어간다.

const promise = new Promise((res, rej) => {
  setTimeout(() => {
    res(111);
  }, 1000);
});

promise.then((res) => console.log(res));

// 출력값
111

reject시 catch
resolve와 반대로 reject되는 값은 catch메소드의 인자로 넘겨 에러 핸들링을 할 수 있다.

const promise = new Promise((res, rej) => {
  setTimeout(() => {
    rej('error!');
  }, 1000);
});

promise
  .then((res) => console.log(res))
  .catch((err) => console.log('error : ' + err));

// catch 메소드에 잡혀서 console.log(err)에서 출력된 값
error!

여기서 또 중요한 점은 then메소드는 다시 Promise를 반환한다는 것!!
Promise객체를 반환한다는 것은 then, catch메소드를 사용할 수 있다는 것을 뜻하며, 이를 통해 then메소드로 연속적인 Promise chaining이 가능하다는 것을 의미한다.

.catch() 이후에도 chaining가능

new Promise((res, rej) => {
  console.log('Initial');
  res(); // resolve된 후 then 실행
})
.then(() => {
  throw new Error('Something failed');
  console.log('Do this'); // error 발생으로 실행되지 않는다.
})
.catch((err) => {
  console.log(err); // throw new Error의 인자값이 넘어온다.
})
.then(() => {
  console.log('Do this, whatever happened before'); 
  // catch 구문 이후 chaining
});

// 출력값
Initial
Something failed // ERROR
Do this, whatever happened before // catch 메소드 이후 then 메소드 실행  

Promise.all

Promise.all() 메서드는 순회 가능한 객체에 주어진 모든 프로미스가 이행된 후, 혹은 프로미스가 주어지지 않았을 때 이행하는 Promise를 반환.
주어진 프로미스 중 하나가 거부하는 경우, 첫 번째로 거절한 프로미스의 이유를 사용해 자신도 거부
출처 MDN

Promise.allPromise인스턴스들이 담긴 배열을 인자로 받아 사용하는데, 배열의 모든 요소가 Promise인스턴스일 필요는 없다.

const promise1 = Promise.resolve(3);
const promise2 = 42; // 프로미스가 아니어도 괜찮아요~
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(values => {
  console.log(values); // Array [3, 42, 'foo']
});

// resolve되는 값들을 destructuring 할 수 있다.
Promise.all([promise1, promise2, promise3])
.then(([one, two, three]) => {
  console.log(one); // 3
  console.log(two); // 42
  console.log(three); // 'foo'
});

Promise인스턴스 외 사용 예시

const emptyPromiseAll = Promise.all([]); // 빈 배열을 동기적 실행

// 프로미스가 아닌 값은 무시하지만 비동기적으로 실행됨
const promiseA = Promise.all([1337, "hi"]); 
// 위와 동일
const promiseB = Promise.all([1, 2, 3, Promise.resolve(444)]);


p; // [] 동기적 실행히라 then 메소드 사용하지 않아도 된다. 
p2.then(res => console.log(res)); // [1337, 'hi']
p3.then(res => console.log(res)); // [1, 2, 3, 444]

MDN에 매우 다양한 예시가 있는데 다양한 조합으로 동기, 비동기 실행 차이와 비동기 실행시 pending과 settled되는 경우들을 직접 개발자 도구창에 찍어보는 것이 좋다.

3. async / await

async function 선언 AsyncFunction객체 반환하는 하나의 비동기 함수를 정의.
비동기 함수는 이벤트 루프를 통해 비동기적으로 작동하는 함수로, 암시적으로 Promise를 사용하여 결과를 반환합니다.
비동기 함수를 사용하는 코드의 구문과 구조는, 표준 동기 함수를 사용하는 것과 많이 비슷합니다.

출처 MDN

ES2017에 등장한 async / await구문은 Promise를 기반으로 사용되는데 위의 MDN의 설명과 같이 비동기 코드를 좀 더 동기적인 코드처럼 작성할 수 있게끔 한다.
이번 프로젝트에서는 .then, .catch구문만 사용해봤지만 async / await를 사용해봐야겠다.

async / awaitPromise대체하는 것이 아니라는 것을 유념!!
Promise를 사용하지만 then, catch메소드를 사용하여 컨트롤 하는 것이 아닌 동기적 코드처럼 반환값을 변수에 할당하여 작성할 수 있게끔 도와주는 문법.

await 키워드는 async 함수에서만 사용 가능하며 async 함수가 아닌 곳에서 사용하면 아래와 같이 SyntaxError가 발생

error handling
try.. catch..구문을 사용할 수 있으며 Promise chaining에서 연결된 then메소드 중 하나라도 에러가 발생할 시 catch메소드로 잡는 것처럼 비동기 처리 중 에러 발생 시 catch블럭에서 잡히게 된다.

function promise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  try {
      console.log(1);
      const result = await promise(); // Promise가 settled될 때까지 기다린 후 resolve된 값을 할당한다.
      console.log(result);
      console.log(2);
  } catch(err) {
    console.error(err); // error 발생 시 catch 블락에서 잡히도록 handling
  }
}

asyncCall();

// 출력 값
1  // asyncCall 호출
resolved  // resolve 함수 호출
2  // await 후 다음 코드 실행

아래의 경우는?

console.log(0);

function promise() {
  console.log(1);
  
  return new Promise(resolve => {
    setTimeout(() => {
      
      console.log(2);
      resolve('resolved');
    }, 2000);
  });
}

console.log(3);

async function asyncCall() {
  try {
      console.log(4);
    
      const result = await promise(); // Promise가 settled될 때까지 기다린 후 resolve된 값을 할당한다.
    
      console.log(result); 
      console.log(5);
  } catch(err) {
    console.error(err); // error 발생 시 catch 블락에서 잡히도록 handling
  }
}

console.log(6);

asyncCall();

// 출력 값
0
3
6
4  // asyncCall 호출
1  // promise 함수 호출
2  // 2초 후 setTimeout 콜백 함수 호출
resolved // resolve함수 호출
5  // await 후 다음 코드 실행

두 예제에서 눈여겨볼 점은 await로 호출한 함수 출력값 result를 할당한 후의 console.log들이다.

비동기 상황에서는 어떤 이벤트가 먼저 완료될 지 순서가 불명확하나,
async await사용 시 먼저 완료되어야 할 이벤트 순서대로, 동기적으로 실행되는 코드처럼 작성할 수 있다.

What I truly say...

  1. 콜백 함수 사용
  2. Promise - then, catch
  3. Promise를 활용한 async / await

실제 코드 작성 시에는 axios나 fetch를 통해 서버에 요청을 하면 자동으로 Promise를 반환해주기 때문에 Promise객체를 직접 생성하는 경우보다 Promise로 반환되는 객체들을 async await를 사용하여 비동기 처리하는 경우가 대부분.
하지만, Promise의 구동 방식을 알고 있어야 사용할 때도 그 흐름을 알 수 있다.
Finally, async / await 사용 시도해보자

profile
hello I'm Frankie!

0개의 댓글