[JavaScript] Asynchronous - Callback, setTimeout

Steve·2021년 5월 19일
0

웹개발 코스

목록 보기
25/59

비동기적이란?
비동기 프로그래밍의 예시
비동기 함수

  • callback
  • setTimeout
  • setTimeout 의 원리

setTimeout 을 활용한 비동기 구현 연습

  • 순서가 필요없을 때
  • 순서가 필요할 때
  • Error handling
  • Callback hell 해결하기

비동기적(Asynchronous)이란?

Synchronous 는 "동시에 일어나는" 이라는 뜻이다. (한국어: 동기적)
Asynchronous 는 "동시에 일어나지 않는" 이라는 뜻이다. (한국어: 비동기적)

컴퓨터 프로그래밍에서의 "동기적"은, 요청에 대한 결과가 동시에 일어나는 것이다. 예를 들어 함수 func1 을 호출하여 func1 이 바로 실행되면 요청에 대한 결과가 동시에 일어난다고 볼 수 있다. 컴퓨터 코드는 기본적으로 순차적으로 실행되기 때문에 기본적으로 동기적으로 실행된다.

컴퓨터 프로그래밍에서의 "비동기적"요청에 대한 결과가 동시에 일어나지 않는 것이다. 예를 들어 func1 을 호출했는데, func1 이 바로 호출되지 않고 나중에 호출되도록 만들거나, 특정 조건을 만족할 때 호출된다면 요청에 대한 결과가 동시에 일어나지 않는 것이므로 "비동기적"이라고 할 수 있다.

비동기 프로그래밍의 예시

비동기 프로그래밍은 클라이언트와 서버와의 통신에서 주로 발생한다.
만약에 웹사이트가 유저의 닉네임을 서버로부터 불러와서 그 닉네임을 주황색으로 만들어 화면에 표시한다고 해 보자. 그럼 두가지 함수를 생각해볼 수 있다.

1) 서버에서 닉네임을 찾는 함수
2) 닉네임을 가지고 주황색으로 만드는 함수

먼저 서버에 닉네임을 찾는 함수가 실행된다. 만약 서버가 닉네임을 찾는데 10초가 걸린다고 해 보자. 그러면 그 10초 동안 아무것도 못하고 닉네임을 찾기만을 기다릴 것이다. 동기적 프로그래밍에서는 요청에 대한 결과가 나올 때까지 다른 요청을 할 수 없기 때문이다. 이것을 blocking 이라고 한다. 따라서 서버와 통신이 많은 상황에서 동기적 프로그래밍으로 모든걸 해결하기에는 너무 비효율적이고 느리다.

이러한 상황에서는 비동기적 프로그래밍이 필요하다. 비동기적 프로그래밍은 요청에 대한 결과가 동시에 일어나지 않아도 된다. 즉 컴퓨터는 서버에서 닉네임을 찾는 함수를 실행한 후 닉네임이 찾을 때까지 기다리는 것이 아니라, 다른 요청(작업)을 처리한다. 그러면서 닉네임을 받았는지 계속 확인하고, 닉네임을 받았다고 확인이 되면 그제서야 닉네임을 가지고 주황색으로 만드는 함수를 호출한다. 하나의 요청이 다른 요청을 막지 않기 때문에 Non-blocking 이라고 한다.

비동기 함수

callback 활용

함수에 callback 함수 를 넘겨준 뒤, 특정 조건에 맞을 때만 callback 을 실행하게 만든다.

// 예) 이벤트 핸들러인 addEventListener() 함수
// 상황(onclick)과 callback 을 인자로 받아 상황이 일어날 때만 callback 을 호출한다.
.addEventListner('onclick', callback);

setTimeout API

setTimeout()callback시간을 인자로 받아 지정한 시간 이 흐르면 callback 을 실행시켜주는 함수이다.

function printHello() { console.log('Hello!'); return 1; }
function printString(string) { console.log(string); return 1; }

// 1. 매개변수가 없는 함수를 callback 으로 전달할 때
setTimeout(printHello, 2000); // 2초 후에 printHello() 를 실행

// 2. 매개변수가 있는 함수를 callback 으로 전달할 때
// 틀린 코드
setTimeout(printString('world!'), 2000); // --- 1

// 1 처럼 실행하면 print('world')를 전달하는 것이 아니라 
// 인자를 전달할 때 바로 실행하여 print('world') 의 return 값을 전달하는 것이 된다. 따라서
// setTimeout(1, 2000) 
// 이 되어 버려 의도한 대로 작동하지 않는다.

// 따라서 매개변수가 있는 callback 을 넘겨주고 싶을 땐
// anonymous function 을 callback 함수로 제공하여 
// 그 안에서 print() 에 원하는 인자를 넣어 실행하면 된다.
// callback 을 받는 대부분의 JavaScript 함수를 이러한 방식을 사용한다.

// 맞는 코드
setTimeout(() => { 
  print('world!'); 
}, 2000);

setTimeout 함수의 구현 방식

다음 코드는 아래 영상처럼 작동한다. setTimeout 함수의 callback 은 정해진 시간 후 message queue 에 저장되며, call stack 이 비었을 때 stack 에 추가되어 실행된다.

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World'); // -- 1
networkRequest(); // -- 3
console.log('The End'); // -- 2


영상 : Understanding Asynchronous JavaScript | Medium

** setTimeout 함수는 API 에서 제공되는 함수이며, event loop, web API, message queue 를 사용한다. 이 구조는 자바스크립트 엔진이 아니라 자바스크립트 런타임 환경 (브라우저 혹은 node.js) 의 일부이다. (브라우저는 web API, node.js 는 C/C++ API).

callback 을 활용한 비동기 구현 연습

순서가 필요없을 때

클라이언트는 서버에서 데이터를 불러오는 경우가 많을 것이다. 그런데 이 작업은 여러가지 변수 및 환경에 따라 시간이 다르게 걸릴 수 있으므로, 걸리는 시간을 setTimeout 과 랜덤시간을 넣어서 비동기적인 상황을 구현해보자.

function printWord(word) { 
  setTimeout(()=> {
    console.log(word);
  }, Math.random() * 5000)
}

printWord('1');
printWord('2');
printWord('3');

위 코드를 실행하면 1 ~ 3 이 랜덤한 순서로 출력되어 나온다.

순서가 필요할 때

그런데 우리는 순서를 지켜야 하는 작업이 있을 것이다. 위에서 예시를 들었던 것 처럼 닉네임을 받아 온 후에 닉네임을 주황색으로 만들어 출력할 수 있을 것이다. 이처럼 함수간의 순서를 지켜야 하는 경우를 생각해보자. 매번 함수를 진행할 때 이전 함수의 결과값을 이어붙여서 값을 출력한다고 해보자. 즉 1을 받아서 12를 출력하고, 다시 12를 받아서 123 을 출력하는 식이다.

function printWord(word, callback) {
  setTimeout(() => {
    console.log(word);
    callback(word);
  }, Math.random() * 5000)
}

// 각 함수를 이전 함수의 callback 으로 넣어 준다.
printWord('1', (result) => {
  printWord(result + '2', (result) => {
    printWord(result + '3', (result) => {})
  })
})

// 1 12 123

위 코드대로 하면, random 시간 후에 1을 출력한 후 콜백을 호출하고, 그 콜백은 random 시간 후에 '1' + '2'를 출력하고 콜백을 호출하고, 그 콜백은 random 시간 후에 '12' + '3'을 호출하고 콜백을 호출한다. 따라서 1, 12, 123 순서대로 출력하게 된다.

함수들을 순서대로 실행하도록 만드는 것을 chaining 이라고 한다.

Error handling

그런데 만약 중간에 문제가 생겨서 결과를 만들어내지 못했다면?

예를 들어 컴퓨터 안에 벌레가 들어가서 첫번째 함수가 1 대신 undefined 를 출력했다고 하자. 우리는 undefined 뒤에 2를 이어붙이고 싶지 않기 때문에 chain 을 중단시켜야 한다.

즉, 각 함수의 분기를 다음과 같이 두개로 나눌 수 있다:

  1. 함수가 결과를 내는 데 성공했을 경우 - 다음 작업 진행
  2. 함수가 결과를 내는 데 실패했을 경우 - 특정 작업을 수행하고(보통 에러 출력) chain 을 종료시킴

위 개념을 예시로 만들어보자. 0 부터 9 사이의 숫자를 랜덤하게 하나 뽑고, 3초가 지난 후 만약 숫자가 0 ~ 4 라면 숫자를 출력한 뒤 다음 코드를 진행하고, 5 ~ 9 라면 에러를 출력하고 chain 을 종료시키는 코드를 만들어보자.

function randNum() { 
  return Math.floor(Math.random() * 10); 
}
function handleNum(number, callback) {
  setTimeout(()=> {
    if (number > 4) { // 실패했을 경우 callback 에 에러 전달
      let error = number;
      callback(error, null);
    } else { // 성공했을 경우 callback 에 값 전달
      callback(null, number);
    }
  }, 3000)
}

handleNum(randNum(), (error, result) => {
  if (error) { // 실패했을 경우 에러메세지를 출력
    console.log(`Error! The number was ${error}.`);
  } else { // 성공했을 경우 값을 출력하고 다음 코드 진행
    console.log(result);
    handleNum(randNum(), (error, result) => {
      if (error) {
        console.log(`Error! The number was ${error}.`);
      } else {
        console.log(result);
        handleNum(randNum(), (error, result) => {
          if (error) {
            console.log(`Error! The number was ${error}.`);
          } else { 
            console.log(result); 
          }
        })
      }
    })
  }
})

위와 같이 에러 핸들링을 위한 콜백 함수를 if (error)... else... 로 작성하는 스타일을 “error-first callback” 스타일이라고 하며 자주 사용되는 스타일이다.

Callback hell 해결하기

위에서 볼 수 있는것 처럼 콜백으로 순서를 만들어줄 경우 코드가 늘어나면 늘어날수록 옆으로 튀어나가는 모양을 한다. 그런데 만약 chaining 을 해야 하는 함수가 100개라면 어떻게 될까?
이렇게 되면 보기에도 좋지 않고, 수정이 필요한 경우 구조를 변경하기가 힘들며, 괄호 하나 잘못 써서 수시로 문법 에러가 날 것이다. 위 함수를 작성하는데도 자꾸 에러가 나서 힘들었는데, 100개는 상상조차 할 수 없다.

이처럼 chaining 을 위해 callback 이 지나치게 물리고 물리는 구조를 callback hell 혹은 pyramid of doom 이라고 한다.

Callback hell 을 해결하려면 어떻게 해야 할까? 방법 중 하나는 각 순서를 step1(), step2(), step3()... 같은 이름의 함수로 만들어주는 것이다.

function step1(error, result) {
  if (error) {
    console.log(`Error! The number was ${error}.`);
  } else {
    console.log(result);
    handleNum(randNum(), step2);
  }  
}

function step2(error, result) {
  if (error) {
    console.log(`Error! The number was ${error}.`);
  } else {
    console.log(result);
    handleNum(randNum(), step3);
  }  
}

function step3(error, result) {
  if (error) {
    console.log(`Error! The number was ${error}.`);
  } else {
    console.log(result);
  }  
}

handleNum(randNum(), step1);

각 단계를 함수로 만듦으로써 피라미드 구조를 해결했다.
그런데 각 단계가 쪼개져 떨어져 있기 때문에 코드 길이가 상당히 늘어나고, 각 단계를 일일이 함수 이름을 붙여 만들어주어야 하기 때문에 불편하다 (= "귀찮다").

이 불편함을 개선하기 위해 Promise 라는 객체가 만들어졌다.
Promise 는 함수의 chaining 과 error handling 을 하기 쉽도록 자바스크립트에서 지원하는 클래스이다.


Understanding Asynchronous JavaScript
https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff
Introduction: callbacks
https://javascript.info/callbacks

profile
게임과 프론트엔드에 관심이 많습니다.

0개의 댓글