비동기 함수는 어떻게 동작될까? 콜백 함수, 비동기와 동기 그리고 이벤트 루프

손연주·2021년 7월 3일
2

콜백 함수(Callback function)

함수(caller)의 인자(argument)로 전달되는 함수

  • 콜백 함수는 다른 함수가 부르는 함수
  • or 앞으로 큐에 쌓일 비동기식 콜백

함수를 인자로 받는 함수는 고차함수고, 그 인자인 함수가 콜백함수가 된다. 파라미터를 넘겨받는 함수는 콜백 함수를 필요에 따라 즉시 실행(synchronously)할 수도 있고, 아니면 나중에(asynchronously) 실행할 수도 있다. 비동기의 첫 시작이다.

자바스크립트는 동기적(정해진 순서에 맞게 코드가 순서대로 작동하는 거) 언어이다. 따라서 필요에 따라 비동기적으로 만들어주는 것이 중요하다.

비동기와 동기

동기(Synchronous)

  • Thread1이 작업을 시작 시키고, Task1이 끝날때까지 기다렸다 Task2를 시작한다.
  • 작업 요청을 했을 때 요청의 결과값(return)을 직접 받는 것이다.

blcoking

  • 처음 요청한 작업을 마칠 때까지 다음 작업이 계속 대기한다.
  • 동기적으로 처리해 스레드B(느린 동작)가 스택에 남아있는 것이다.

비동기(Asynchronous)

  • Thread1이 작업을 시작 시키고, 완료를 기다리지 않고, Thread1은 다른 일을 처리할 수 있다.
  • 작업 요청을 했을 때 요청의 결과값(return)을 간접적으로 받는 것이다.
  • 해당 요청 작업은 별도의 스레드에서 실행하게 된다.
  • 콜백을 통한 처리가 비동기 처리라고 할 수 있다.
  • 호출된 함수(callback 함수)가 작업 완료를 신경 쓴다.
    : 어떤 코드를 실행하면 콜백으로 받고 이걸 나중에 실행하는 것

non-blocking

비동기의 주요 사례

  • DOM Element의 이벤트 핸들러
    -마우스, 키보드 입력 (click, keydown 등)
    -페이지 로딩 (DOMContentLoaded 등)
  • 타이머
    -타이머 API (setTimeout 등)
    -애니메이션 API (requestAnimationFrame)
  • 서버에 자원 요청 및 응답
    -fetch API
    -AJAX (XHR)

비동기 작동 과정

https://www.youtube.com/watch?v=8aGhZQkoFbQ

JavaScript

: single-threaded language. This means it has one call stack and one memory heap.

  • 싱글 스레드(하나의 직렬로 처리하는 스레드실행되는 흐름의 단위 방식)
  • 하나의 싱글 콜스택과 메모리 힙을 가지고 있다.
  • 동시에 하나의 코드만 작동할 수 있다.
  • 다른 코드를 실행시키는 동안 ajax 요청이나 setTimeout을 실행할 수 없다.

콜스택

: 데이터 스트럭처로 실행되는 순서를 기억한다.

콜스택이 하는 일

함수 실행 -> 스택에 해당하는 함수를 집어넣고, 함수에서 리턴이 일어나면 스택의 가장 위쪽에서 해당 함수를 꺼낸다

Call stack

function multiply(a,b) {
  return a*b
}

function square(n) {
  return multiply(n,n);
}

function printSquare(n) {
  var squared = square(n);
  console.log(squared); //리턴은 없지만 암묵적으로 콘솔로그 리턴
}

printSquare(4);

Stack

multiply(n,n) // 세번째로 스택에 들어옴
square(n) // 두번째로 스택에 들어옴
printSquare(4) // 첫번째로 스택에 들어옴
main()

과정

  1. printSquare(4) 함수의 실행이 일어남 => 스택에 쌓임
  2. square(n)이 스택에 쌓임
  3. multiply(n,n)이 스택에 쌓임
  4. 스택에 쌓인 제일 위의 함수부터 차례대로 리턴

비동기 함수는 어떻게 동작되는가?

event loop

event loop가 하는일

콜스택과 테스크 큐를 주시하고 있다가 스택이 비어있으면 큐의 첫번째 콜백을 스택에 쌓아 실행한다.

stack : 메모리 할당이 일어나는 곳

  1. console.log('hi')가 첫번째 스택에 쌓이고 콘솔을 찍어 스택에서 나간다. 콘솔에 hi가 찍히고 스택이 비워진다.
  2. setTimeout(cb)이 스택에 쌓이고 콜백 함수가 webapis로 이동한다. 이때 setTimeout함수가 호출된 것이므로 스택에서 나간다.
  3. console.log('JSConfEU')가 스택에 쌓이고 콘솔에 JSConfEU가 찍히고 스택이 비워진다.
  4. Web API가 콜백을 테스크 큐에 밀어넣는다.
  5. 이때 이벤트 루프가 발동되어 큐의 첫번째 콜백을 스택에 쌓는다. 이벤트 루프는 '스택이 비어있으면' 실행한다. 5초가 지나 WebAPI가 태스크 큐에 콜백을 넣었다고 해도, 콜스택이 비지 않았다면 계속 태스크 큐에서 대기하며 기다려야 한다는 뜻이다. 따라서 setTimeout에 입력한 시간은 정확하게 지켜지지 않을 수도 있다.
  6. 콜백 함수가 실행되어 콘솔에 'there'이 찍히고 스택이 비워진다.

setTimout 함수를 0초 뒤에 설정했을 때

콜백 함수를 0초 뒤에 실행한다. 그럼 setTimout 자체를 왜 쓰는지 의문을 가질 수 있다. 이는 스택이 비어있을 때까지 기다리게 하기 위해서다. 0초로 설정된 코드가 0초 뒤에 바로 실행되지 않고, 차례를 기다린 후 실행된다.

  1. console.log('hi')가 첫번째 스택에 쌓이고 콘솔을 찍어 스택에서 나간다. 콘솔에 hi가 찍히고 스택이 비워진다.
  2. setTimeout(cb)이 스택에 쌓이고 콜백 함수가 webapis로 이동한다. webapis에 찍힌 콜백 함수는 바로 task queue에 쌓인다. setTimeout함수가 호출된 것이므로 스택에서 나간다.
  3. console.log('JSConfEU')가 스택에 쌓이고 콘솔에 JSConfEU가 찍히고 스택이 비워진다.
  4. 이때 이벤트 루프가 발동되어 큐의 첫번째 콜백을 스택에 쌓는다.
  5. 콜백 함수가 실행되어 콘솔에 'there'이 찍히고 스택이 비워진다.

more
우리는 콜백 함수가 동기적으로, 또는 비동기적으로 실행된다고 배웠다. setTimeout 0초와 함께 알아보자.

ex) forEach

[1,2,3,4].forEach((i) =>{
  console.log(i);
})

forEach는 콜백 함수를 받지만 비동기적으로 실행되진 않는다. 자신의 자체적 스택에서 실행시킨다.

  1. forEach 함수와 콜백 함수가 스택에 쌓인다
  2. 스택에서 console.log(i)를 실행한다
  3. console.log(1);
  4. console.log(2);
  5. console.log(3);
  6. console.log(4);
  7. 스택에서 forEach가 지워진다.

ex) 비동기식 콜백 함수를 받는 forEach

function asyncForEach(array, cb) {
  array.forEach(() => {
    setTimeout(cb,0);
  })
  
asyncForEach([1,2,3,4], function(i) {
  console.log(i);
})
  1. asyncForEach([1,2,3,4], function(i) { console.log(i);}) 가 스택에 쌓인다.
  2. array.forEach(() => {setTimeout(cb,0);}) 가 스택에 쌓인다.
  3. setTimeout(cb,0);가 스택에 쌓인다.
  4. setTimeout(cb,0)가 webAPIs로 넘어간다. 스택에서 나간다.
  5. setTimeout(cb,0)가 task queue에 쌓인다.
  6. 3~5번째를 4번 반복한다 (배열의 길이가 4니까)
  7. 스택이 전부 비워진다.
  8. 큐를 순서대로 출력한다. console.log(1),(2),(3),(4)

setTimeout함수를 0초로 불러서 비동기적으로 처리할 때의 차이점

웹 페이지는 스택이 깨끗해진 후에야 렌더한다.

각 배열 요소에 대해 오래 걸리는 처리를 한다고 할 때로 생각해보자. 동기적으로 그 느린 동작을 할 때마다 스택에는 값이 계속 쌓여있다. 그리고 그 느린 동작이 다 끝나고 나서야 완료되어 렌더를 할 수 있다. 이 말은 즉 스택에 무언가 쌓여있을 때는 렌더가 막히게 된다. 렌더가 막히면, 화면의 텍스트를 선택하거나 선택해서 반응을 보거나 하는 게 불가능해진다.

두번째 예시에서는 async time out을 큐에 쌓는 동안 스택에 쌓이지만 상대적으로 빨라지고 있다. 이때 우리는 렌더에게 각 요소 중간중간에 렌더가 끼어들 수 있는 기회를 줄 수 있다. 큐가 async를 통해 쌓였기 때문이다.

event loop를 막지 말라
-> 스택에 필요없는 느린 코드를 쌓아서 브라우저가 할일을 못하게 만들지 말아라 유동적인 UI를 만들어라.

profile
할 수 있다는 생각이 정말 나를 할 수 있게 만들어준다.

2개의 댓글

comment-user-thumbnail
2021년 7월 3일

"손연주보다 비동기에 대한 이해가 뛰어난 사람은 없을것이다.
그녀는 비동기의 신이다. "
-- L.DongUk--

1개의 답글