자바스크립트 - 비동기

정진우·2022년 6월 4일
0

JavaScript

목록 보기
3/3
post-thumbnail

동기(Synchronous)

동기식 처리 모델은 직렬적으로 작업을 수행한다.
즉, 작업은 순차적으로 실행되며 어떤 작업이 수행 중이면
다음 작업은 대기하게 된다.


아래는 동기식으로 동작하는 코드이다.

function func1() {
  console.log(1);
  func2();
}
function func2() {
  console.log(2);
  func3();
}
function func3() {
  console.log(3);
}
func1();
// 실행 결과
// 1
// 2
// 3

비동기(Asynchronous)

비동기식 처리 모델은 병렬적으로 작업을 수행한다.
즉, 작업이 종료되지 않은 상태라 하더라도 대기하지 않고
다음 작업을 실행한다.
자바스크립트의 대부분의 DOM 이벤트 핸들러와 Timer 함수(setTimeout, setInterval),
Ajax 요청은 비동기식 처리 모델로 동작한다.


아래는 비동기식으로 동작하는 코드이다.

function func1() {
  console.log(1);
  func2();
}
function func2() {
  setTimeout(function() {
    console.log(2);
  }, 0);
  func3();
}
function func3() {
  console.log(3);
}
func1();
// 실행 결과
// 1
// 3
// 2

함수 func1이 호출되면 func1은 Call Stack에 쌓인다.
그리고, 함수 func1은 함수 func2을 호출하므로 함수 func2가
Call Stack에 쌓이고 setTimeout이 호출된다.
setTimeout의 콜백함수는 즉시 실행되지 않고 지정 대기 시간만큼
기다리다가 "tick" 이벤트가 발생하면 Task Queue로 이동한 후
Call Stack이 비어졌을 때 Call Stack으로 이동되어 실행된다.


호출 스택(Call Stack)

호출 스택은 여러 함수들을 호출하는 스크립트에서 해당 위치를
추적하는 인터프리터를 위한 메커니즘이다. 현재 어떤 함수가 동작하고있는지,
그 함수 내에서 어떤 함수가 동작하는지, 다음에 어떤 함수가
호출되어야하는지 등을 제어한다.

  • 스크립트가 함수를 호출하면 인터프리터는 이를 호출 스택에 추가한 다음 함수를 수행하기 시작한다.
  • 해당 함수에 의해 호출되는 모든 함수는 호출 스택에 추가되고 호출이 도달하는 위치에서 실행한다.
  • 메인 함수가 끝나면 인터프리터는 스택을 제거하고 메인 코드 목록에서 중단된 실행을 다시 시작한다
  • 스택이 할당된 공간보다 많은 공간을 차지하면 "stack overflow" 에러가 발생한다.

태스크 큐(Task Queue)

태스크 큐는 자료구조 큐(Queue)를 기반으로 구성되어 있다.
큐는 먼저 들어온 것이 먼저나가는 FIFO(First In First Out)의 특징을 갖는다.
자바스크립트의 브라우저 엔진의 태스크 큐에는 비동기로 처리될 콜백 함수가 저장된다.
Call Stack이 비어있게 되면 태스크 큐에 있는 콜백 함수를 꺼내와 실행하게 된다.
태스크 큐에서 콜백 함수를 꺼내오는 과정은 이벤트 루프가 담당한다.


이벤트 루프

이벤트 루프는 Call Stack이 비어있으면 태스크 큐에서 콜백 함수를 꺼내 실행하는 역할을 한다.


Callback

콜백은 다른 함수의 인수로 넘기는 함수를 말하는데,
이 콜백을 가지고 비동기 프로그래밍을 할 수 있다.
하지만, 순수하게 콜백만 사용한다면 데이터 흐름이 조금만 복잡해져도
코드가 복잡해지는 문제가 생긴다.
아래는 콜백 지옥(Callback Hell)의 예시다.

function add(x, callback) {
    let sum = x + x;
    console.log(sum);
    callback(sum);
}

add(2, function(result) {
    add(result, function(result) {
        add(result, function(result) {
            console.log('finish!!');
        })
    })
})

// 실행 결과
// 4
// 8
// 16
// finish!!

Promise

Promise는 프로미스가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로,
비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다.
다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)을 반환한다.

Promise는 다음 중 하나의 상태를 가진다.

  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

대기 중인 프로미스는 값과 함께 이행할 수도,
어떤 이유(오류)로 인해 거부될 수도 있다.
이행이나 거부될 때, 프로미스의 then 메서드에 의해 대기열(큐)에 추가된 처리기들이 호출된다.
이미 이행했거나 거부된 프로미스에 처리기를 연결해도 호출되므로,
비동기 연산과 처리기 연결 사이에 경합 조건은 없다.

promise

예제

let myFirstPromise = new Promise((resolve, reject) => {
  // 우리가 수행한 비동기 작업이 성공한 경우 
  // resolve(...)를 호출하고, 실패한 경우 reject(...)를 호출함
  // 이 예제에서는 setTimeout()을 사용해 비동기 코드를 흉내냄
  setTimeout( function() {
    resolve("성공!");
  }, 250)
})

myFirstPromise.then((successMessage) => {
  // successMessage는 위에서 resolve(...) 호출에 제공한 값
  console.log("와! " + successMessage);
});

async & await

async와 await는 자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법이다.
기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완하고
개발자가 읽기 좋은 코드를 작성할 수 있게 도와준다.

기본 문법

async function 함수명() {
  await 비동기_처리_메서드_명();
}

먼저 함수의 앞에 async라는 예약어를 붙인다.
그리고 함수의 내부 로직 중 HTTP 통신을 하는 비동기 처리 코드 앞에 await를 붙인다.
비동기 처리 메서드가 꼭 Promise 객체를 반환해야 await가 의도한 대로 동작한다.

예제

function logName() {
  const dog = fetchDog('도메인 주소/dogs/1');
  if (dog.id === 1) {
    console.log(dog.name);
  }
}

위의 코드는 코드의 실행 순서를 보장받을 수 없다.
(fetchDog라는 함수는 서버에서 데이터를 받아오는 HTTP 통신 코드라고 가정)


아래와 같이 콜백을 사용하면 실행 순서를 보장받을 수 있다.

function logName() {
  const dog = fetchDog('도메인 주소/dogs/1', function(dog) {
    if (dog.id === 1) {
      console.log(dog.name);
    }
  });
}

async await를 사용하면 조금 더 간단하게 구현할 수 있다.

async function logName() {
  const dog = await fetchDog('도메인 주소/dogs/1');
  if (dog.id === 1) {
    console.log(dog.name);
  }
}

실용 예제

강아지 정보를 받아오는 fetchDog 함수와
강아지의 건강 정보를 받아오는 fetchHealth 함수가 있다고 가정

function fetchDog() {
  const url = 'https://mydogtest.com/dogs/1'
  return fetch(url).then(function(response) {
    return response.json();
  });
}

function fetchHealth() {
  const url = 'https://mydogtest.com/health/1';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

위 함수들을 실행하면 각각 강아지 정보와 건강 정보가 담긴 Promise 객체가 반환된다.

아래는 두 함수를 이용하여 강아지의 건강 정보를 출력하는 코드이다.

async function logHealth() {
  const dog = await fetchDog();
  if (dog.id === 1) {
    var health = await fetchHealth();
    console.log(health.firstInoculation); // true
  }
}

logHealth()를 실행하면 콘솔에 true가 출력될 것이고,
만약 콜백이나 프로미스를 사용했다면 코드의 가독성이 떨어졌을 것이다.
async await 문법을 이용하면 기존의 비동기 처리 코드 방식으로
사고하지 않아도 되는 장점이 생긴다.


async & await 예외 처리

async & await에서 예외 처리를 할 때는 try catch를 사용하면 된다.

async function logHealth() {
  try {
    const dog = await fetchDog();
    if (user.id === 1) {
      var health = await fetchHealth();
      console.log(health.firstInoculation); // true
    }
  } catch (error) {
    console.log(error);
  }
}

코드를 실행하다가 발생한 네트워크 통신 오류뿐만 아니라
간단한 타입 오류 등의 일반적인 오류까지도 catch로 잡아낼 수 있다.
발견된 에러는 error 객체에 담기기 때문에 에러의 유형에 맞게 에러 코드를 처리하면 된다.







Referenece

poiemaweb - 비동기

MDN - Call Stack

MDN - Promise

메시지 큐와 이벤트 루프 관련 블로그

콜백 함수 관련 블로그

async await 관련 블로그

profile
프론트엔드 개발자를 꿈꾸는

0개의 댓글