본문 바로가기
JavaScript

자바스크립트에서의 비동기

by AlbertIm 2024. 8. 1.

비동기 프로그래밍

비동기 프로그래밍은 프로그램이 오래 실행되는 작업을 시작하고 해당 작업이 완료될때까지 기다릴 필요없이 다른 작업을 실행할수 있게하는 기술입니다.

브라우저에서 제공하는 많은 기능이 오랜 시간이 걸릴 수 있는 비동기 기능입니다.

 

예를 들어:

  • HTTP 요청을 하는 fetch.fetch()
  • 사용자의 카메라 또는 마이크 접근하기 위한 getUserMedia()
  • 사용자에게 파일을 선택하도록 요청하는 showOpenFilePicker

비동기 함수를 직접 구현하지 않더라도 사용해야 할 가능성은 매우 높기 때문에 비동기 프로그래밍에 대해 알고 있어야 합니다.

동기 프로그래밍

const name = "Albert";
const greeting = `안녕하세요. ${name}!`
console.log(greeting);
// 결과: "안녕하세요. Albert"

 

브라우저는 한 번에 한 줄순서대로 실행하고 다음 줄로 넘어가기 전에 작업을 마칠 때까지 기다립니다. 이것이 동기적 프로그램입니다.

동기 프로그래밍의 한계

동기식 기능이 오랜 시간이 걸린다면 어떻게 되나요?

JavaScript 프로그램이 단일 스레드로 구성되어 있기 때문에 한 번에 한 가지 일만 할 수 있습니다. 따라서 오랜 시간이 걸린 동기식 기능이 끝낼 때까지 다른 일을 할 수 없습니다.

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">  
  <title>동기 프로그래밍</title>  
</head>  
<body>  
<label for="quota">소수 범위:</label>  
<input type="text" id="quota" name="quota" value="200000"/>  

<button id="generate">소수 생성</button>  
<button id="reload">새로 고침</button>  

<div id="output"></div>  
<script>  
  let quota = document.getElementById('quota');  
  const generate = document.getElementById('generate');  
  const reload = document.getElementById('reload');  
  const output = document.getElementById('output');  

  // 시간이 오래 걸리는 작업  
  function generatePrimes(n) {  
    const primes = [];  
    for (let i = 2; i <= n; i++) {  
      let isPrime = true;  
      for (let j = 2; j < i; j++) {  
        if (i % j === 0) {  
          isPrime = false;  
          break;  
        }  
      }  
      if (isPrime) {  
        primes.push(i);  
      }  
    }  
    return primes;  
  }  

  // 소수 생성 버튼 클릭 이벤트  
  generate.addEventListener('click', () => {  
    const start = performance.now();  
    const n = parseInt(quota.value, 10);  
    output.textContent = '계산 중...';  
    const primes = generatePrimes(n);  
    output.textContent = primes.join(', ');  
    const end = performance.now();  
    console.log('소요 시간:', end - start);  
  });  

  // 새로 고침 버튼 클릭 이벤트  
  reload.addEventListener('click', function () {  
    location.reload();  
  });  

</script>  
</body>  
</html>

 

브라우저에 열면 다음 화면이 보입니다.

  • 소수 생성 버튼은 입력한 소수 범위내의 모든 소수를 화면에 보여줍니다.
  • 새로 고침 버튼은 화면을 reload합니다.

그런데 200,000 범위 내의 소수 생성 버튼을 누르면 소수를 생성하는데 브라우저 개발자 도구로 확인했을 때 약 2초 걸립니다.

 

동기적으로 기능을 설계하였기 때문에 2초동안 새로고침을 누를 수가 없습니다. 즉 2초동안 아무것도 할 수 없습니다. 만약에 200,000가 아니라 실수로 10,000,000가 20,000,00을 잘못 입력하면 화면이 멈춘것처럼 해당 작업이 끝날 때가 아무것도 할수가 없습니다.

Event loop

비동기 프로그램을 설명하기 전에 이벤트 루프에 대해 이야기하려고 합니다. JavaScript는 이벤트 루프 기반의 런타임 모델을 가지고 있습니다. 이 모델은 코드 실행, 이벤트 수집처리, 대기 중인 하위 작업 실행을 담당합니다. 이 모델은 C나 Java와 같은 다른 언어의 모델과 많이 다릅니다. 이를 이해하면 JavaScript에서 비동기를 어떻게 지원하는지 알 수 있습니다.

 

핵심은 JavaScript 언어가 비동기 실행하는 것이 아니라 브라우저libuv(Node.js) 내장 라이브러리에게 비동기 작업을 맡긴다는 점입니다. JavaScript의 Callback Queue은 MicroTask Queue와 (Macro)Task Queue의 두가지 종류가 있습니다. MicroTask Queue는 가장 먼저 우선으로 콜백이 처리하게 됩니다.

  • Task Queue: setTimeout,setInterVal,setImmediate
  • MicroTask Queue: Promisethen,catch,finally

흐름

Callbacks

Event Handler도 실제로 비동기 함수이며 callback의 한 유형입니다. callback은 적절한 시간에 호출될 것을 약속하며 다른 함수에 전달되는 함수일 뿐입니다. callback은 JavaScript에서 비동기 함수를 구현하는 주요 방법이었습니다.

 

그러나 콜백 기반 코드가 중첩되면 이해하기 어려울 수 있습니다.

function doStep1(value,callback) {  
    return callback(value + 1);  
}  

function doStep2(value,callback) {  
    return callback(value + 2);  
}  

function doStep3(value,callback) {  
    return callback(value + 3);  
}  

function doStep4(value,callback) {  
    return callback(value + 4);  
}  

function doOperation(value) {  
    return doStep1(value,function(result1) {  
        return doStep2(result1,function(result2) {  
            return doStep3(result2,function(result3) {  
                return doStep4(result3,function(result4) {  
                    return result4;  
                });  
            });  
        });  
    });  
}  

console.log(doOperation(1));

 

위 코드가 아래 코드와 같은 기능을 수행하지만 위 코드는 정말 이해하기 어렵습니다. 이를 callback 지옥이라고 합니다. 그래서 대부분의 최신 비동기 API는 callback을 사용하지 않고 Promise를 사용합니다.

function doStep1(value) {  
    return value + 1;  
}  

function doStep2(value) {  
    return value + 2;  
}  

function doStep3(value) {  
    return value + 3;  
}  

function doStep4(value) {  
    return value + 4;  
}  

function doOperation2(value) {  
    let result = doStep1(value)  
    result = doStep2(result)  
    result = doStep3(result)  
    result = doStep4(result)  
    return result;  
}

console.log(doOperation(1));

Promise

Promise비동기 작업최종 완료 또는 실패와 그 결과 값을 나타내는 객체입니다. 일반적으로 Promise는 콜백을 함수에 전달하는 대신 콜백의 최종 결과를 전달합니다. 이는 콜백 지옥을 해결할 수 있습니다.

위 콜백 함수를 아래와 같이 작성할 수 있습니다.

// 각 단계 함수들을 비동기적으로 처리할 수 있는 Promise 기반 함수들
function doStep1(value) {
    return new Promise((resolve) => {
        resolve(value + 1);
    });
}

function doStep2(value) {
    return new Promise((resolve) => {
        resolve(value + 2);
    });
}

function doStep3(value) {
    return new Promise((resolve) => {
        resolve(value + 3);
    });
}

function doStep4(value) {
    return new Promise((resolve) => {
        resolve(value + 4);
    });
}

// 순차적으로 비동기 작업을 처리하는 함수
function doOperation(value) {
    return doStep1(value)
        .then(result1 => doStep2(result1))
        .then(result2 => doStep3(result2))
        .then(result3 => doStep4(result3));
}

// 결과를 출력
doOperation(1).then(result => console.log(result)); 

 

코드를 비교해보면 가독성이 크게 향상되었습니다.

Promise의 상태

  • pending: 보류 상태 (초기 상태)
  • fulfilled: 작업이 성공 상태
  • rejected: 작업이 실패 상태

보류 상태성공 상태 또는 실패 상태가 되면 promisethen 메서드에 의해 큐에 추가되고 핸들러가 호출됩니다.

Chained Promises

promise 의 메서드 then(),catch(), 및 finally()는 해결된 promise와 추가 작업을 연결하는 데 사용됩니다.

  • then(): 최대 두 개의 인수를 사용합니다. 첫 번째 인수는 약속이 이행된 경우에 대한 콜백 함수이고 두 번째 인수는 거부된 경우에 대한 콜백 함수입니다.
  • catch(): 내부적으로 then()을 호출하여 오류 처리를 덜 복잡하게 만듭니다. catch()이행 핸들러를 전달하지 않고 실제로는 then()을 실행하는 것입니다.
  • finally(): Promise완료될 때 호출될 함수를 예약합니다.

async... await...

async

이 키워드는 비동기 함수선언하는 데 사용된다. async 함수는 Promise 를 반환한다. 서버에서 데이터를 가져오는 등의 시간이 걸리는 작업을 수행하는 함수가 있을 때 async를 선언하고 사용한다. 자바스크립트에게 이 함수를 비동기적으로 처리하라고 알려준다

async function fetchData() {
    // API에서 데이터 가져오기
}

await

이 키워드는 async 함수 내에서 Promise해결될 때까지 기다리는 데 사용된다. 함수의 실행을 Promise 가 처리될 때까지(이행되거나 거부될 때까지) 일시 중지한다. await 는 오직 async 함수 내에서만 사용할 수 있다.

async function fetchData() {
  let data = await fetch('https://api.example.com/data');
  let jsonData = await data.json();
  return jsonData;
}

await 사용하는 경우

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('해결');
      console.log('완료!');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('요청');
  const result = await resolveAfter2Seconds();
  console.log(result);
}

asyncCall();

// 출력 결과: 
// > "요청" 
// > "완료!" 
// > "해결"

await 사용하지 않는 경우

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('해결');
      console.log('완료!');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('요청');
  const result = resolveAfter2Seconds();
  console.log(result);
}

asyncCall();

// 출력 결과:
// > "요청"
// > Promise Object
// > "완료!"

마무리

전에도 JavaScript를 공부하면서 비동기가 어떻게 실행되는지 궁금했었는데 이번에 다시 공부하고 정리하면서 확실하게 알고 넘어가니까 마음속의 궁금증이 해결되었습니다. 이제 JavaScript 비동기를 알고 사용할 수 있을 것 같아서 매우 기쁩니다.

참고자료

댓글