치춘짱베리굿나이스

비동기와 콜백 함수 ver. 2023 본문

Javascript + Typescript/이론과 문법

비동기와 콜백 함수 ver. 2023

치춘 2023. 9. 19. 16:06

비동기와 콜백 함수 ver. 2023

https://blog.chichoon.com/520

 

비동기 처리와 Promise

비동기 처리와 Promise 동기 (Synchronous) 와 비동기 (Asynchronous) const [testValue, setTestValue] = useState(0); const handleOnClick = () => { setTestValue(testValue + 1); setTestValue(testValue + 1); setTestValue(testValue + 1); setTestValue(t

blog.chichoon.com

비동기와 Promise에 관해서 한번 글을 적은 적이 있었는데

이번에는 2023 ver. 느낌으로 좀 더 업그레이드해서 적어보려 한다

맨날 공부해도 헷갈리는 Promise, async / await 키워드까지 내가 공부한 바를 한번 꽉꽉 눌러담아보자

근데 글이 너무 길어져서 결국 비동기와 콜백 함수부터 분리하기로 함

자바스크립트 엔진 구조와 비동기 처리 방식

동기와 비동기

https://chichoon.tistory.com/946

 

블로킹, 논블로킹, 동기, 비동기

이거자꾸헷갈려서적음 이런걸 블로킹이라고 한다 엄청나게 헷갈려서 (…) 엄청나게 많은 블로그 글들을 뒤져가면서 머리속으로 짱구를 요리조리 굴리고 있는데 역시나 엄청나게 헷갈린다 동기

blog.chichoon.com

여기서 잠깐! 동기와 비동기를 잠깐 훑고 가자

  • 동기란?
    • 앞의 작업이 끝날 때까지 기다렸다가 다음 작업으로 넘어가는 방식
    • 앞의 작업의 결과값이 뒤의 작업에 영향을 주는 경우가 많다
  • 비동기란?
    • 앞의 작업이 끝나지 않았더라도 다음 작업을 병렬로 수행하는 방식
    • 한 번에 여러 작업을 처리하기 때문에 효율적이다
    • 앞의 작업의 결과값이 뒤의 작업에 영향을 끼치지 않는다

쉽게 말하자면 동기는 직렬 처리 (?), 비동기는 병렬 처리라고 생각하면 되겠다

자바스크립트에서는?

자바스크립트는 단일 스레드 기반의 언어이다

단일 스레드 기반이라는 것은? 멀티 스레딩이 되지 않기 때문에 한번에 하나의 작업만을 (하나의 스레드에서) 수행할 수 있다

그러면 뭔가 당연히 비동기로 동작할 수 없어야 할 것 같다…만 자바스크립트는 비동기로 동작할 수 있기 때문에, 단일 스레드임에도 불구하고 여러 가지 작업을 복합적으로 수행할 수 있다

이게 대체 왜 되는 걸까? 그 비밀은 바로 자바스크립트를 구동시키는 브라우저에 있다

자바스크립트 엔진의 구조

  • 메모리 힙
    • 참조 타입 (쉽게 말해, 객체) 이 저장되는 장소이다
    • 다른 언어들과 비슷하게, 동적으로 메모리를 할당할 수 있는 장소이기 때문에 크기가 들쑥날쑥한 객체들이 주로 저장된다
  • 콜 스택
    • https://blog.chichoon.com/921
    • 여기 정리해 놓은 실행 컨텍스트들이 콜 스택으로서 쌓이는 곳이다
    • 원시 타입 (number, string, boolean, symbol, null, undefined, BigInt) 값들과, 참조 타입 값의 주소값이 실행 컨텍스트 내부에 저장되기도 한다

근데 이렇게만 보면 비동기 로직이 실행될 여지가 전혀 없어 보인다

단일 콜 스택으로만 이루어져 있으면 당연히 단일 스레드로만 동작하는 것이 아닌가요? ㅇㅅㅇ.;;

브라우저 런타임 구조

실제 브라우저는 엄청 복잡하지롱 (…)

당연하지만(?) 브라우저에서는 자바스크립트 엔진만으로 돌아가지 않고, 그렇기 때문에 런타임 환경에서는 비동기 동작이 가능한 것이다

간단한 그림들로 각 구성요소들이 무슨 역할을 하는지 알아보자

 

  • 웹 API (Web API)
    • 웹 페이지에서 발생하는 여러 DOM 이벤트들 (onclick, onchange 등) 이나, ajax를 통한 데이터 받아오기, 1시간짜리 setTimeout 등은 비동기로 처리하지 않으면 사용자 경험에 치명적인 영향을 끼친다
    • 한 이벤트가 처리될 때까지 사용자가 아무런 행동도 할 수 없다고 생각해보자…
    • 브라우저에서는 몇몇 동작에 대해 비동기로 처리할 수 있도록 API를 제공하는데, 이것이 바로 웹 API (Web API) 이다
    • 콜 스택에서 실행된 비동기 함수는 웹 API로 넘어가 각 API들이 적절히 처리하게 된다
    • 모든 함수가 웹 API에 의해 처리되는 것이 아니고, 몇몇 함수들만 실행 환경 (브라우저, node.js) 의 도움을 받아 비동기식으로 동작한다

 

  • 콜백 큐 (Callback Queue = 태스크 큐, Task Queue)
    • 웹 API에서 적절히 처리된 비동기 함수들은 콜백 큐에 적재된다
    • 이곳은 비동기 함수들을 보관하는 장소로, 이벤트 루프에 의해 꺼내지기 전까지는 계속 선입선출 방식으로 보관되어 있게 된다
  • Job Queue
    • 잡 큐는 ES6에 처음 추가되었으며, ES6 기점으로 콜백 함수가 Task 와 Job 의 두 종류로 나뉘게 되었다
      • Task에는 기존의 콜백 큐에 들어가던 DOM 이벤트, setTimeout 등이 있다
      • Job에는 Promise 등이 있다
    • Job Queue의 우선순위가 Callback Queue보다 높으며, 콜 스택이 비었을 경우 Job Queue에 있는 (처리된) 콜백 함수를 콜백 큐의 콜백 함수보다 먼저 넣는다

 

  • 이벤트 루프 (Event Loop)
    • 지속적으로 콜 스택과 콜백 큐 (와 잡 큐) 를 감시하는 역할을 한다
    • 콜 스택이 비었다면, 콜백 큐에 있는 (처리된) 비동기 함수들을 콜 스택에 밀어넣는다
    • 콜 스택에 밀어넣어진 함수는 콜 스택에 의해 실행된다

자바스크립트에서의 비동기 실행 방식

const foo = () => {
    console.log("b");
}

console.log("a");
setTimeout(foo, 1000);
console.log("c")

2022년의 그 자료를 다시 가져와보았다…

  1. console.log(”a”) 가 콜 스택에 들어간다 (push)
  2. console.log(”a”) 가 실행되고, 콜 스택에서 제거된다 (pop)
  3. setTimeout(foo, 1000) 이 콜 스택에 들어간다 (push)
    1. 이때 setTimeout(foo, 1000) 은 콜 스택에서 처리하지 않고, webAPI로 넘어간다
    2. setTimeout(foo, 1000) 이 webAPI로 넘어갔으므로, 콜 스택에서 제거된다 (pop)
  4. console.log(”c”) 가 콜 스택에 들어간다 (push)
  5. console.log(”c”) 가 실행되고, 콜 스택에서 제거된다 (pop)
    • 이 시점에서 콜 스택이 비게 된다
  6. 1초 후, setTimeout(foo, 1000) 이 webAPI에서 처리되었다
  7. webAPI는 콜백 큐에 foo 함수를 반환한다 (enqueue)
  8. 이벤트 루프는 콜백 큐와 콜 스택을 확인하고, 콜 스택이 비어 있는 것을 발견한다
  9. 이벤트 루프가 콜백 큐의 foo를 꺼내 콜 스택으로 밀어넣는다 (dequeue + push)
  10. 콜 스택에 의해 foo가 실행된다
  11. console.log(”b”) 가 콜 스택에 들어간다 (push)
  12. console.log(”b”) 가 실행되고, 콜 스택에서 제거된다 (pop)
  13. foo 함수가 종료되고, 콜 스택에서 제거된다 (pop)

web API로 넘어가는 비동기 함수들은 콜 스택이 비어있을 때만 콜 스택에 적재되므로, setTimeout의 시간을 아무리 짧게 줄인다고 해도 호출 순서는 acb로 유지된다

콜백 함수

비동기 작업의 문제점 - 흐름 예측

비동기 작업의 문제점으로 어떤 작업이 언제 끝날지 모른다는 것이 꼽힌다

fetch 함수를 예로 들면, 응답이 언제 돌아올 지 우리는 알 수가 없다

어떤 값을 서버에 요청해서 응답을 받고 → 그 응답받은 값을 이용해서 또 다른 서버에 요청해서 응답을 받고 → 이런 방식이라면 각 요청에 대한 응답이 언제 돌아올지 모르기 때문에 코드를 작성하는 데 어려움이 발생한다

위의 흐름 예측이 불가능하다는 점을 보완하기 위해 사용하는 것이 콜백 함수이다

콜백 함수란?

function foo() {
    console.log("hi");
}

const bar(callback) {
    console.log("callback: ");
    callback();
}

bar(foo);

비동기가 어떻게 동작하는지 알았으니, 비동기 실행에서 자주 사용되는 기법인 콜백 함수를 알아보자

자바스크립트는 특이하게 함수를 일급 객체 취급하여 다른 함수의 인자로 넘겨줄 수 있는데, 이처럼 다른 함수의 인자로 받아와져 내부에서 실행되는 함수를 콜백 함수라고 한다

위의 예시에서 foo는 콜백 함수로 사용되고 있다

 

function handleButtonClick() {
    console.log("clicked!");
}

document.querySelector(".button").addEventListener("click", handleButtonClick);

인자로 넘겨주어 내부에서 실행되는 함수 뿐만 아니라, 어떠한 이벤트의 결과로 실행되는 함수도 콜백 함수라고 한다

위의 예시에서 handleButtonClick 은 콜백 함수로 사용되고 있다

콜 백… call back… 뒤에 호출된다… “특정한 동작 뒤에 호출되는 함수” 라는 뜻을 상기시키면 조금 이해가 된다

정리하자면, 콜백 함수는 어딘가에 함수를 등록해 놓고, 특정 이벤트나 함수에서의 특정 시점에 실행되는 함수라고 할 수 있다

콜백 함수는 비동기 이벤트가 완료된 시점에 호출되는 것이 보장되므로, 비동기 작업을 마치고 후속 동작을 정의하기에 적절한 방법이다

콜백 함수의 사용처

setTimeout(foo, 1000) 
// 여기서 foo는 콜백 함수이다

document.querySelector(".button").addEventListener("click", handleButtonClick);
// 여기서 handleButtonClick은 콜백 함수이다

평범한 함수의 인자로 넘겨서 특정 동작 후에 실행되도록 하는 경우도 있지만, 대부분은 비동기 처리를 할 때 특정한 비동기 동작이 끝난 후 실행됨을 보장하고 싶을 때 많이 사용한다

  • setTimeout 의 첫 번째 인자로 넘기는 콜백 함수는 두 번째 인자로 넘겨준 ms 만큼의 시간이 흐른 뒤 호출됨을 보장한다
  • DOM 요소의 이벤트 핸들러로 넘겨지는 콜백 함수는 해당 이벤트 (click, change, focus, blur…) 가 처리된 뒤 호출됨을 보장한다

비동기 작업의 문제점 - 콜백 지옥

foo(() => {
    bar(() => {
        baz(() => { 
            console.log("hi"); //          >-->        >--->
        }, 1000);
    }, 1000);
}, 1000);

하지만 뭐든지 과하게 쓰면 문제가 되는 법… 과한 콜백은 콜백 지옥을 낳는다

여러 비동기 작업이 중첩되면서 콜백 함수도 중첩되고, 가독성을 완전히 망쳐버리게 된다

예를 들어 위의 예시의 경우, foo 함수의 콜백 함수로 bar이 호출되고, bar 함수의 콜백 함수로 baz 함수가 호출되고…

마치 전투기처럼 삼각형으로 코드가 구성되는 것을 볼 수 있다

콜백 지옥 하면 딱 떠오르는 이미지가 바로 이것일 것이다

콜백 함수를 익명 함수 (화살표 함수 등) 로 전달하는 과정에서 코드의 복잡성이 증가하는 현상을 콜백 지옥이라고 한다

딱 봐도 디버깅이 힘들게 생겼다 (당장 위의 예제를 만들 때에도 중괄호 위치가 너무 헷갈렸다…)

다음 포스팅

다음 포스팅에는 콜백 지옥을 어느 정도 해결할 수 있는 Promise에 대해 알아보자

참고 자료

자바스크립트 엔진 구조, 런타임 환경

https://seokzin.tistory.com/entry/JavaScript-JavaScript-엔진-구조-Call-Stack-Memory-Heap

https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/

https://blog.toktokhan.dev/t-767eb0fa38f3

https://velog.io/@dahyeon405/자바스크립트-동작-원리-이벤트-루프를-통한-비동기-처리

https://medium.com/sjk5766/javascript-비동기-핵심-event-loop-정리-422eb29231a8

콜백 함수

https://www.freecodecamp.org/korean/news/https-www-freecodecamp-org-news-javascript-callback-functions-what-are-callbacks-in-js-and-how-to-use-them/

'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글

호이스팅  (0) 2023.08.24
실행 컨텍스트  (0) 2023.08.23
any, unknown, never  (0) 2023.08.21
var, let, const 차이점  (0) 2023.08.17
문자열의 특정 문자 변경하기  (0) 2023.08.08
Comments