비동기1 - 개념

jeongjwon·2023년 3월 17일
0

SEB FE

목록 보기
22/56

📌 비동기

단어로 보면 동기는 '동시에 일어나는', 비동기는 '동시에 일어나지 않는' 이라는 의미를 가진다. 하지만, 그림에서 보는 것과는 반대로 느껴진다.
동기와 비동기의 동시라는 것은 동시성의 발생을 어디로 보느냐의 차이다.

동기는 요청의 결과가 그 자리에서 동시에 일어나야 하지만, 비동기는 그렇지 않다.
따라서 동기적 주문의 경우 그 자리에서 커피를 받아야만 다음 작업을 처리할 수 있고, 비동기적 주문의 경우 진동벨을 받는 방식이라 한 번에 여러 주문을 처리할 수 있어 속도가 빨라지게 된다.

✅ 동기 Synchronous

특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것

function lazy(func, delay){
  const delayUntil = Date.now() + delay; 
  //Date.now() 현재시간을 숫자 ms 으로 반환 (1000ms = 1s)
  
  while(Date.now() < dealyUntil);
  func();//일정시간 delay 이후에 콜백함수 func 호출
}
function foo(){ console.log('foo'); }
function bar(){ console.log('bar'); }

lazy(foo, 3*1000); //3초 이상 실행 후 콜백 함수 foo 실행
bar();//lazy 함수의 실행이 종료된 이후에 호출되므로 3초이상 블로킹 => bar 호출

다음 실행할 태스크가 실행중인 태스크가 종료될 때까지 대기하는 방식동기처리라고 한다. 동기는 실행순서가 보장된다는 장점이 있지만 태스크가 종료될 때까지 이후 태스크들이 블로킹되는 단점이 있다.

왜냐하면 자바스크립트는 엔진이 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드(single thread) 방식으로 동작한다. 싱글 스레드는 한 번에 하나의 태스크만 실행할 수 있으므로 시간이 걸리는 태스크를 실행하면 블로킹(blocking) = 작업중단이 발생한다.


✅ 비동기 Asynchronous

특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 수행하는 것

setTimeout(foo, 3*1000);
//타이머 함수 setTimeout 는 일정시간이 경과 후 콜백 함수 foo 호출
//이는 다음 태스크인 bar 함수를 블로킹 하지 않는다.
bar();

위와 같이 타이머함수 setTimeout 는 블로킹 하지 않고 콜백함수를 바로 호출하여 실행한다. 이처럼 실행 중 태스크가 종료되지 않은 상태여도 다음 태스크를 곧바로 실행하는 방식비동기처리라고 한다.




📌 Timer API

내장 비동기 함수
millisecond 단위 사용 => 1초 1000ms , 10초 10000ms

✅ setTimeout(callback, millisecond)

  • 일정시간 이후에 콜백함수를 실행, timeId 반환

✅ clearTimeout(timeId)

  • setTimeout 타이머 종료

✅ setInterval(callback, millisecond)

  • 일정 시간의 간격을 가지고 함수를 반복적으로 실행, timeId 반환

✅ clearInterval(timerId)

  • setInterval 타이머 종료
setTimeout(() => {console.log("1번")}, 5000);
setTimeout(() => {console.log("2번")}, 3000);
setTimeout(() => {console.log("3번")}, 1000);
console.log("4번") 
// 4번 -> (1초) -> 3번 -> (2초) -> 2번 -> (2초) -> 1번


비동기식을 이용하므로써 총 5초가 걸림

동기식으로 이용할 경우, 코드가 작성해놓은 순서대로 1번, 2번, 3번, 4번이 호출됨에 따라 앞의 함수 호출이 끝났을 때 해당 함수의 호출이 시작되어 총 9초가 걸린다.

const printString = (string) => {
  setTimeout(function(){
    console.log(string);
  }, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
  printString('A');
  printString('B');
  printString('C');
};
printAll();

위와 같은 timer API를 사용한 비동기 코드는 작성된 순서대로 작동되는 것이 아니라 동작이 완료되는 순서대로 작동하게 된다. 따라서 코드의 순서를 예측 및 보장할 수 없다.




📌 콜백

✅ 콜백 함수 Callback

  • 다른 함수의 전달인자로 넘겨주는 함수
  • 필요에 따라 즉시 실행(synchronous) 할 수도 있고 나중에 실행(asynchronous)할 수도 있다. (비동기 코드의 순서를 제어= 비동기를 동기화 시킬 수 있다.)
const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {});
    });
  });
};
printAll(); // A B C

✅ 콜백 지옥 Callback Hell

  • 비동기 프로그래밍시 발생하는 문제로, 함수의 매개변수로 넘겨지는 콜백함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
  • 가독성이 떨어지고, 코드를 수정하기 어렵다. => 이를 위해 promise 를 사용



📌 Promise

✅ Promise

  • 비동기로 작동하는 코드를 제어하고 Callback Hell 을 방지할 수 있다.
  • class 이므로 new 키워드를 이용해 객체를 생성하여 생성자 함수는 콜백함수로 resolve, reject 를 인수로 전달받는다. 내부에서 콜백 함수(executor)를 비동기 처리하는데, 성공적으로 처리되었다면 resolve 함수를, 에러가 발생되었다면 reject 함수를 호출한다.
  • promise 객체에는 state, result 내부 프로퍼티를 갖는다. 하지만 이는 직접 접근할 수가 없고, 후속 처리 메서드인 .then, .catch, .finally 를 이용하여 접근이 가능하다.
    • state : 기본 상태로는 pending(대기) 에서 콜백함수가 성공적으로 처리했다면 fulfilled(이행)으로, 에러가 발생했다면 rejected(거부)가 된다.
    • result : 기본 상태는 undefined 에서 콜백함수가 성공적으로 처리하여 resolve(value) 가 호출되면 value로, 에러가 발생했다면 reject(error)가 호출되어 error 로 변한다.

✅ then, catch, finally

  • then
    • 콜백함수에 작성했던 코드가 정상처리 > resolve > then 메서드로 접근
    • then 메서드는 두 개의 인자를 콜백함수로 받는다.(첫 번째는 성공시에 실행되는 값, 두 번째는 실패시 실행되는 값 -> 더 많은 예외를 처리하는 catch 로 에러 잡는 것을 지향)
    • 기본적으로 promise 반환
  • catch
    • 콜백함수에 작성했던 코드가 에러발생 > reject > catch 메서드로 접근
    • 기본적으로 promise 반환
  • finally : 콜백함수에 작성했던 코드의 정상이든 에러발생이든 상관없이 finally 메서드로 접근

✅ Promise chaining

비동기 함수의 결과를 가지고 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩되어 콜백 지옥이 발생할 수 있다. Promise 는 후속 처리 메소드를 체이닝하여 프로미스를 반환하는 여러개의 비동기 함수들을 연결하여 사용할 수 있습니다.

비동기 함수의 결과값을 .then 을 통해 연결하고 에러가 발생할 경우 .catch 로 처리한다.

✅ Promise.all()

여러 개의 비동기 작업을 동시에/병렬로 처리하고 싶을 경우 사용

const promise1 = () => new Promise(resolve => setTimeout(() => resolve(1), 1000))
const promise2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const promise3 = () => new Promise(resolve => setTimeout(() => resolve(3), 3000))

promise1().then(result => {
    console.log(result) // 프로그램을 실행하고 1초뒤에 수행됨
    return promise2()
}).then(result => {
    console.log(result) // 프로그램을 실행하고 3초뒤에 수행됨 (1 + 2)
    return promise3()
}).then(result => {
    console.log(result) // 프로그램을 실행하고 6초뒤에 수행됨 (1 + 2 + 3)
})
// 1 2 3

Promise Chaning 을 이용하여 코드가 순차적으로 동작되어 총 6초의 시간이 걸리고 같은 코드가 중복적으로 발생하게 된다.


Promise.all([
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(3), 3000))
]).then(console.log) // 프로그램을 실행하고 3초뒤에 실행됨
.catch(console.log)

서로 의존관계이지 않은 여러 프로미스들을 이터러블 객체에 담아 Promise.all 메소드를 이용해 한번에 병렬처리한다.가장 마지막으로 끝나는 프로미스를 기준으로 수행되고, 모든 프로미스가 fullfilled 상태가 되면 결과값을 배열에 담아 새로운 프로미스를 반환합니다.프로미스를 수행하던 도중 하나라도 에러(rejected)가 발생하면 rejected 상태가 되고 수행을 종료합니다.

✅ Promise Hell

Callbak Hell 를 보완하기 위한 Promise 또한 코드가 길어질수록 복잡해지고 가독성이 떨어지면서 Promise Hell 이 발생한다.

이를 보완하기 위한 것은 async.await 키워드

📌 Async / Await

기존의 콜백함수와 프로미스의 단점을 보완하여 가독성있는 코드를 작성할 수 있다.

  • 함수 앞에 async 키워드 사용 -> 자동적으로 promise 반환
  • async 함수 내에서만 await 키워드 사용 -> 동기적으로 처리
  • async 함수에서 return은 resolve() 와 같은 역할을 한다.
//함수 선언식
async function funcDeclarations(){
  await 코드
}

//함수 표현식
const funcExpression = async function(){
  await 코드
}

//화살표 함수
const arrowFunc = async () => {
  await 코드
}

프로미스에서 동기적으로 처리하기 위에 후속 처리 메소드인 then() 메소드를 사용해 동기적으로 처리했다면, 이제는 그럴필요 없이 프로미스를 반환하는 함수앞에 await을 붙여 더 간편하게 동기적으로 처리할 수 있다.

또한 프로미스를 사용해 동기적으로 처리하는 경우 fullfilled 값을 then() 후속 처리 메소드를 통해 결과값을 인자로 넘겨 체인내에서 처리해야하지만 await을 사용하면 fullfilled 값을 외부로 넘겨줄 수 있다.

다음과 같이 프로미스로 작성한 코드를 async와 await을 사용하여 동기적으로 처리할 수 있다.

  1. Promise로 동기적으로 처리 시
const timeout = (value, timeout) => new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), timeout)
})

timeout('Hello ', 1000)                                       // 프로그램 실행후 1초뒤에 수행됨
	.then(result => {
		console.log('complete promise')
		return timeout(result + 'My Name is ', 2000)  // 프로그램 실행후 3초뒤에 수행됨(1 + 2)
	}).then(result => {
		console.log('complete promise')
		return timeout(result + 'manja ', 3000)       // 프로그램 실행후 6초뒤에 수행됨(1 + 2 + 3)
	}).then(result => {
		console.log('complete promise')
		console.log(result)
	})
  1. async/await 로 동기적으로 처리 시
const timeout = (value, timeout) => new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), timeout)
})

async function awaitFunc() {
    let str = ''

    str += await timeout('Hello ', 1000)       // 프로그램 실행후 1초뒤에 수행됨
    console.log('complete promise')
    str += await timeout('My name is ', 2000)  // 프로그램 실행후 3초뒤에 수행됨(1 + 2)
    console.log('complete promise')
    str += await timeout('manja ', 3000)       // 프로그램 실행후 6초뒤에 수행됨(1 + 2 + 3)
    console.log('complete promise')

    return str
}

awaitFunc().then(console.log)

결과값은 동기식으로 진행되어 작성한 순서대로 콘솔에 출력이 된다.

complete promise
complete promise
complete promise
Hello My name is manja




결과적으로 정리하자면
Callback (Callback Hell) > Promise (Promise Hell) > Async/Await

비동기식 코드의 순서를 제어하기 위해서 콜백함수를 사용하는데,
이를 너무 길게 복잡하게 쓰면 CallBack Hell 이 발생하여 가독성이 떨어진다.

이를 보완하기 위해 Promise 를 통해 성공처리, 실패처리의 결과값을 반환하고 후속 처리 메소드를 통해 내부 프로퍼티에 접근할 수 있다. 후속 처리 메소드를 프로미스 체이닝하여 연결로 여러 개의 비동기 함수를 연결시킬 수 있다. 그리고 비동기 함수를 동시에/병렬적으로 사용할 수 도 있다.
하지만 이도 콜백헬 과 같이 길게 복잡하게 쓰면 Promise Hell 이 발생하게 되어 가독성이 떨어진다.

이를 또 보완하기 위한 것은 async/await 키워드를 사용하여 동기적으로 처리할 수 있다. 프로미스에서 후속 처리 메소드를 이용해서 결과값을 프로미스로 리턴해야 했다면 , async/await 는 직접 성공값을 외부에 전달할 수 있다.

0개의 댓글