JS(Section 14. 비동기 프로그래밍)

짜스의 하루 ·2024년 4월 28일

비동기 개념과 타임아웃

비동기 프로그래밍은 한 가지 작업이 끝나기 전에 다른 작업을 시작할 수 있는 프로그래밍 방식이다. 이것은 특히 네트워크 요청, 파일 읽기/쓰기, 타이머와 같은 작업에서 중요하다. 이러한 작업은 완료되기까지 시간이 걸리며, 이 시간 동안 프로그램이 대기하면서 다른 작업을 수행할 수 있도록 해주는 것이 바로 비동기 프로그래밍의 핵심이다.

⏲️ setTimeout 함수

  • 첫 번째 인자로 넣은 콜백 함수를 두 번째 인자로 넣은 수 만큼의 밀리초 후 실행
  • Web API, Node.js 등 자바스크립트 환경의 기능 - 언어의 기능이 아님

  • setTimeout() 함수는 주어진 시간(여기서는 2000밀리초, 즉 2초)이 지난 후에 콜백 함수를 실행하도록 스케줄링한다. 따라서 이 코드는 2초가 지난 후에 콘솔에 "타임아웃!"을 출력한다.

⭐ 비동기 asynchronous 코드
--> 동기 synchronous 코드와 달리, 코드가 순서대로 실행되지 않음!

  • 일반적으로 타임아웃, 네트워크 요청 등 시간이 걸리는 작업에 사용
  • 당장 할 수 있는 것들을 먼저 다 하고, 시간이 걸리는 작업은 뒤로 넘기기
  • 💡 만약 비동기 방식이 없다면 - 데이터를 받아오는 동안 화면이 어는 등 불편함이 발생한다.

🏃 달리기 경주 예제

setTimeout (() => {
   console.log(`🚩 ${num}번 ${name} 도착`);
 }, 1000 + Math.random() * 500);
}
  • Math.random() * 500 은 0 이상 500 미만의 무작위한 숫자를 반환한다. 이는 경주 도착까지의 임의의 시간을 나타낸다. 이 값은 1000에서 1500 사이의 시간에 추가된다. 따라서 전체적으로는 1000에서 2000 사이의 시간 동안의 임의의 시간이 소요된다.
  • forEach 메서드는 배열의 각 요소에 대해 주어진 함수를 순차적으로 호출한다. 여기서는 배열의 각 요소를 item으로 받고, 인덱스를 idx로 받아서 doRace(++idx, item)를 호출한다.

이 코드는 배열의 각 요소에 대해 번호와 이름을 전달하여 doRace 함수를 실행하며, 각 참가자는 무작위로 지정된 시간에 출발하고 도착한다.

⭐ 과정 설명

  • 자바스크립트의 코드는 싱글 스레드로 실행 - 외나무다리
  • 그림에서 흰색 줄로 (싱글 스레드)로 실행되는 것을 확인할 수 있다.

  • 실행시점에 도착하면 동기 코드는 실행
    비동기 콜백 코드는 환경(Web API 등)으로 넘겨진다.

  • 자바스크립트의 실행환경은 멀티스레드 - 다차선 도로
    여러 작업들이 동시다발적으로 진행될 수 있다.

  • 주어진 작업을 마친 작업들은 태스크 큐 task queue 선로로 진입

  • 이벤트 루프 event loop - 태스크 큐의 작업이 도착하는대로 자바스크립트 실행도로에 올려놓음
  • 동기 코드가 모두 실행된 다음 진행됨 - 지연시간을 0으로 해도 나중에 실행되는 이유이다.

Promise

콜백 지옥 callback hell

  • 중첩된 구조로 되어 있어서 각 setTimeout 함수가 이전 함수가 실행된 후에 호출되므로, 0.5초마다 1부터 5까지의 숫자가 출력된다.

💡 연속적으로 비동기 코드를 써야 하는 경우

  • 위와 같이 콜백 함수 안에 또 다른 콜백 함수를 넣어야 하는 상황 발생 - 콜백 지옥
  • 횟수가 많을수록 가독성도 낮아지고 직관성이 떨어짐
  • 실전에서는 더더욱 복잡하고 난해해짐

🏃🏃🏃 릴레이 예제

const DEADLINE = 1500;

function relayRun (name, start, nextFunc, failMag) {
    console.log(`${name} 출발`);
    const time = 1000 + Math.random() * 500;

    setTimeout(()=> {
        if(time < DEADLINE){
            console.log( `${name} 도착 = ${(start + time)/1000}초`);
            nextFunc ?. (start + time);
        } else {
            console.log(failMag);
            console.log(`완주 실패 = ${(start + time)/1000}초`);
        }

        if(time >= DEADLINE || !Function){
            console.log('---경기 종료---');
        }
    }, time);
}
  • time 변수에 1000 밀리초에서 1500 밀리초 사이의 무작위 시간을 할당한다. 이것은 선수가 도착하는 데 소요되는 시간을 시뮬레이션한다.
  • setTimeout 함수를 사용하여 랜덤한 시간 이후에 실행될 비동기 코드 블록을 정의
  • 도착 시간이 DEADLINE(1500 밀리초)보다 작으면 선수가 성공적으로 도착한 것으로 간주하고, 다음 선수의 출발을 시작하는 nextFunc 함수를 호출한다.
  • 도착 시간이 DEADLINE을 넘거나 nextFunc이 제공되지 않은 경우에는 완주 실패 메시지를 출력한다.

setTimeout 함수는 두 번째 매개변수로 전달된 시간(밀리초)이 지난 후에 콜백 함수가 실행되도록 스케줄링한다. 이렇게 하면 콜백 함수가 비동기적으로 실행되며, 지정된 시간 이후에 호출된다.
따라서 코드에서 }, time); 부분은 setTimeout 함수가 time 밀리초 후에 실행되도록 예약하는 역할을 한다. 이렇게 함으로써 경기가 시작된 후 일정한 시간이 지난 후에 선수의 도착을 시뮬레이션하고, 경기의 진행 상황을 효과적으로 제어할 수 있다.

함수를 실행해 보았는데, 작성하기도 어렵고, 가독성이 매우 떨어지는 것을 느낄 수 있다.

프로미스 promise

  • (보통 시간이 걸리는) 어떤 과정 이후 주어진 동작을 실행할 것이란 약속을 함
  • 중첩된 비동기 코드를 직관적이고 연속적인 코드로 작성할 수 있도록 함
    --> 프로미스는 자바스크립트 비동기 처리에 사용되는 객체이며, 비동기 작업의 완료나 실패와 그 결과 값을 나타낸다.
const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업 수행
  if (/* 성공 조건 */) {
    resolve('성공 결과');
  } else {
    reject('실패 이유');
  }
});
  • 프로미스는 then()과 catch() 메서드를 사용하여 처리 결과를 다룬다.
  • then() 메서드는 프로미스가 이행되었을 때 실행되며, catch() 메서드는 프로미스가 거부되었을 때 실행된다.
myPromise.then(result => {
  console.log('성공:', result);
}).catch(error => {
  console.error('실패:', error);
});

예시로 살펴보자

생성자 Promise

  • 새로운 약속을 하는 코드
  • 인자로 받는 콜백함수의 첫 번째 인자 resolve ( 이름은 관례 ) - 약속 이행 성공시, 반환할 값 넣어 실행
  • 프로미스 인스턴스 ( 만들어진 약속 ) 의 then 메서드
    --> resolve를 통해 ( 약속대로 ) 반환된 결과(result)를 인자로 하는 콜백 함수를 넣음
  • ⭐ 또 다른 프로미스를 반환 - 체이닝 가능

then() 메서드는 프로미스가 이행되었을 때 실행된다. resolve() 함수가 실행되면서 넘겨준 값은 .then() 메서드에 전달되어 사용된다. 따라서 result 변수는 resolve() 함수에 전달된 값인 borrow * 1.1을 나타낸다. 그 후에는 .then() 메서드의 콜백 함수가 실행되어 이 값을 처리하고 출력한다.

  • 일반적으로 내부에 비동기 코드를 사용
  • 시간이 소모되는 비동기 과정 후 ~를 반환하겠다는 약속
  • 이 프로미스는 setTimeout 함수를 사용하여 2초 후에 이행될 것을 지정해 두었다.

  • if (Math.random() < 0.5) { ... } : 무작위로 선택된 확률에 따라 실행될 코드 블록을 결정한다. 여기서는 50%의 확률로 실행된다.
  • reject('사업 망함'): 만약 50%의 확률로 이 코드 블록이 실행된다면, reject 함수가 호출되어 프로미스가 거부된다. '사업 망함'이라는 메시지가 거부 이유로 사용된다.
  • resolve(borrow * 1.1) : 위의 조건문이 실행되지 않았거나, 실행되었지만 거부되지 않은 경우에는 resolve 함수가 호출되어 프로미스가 이행된다. 이때 borrow * 1.1의 결과가 이행 값으로 사용된다.

  • .then(result => { ... }) : 프로미스가 이행될 경우 실행될 콜백 함수를 등록한다. 이 콜백 함수는 프로미스가 이행될 때 호출되며, 이때 이행 값이 result 매개변수로 전달된다. 전달된 값과 '만원' 문자열을 결합하여 결과를 콘솔에 출력한다.
  • .catch(msg => { ... }) : 프로미스가 거부될 경우 실행될 콜백 함수를 등록한다. 이 콜백 함수는 프로미스가 거부될 때 호출되며, 거부 이유가 msg 매개변수로 전달된다. 전달된 거부 이유를 콘솔에 에러 메시지로 출력한다.
  • .finally(() => { ... }) : 프로미스의 상태와 상관없이 마지막에 실행될 콜백 함수를 등록한다. 이 콜백 함수는 프로미스가 이행되거나 거부된 후에 호출된다. 이 경우에는 '기한 종료'라는 메시지를 콘솔에 출력한다.

생성자 Promise

  • 인자로 받는 콜백함수의 두 번째 인자 reject ( 이름은 관례 ) - 약속 이행 실패시, 반환할 값 넣어 실행
  • reject가 실행되면 resolve는 무시됨

프로미스 인스턴스의

  • catch 메서드 : reject를 통해 ( 실패로 인해 ) 반환된 결과를 인자로 하는 콜백 함수를 넣음
  • finally 메서드 : 성공하든 실패하든 실행할 콜백 함수 - 필요할 때만 사용
  • then과 더불어 메서드 체이닝으로 사용

예제) 💰 10% 이자, 채무자 파산가능성 10%, 5번 빌려주기

  • 1초 후에 실행될 콜백 함수를 setTimeout을 사용하여 정의한다.
  • 이 콜백 함수 내에서 무작위로 선택된 확률에 따라 프로미스가 이행되거나 거부된다.
  • 확률이 0.1보다 작으면 (Math.random() < 0.1), 프로미스가 거부되고 '채무자 파산'이라는 이유가 거부된다.
  • 그렇지 않으면, 프로미스가 이행되고 borrow * 1.1의 값이 이행 값으로 사용된다.
    --> 따라서 이 함수는 1초 후에 주어진 금액을 빌려오고, 10%의 확률로 채무자가 파산할 수 있다.


이 코드는 moneyLand 함수를 연속적으로 호출하여 금액을 빌리고, 그 금액에 10%를 더한 후에 반복적으로 반환하는 프로세스를 나타낸다.

각각의 .then() 메서드는 프로미스가 이행될 때 실행될 콜백 함수를 등록하며, .catch() 메서드는 프로미스가 거부될 때 실행될 콜백 함수를 등록한다. .finally() 메서드는 프로미스가 이행되든 거부되든 마지막에 실행될 콜백 함수를 등록한다.

각각의 .then() 메서드는 이전 프로미스가 이행된 값을 인자로 받아 moneyLand 함수를 호출하고, 그 반환 값을 다음 .then() 메서드에 전달한다. 이렇게 연속된 호출을 통해 여러 번의 빌림과 반환을 처리한다.

마지막 .then() 메서드에서는 최종 반환된 값을 받아와서 콘솔에 출력한다. .catch() 메서드는 거부된 이유를 받아와서 에러 메시지로 출력한다. 여기서 msg의 값은 reject('채무자 파산')에 해당하는 채무사 파산이다. 마지막으로 .finally() 메서드는 대금업 프로세스의 종료를 알리는 메시지를 출력한다.

🏃🏃🏃 릴레이 예제 프로미스로 구현

  • 함수가 호출되면 출발 메시지가 콘솔에 출력된다. (console.log("👟 ${name} 출발"))
  • setTimeout을 사용하여 랜덤한 시간이 지난 후에 실행될 콜백 함수를 등록한다. 이 콜백 함수는 비동기적으로 실행된다.
  • 콜백 함수 내에서는 랜덤한 시간(time)이 지정된 후에 처리된다. 만약 time이 주어진 DEADLINE보다 작다면, 선수가 도착한 것으로 간주하고 해당 시간을 이전의 출발 시간에 더한 값을 resolve 메서드로 반환한다. --> (resolve(start + time))
  • 그렇지 않은 경우(time >= DEADLINE), 실패 메시지가 콘솔에 출력되고, reject 메서드가 호출되어 프로미스가 거부된다. 이 때 거부 이유로는 도착한 시간을 이전의 출발 시간에 더한 값을 초로 변환한 값이 사용된다. --> (reject((start + time) / 1000))

  • getRelayPromise 함수를 사용하여 첫 번째 선수인 '서연'의 출발을 시작한다. 이 때 출발 메시지가 출력되고, 해당 선수의 경주 결과를 반환하는 프로미스가 생성된다.
  • 첫 번째 .then() 메서드에서는 이전 선수의 결과를 받아와서 다음 선수인 '원식'의 출발을 시작한다. 이전 선수의 결과가 이행되면(도착 성공), '원식'의 출발 메시지가 출력되고 해당 선수의 경주 결과를 반환하는 프로미스가 생성된다.
  • 이후의 .then() 메서드들은 각각의 선수에 대해 위와 동일한 프로세스를 반복한다. 이전 선수의 결과를 받아와서 다음 선수의 출발을 시작하고, 해당 선수의 경주 결과를 반환하는 프로미스가 생성된다.
  • 만약 어떤 선수가 실패하게 되면(reject), 해당 선수의 실패 메시지가 출력된다.
  • 모든 선수의 경주가 완료되면(finally), 경기 종료 메시지가 출력된다.

퀴즈

50% 확률로 ‘홀’ 또는 ‘짝’을 반환하는 비동기 작업이 있다. 이를 세 번 연속으로 사용하여 그 결과를 쉼표로 구분한 문자열로 받아오려 한다. 예를 들면 ‘홀, 짝, 홀’, ‘짝, 짝, 홀’과 같은 결과가 3초 후 출력되는 것이다.

그럼 먼저, 50% 확률로 홀, 짝을 반환하는 비동기 작업을 수행하는 함수를 만들어보자

function getOddEven(){
  return Promise((resolve) => {
    setTimeout(() =>{
      const res = Math.random() < 0.5 ? '홀' : '짝';
      resolve(res);
    })
  })
}
  • return Promise((resolve) => { : 새로운 Promise 객체를 반환한다.
    이 Promise는 비동기 작업을 수행할 하며, 인자로는 resolve라는 함수를 받는다.
  • setTimeout(() => { : setTimeout 함수를 사용하여 비동기적으로 작업을 수행한다. 이 함수는 지정된 시간(여기서는 따로 지정하지 않았으므로 0)이 지난 후에 콜백 함수가 실행된다.
  • const res = Math.random() < 0.5 ? '홀' : '짝'; : Math.random() 함수를 호출하여 50%의 확률로 '홀' 또는 '짝'을 반환
  • resolve(res); : Promise가 이행되고 완료될 때까지 기다리고 있던 resolve 함수를 호출하여 비동기 작업을 완료한다. 여기서는 이 작업의 결과를 res로 전달한다.

먼저, promise를 사용해서 결과를 비동기적으로 출력해보자

function concatRes() {
    let finalRes = '';

    return getOddEven()
        .then(res1 => {
            finalRes += res1;
            return getOddEven();
        })
        .then(res2 => {
            finalRes += ', ' + res2;
            return getOddEven();
        })
        .then(res3 => {
            finalRes += ', ' + res3;
            return finalRes;
        });
}

concatRes().then(res => {
    console.log(res);
});
  • let finalRes = ''; : 결과를 저장할 빈 문자열을 초기화한다.

  • return getOddEven() : getOddEven 함수를 호출하고 해당 Promise를 반환한다.

  • .then(res1 => { : 이전 Promise가 이행될 때 실행할 함수를 정의한다. 이 경우 첫 번째 비동기 호출의 결과가 res1 매개변수로 전달된다.

  • finalRes += res1; : 첫 번째 결과를 finalRes에 추가한다.

  • return getOddEven(); : 새로운 getOddEven 호출의 Promise를 반환한다.

  • .then(res2 => { : 새로운 Promise가 이행될 때 실행할 함수를 정의한다.

  • finalRes += ', ' + res2; : 두 번째 결과를 이전 결과와 쉼표와 함께 finalRes에 추가한다.

  • return getOddEven(); : 새로운 getOddEven 호출의 Promise를 반환한다.

  • .then(res3 => { : 새로운 Promise가 이행될 때 실행할 함수를 정의한다.

  • finalRes += ', ' + res3; : 세 번째 결과를 이전 결과와 쉼표와 함께 finalRes에 추가한다.

  • return finalRes; : 최종 결과를 포함한 Promise를 반환한다.

이번에는 async & await을 사용해보자

async function concatRes() {
    const res1 = await getOddEven();
    const res2 = await getOddEven();
    const res3 = await getOddEven();

    return res1 + ', ' + res2 + ', ' + res3;
}

async function execute() {
    const res = await concatRes();
    console.log(res);
}

execute();

이 코드는 비동기적으로 세 번의 getOddEven() 호출을 수행하고, 그 결과를 연결하여 반환하는 concatRes 함수와, concatRes 함수를 실행하고 그 결과를 출력하는 execute 함수로 이루어져 있다.

async function concatRes(){

  • concatRes 함수는 async 키워드를 사용하여 비동기 함수로 정의된다.
  • res1, res2, res3 변수를 선언하고, 각각 await getOddEven() 을 사용하여 순차적으로 getOddEven() 함수를 실행하고 결과를 받는다.

async function execute(){

  • execute 함수도 async 키워드를 사용하여 비동기 함수로 정의됩니다.
  • const res = await concatRes();를 사용하여 concatRes 함수를 실행하고, 해당 결과를 res 변수에 저장한다.
  • console.log(res);를 통해 결과를 출력한다.

굳이 함수를 실행하고, 출력하는 함수를 따로 만들 필요는 없다!

async function concatRes(){
  const res1 = await getOddEven();
  const res2 = await getOddEven();
  const res3 = await getOddEven();

  const results = res1 + ', ' + res2 + ', ' + res3;
  console.log(results);

  return results

}

concatRes();

위 함수와 같이 결과 값을 console.log()로 출력하고, 반환한다면, execute()함수를 만들 필요 없이, 결과 출력까지 가능하다.

비동기 함수를 사용하여 두 수를 무작위로 생성하고, 그 두 수를 곱한 결과를 출력해보자.
함수를 실행하면 1초 간격으로 두 개의 무작위 수가 생성되고, 그 두 수를 곱한 결과가 출력된다.


randomNum() :

  • 새 Promise를 생성하여 반환한다.
  • 이 Promise는 1초 후에 무작위로 생성된 1에서 10 사이의 정수를 randomNumber에 저장한다.
  • resolve()를 사용하여 생성된 난수를 반환한다.

result() :

  • randomNum() 함수를 사용하여 두 개의 무작위 숫자를 얻는다.
  • 첫 번째 숫자를 num1에 저장하고, 두 번째 숫자를 num2에 저장한다.
  • num1과 num2를 곱한 결과를 계산하여 result 변수에 저장한 후, 계산된 결과를 콘솔에 출력한다.
profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글