Krong Dev.
아카이브 목록

JS의 비동기는 사실 '동시성'이 아니다


오늘은 동기/비동기 처리에 대해서 배웠습니다.

동기(sync)처리란 한 태스크가 끝나기 전까지 다른 태스크는 대기하고 있는 상태를 말합니다. 비동기(async)처리란 한 태스크가 실행되는 중이더라도 다른 태스크도 같이 실행되는 상태를 말합니다.

일반적으로 동기 처리의 예시는 다음과 같습니다.

  • 변수 선언 및 연산 : const a = 1 + 1;
  • 순차적 함수 호출 : function A() , const B = () => {}
  • 반복문 : for, while, forEach 등

비동기 처리의 예시는 다음과 같습니다.

  • 타이머 : setTimeout(), setInterval()
  • 네트워크 요청 : fetch(), xhr(HttpHtmlRequest), 등
  • 이벤트 리스너 : onClick, 콜백함수 등
  • 파일 리더, 디비 접근

보통은 동기 처리에는 가벼운 연산을 부여합니다. 이유는 한 번에 하나의 작업만 처리하는데, 무거운 연산을 쥐어주게 되면, 다른 이벤트가 실행이 안되기 때문에 이런 경우에는 비동기 코드로 처리해야 합니다.

중요한 점은 멀티 스레드의 특징을 지닌 Java와는 달리, JavaScript는 싱글 스레드로만 이루어져 있어 한 번에 하나의 일만 합니다. (참고. I/O에서 file reader, 네트워크 요청같은 작업은 직접 기다리지 않고 시스템 커널이나 ’libuv’라는 네이티브에 위임됩니다. -> java의 멀티 스레드 대신 역할)

앞서 비동기는 ’어떤 태스크가 진행중이더라도 다른 태스크도 진행된다’고 했는데요, 그렇다면 비동기는 순서가 없는걸까요? 그렇지는 않습니다.

비동기 처리는 동시에 작업이 진행되는 것처럼 보일 수 있지만, 실제로 작업을 실행하는 순서는 정해져있습니다. 그 순서를 정해주는 것이 바로 ’이벤트 루프’입니다.

이벤트 루프를 이해하기 위해서는 세 가지 영역을 알아야 하는데요,

  • Call Stack (호출 스택): 현재 실행 중인 동기 작업들이 쌓이는 곳입니다. 자바스크립트는 여기서 하나씩 일을 처리합니다.
  • Web APIs (대기실): setTimeout, fetch 같은 비동기 작업이 실제로 타이머를 재거나 데이터를 기다리는 곳입니다.
  • Task Queue (줄 서는 곳): 비동기 작업이 끝나서 실행될 준비가 된 ’콜백 함수’들이 차례대로 줄을 서는 곳입니다.

이 영역들을 바탕으로 비동기 코드가 브라우저에서 실행됩니다.

예를 들어,

  • console.log();
  • setTimeout();
  • Promise.resolve().then()

이라는 코드가 작성되었다고 가정해봅시다.

  1. 여기서 console.log()는 동기 코드이기 때문에 가장 먼저 실행됩니다.
  2. Promise가 실행됩니다.
  3. setTimeout()이 실행됩니다.

왜 Promise가 더 먼저 실행될까요? setTimeout()과 Promise는 둘 다 비동기 코드이지만, setTimeout()은 매크로태스크, Promise는 마이크로태스크입니다. 비동기 작업에도 계층(계급)이 존재하기 때문에 가장 서열이 낮은 매크로태스크가 가장 나중에 처리되고, 가장 서열이 높은 마이크로 태스크는 콜 스택이 비워지고나서 바로 실행됩니다. (참고. 매크로태스크에도 서열이 존재합니다. poll(I/O) -> check(setImmediate) -> timer(setTimeout))

실제로 이러한 순서로 실행됩니다.

  1. console.log()는 동기처리임으로 콜 스택에서 즉시 실행됩니다.
  2. setTimeout()과 Promise가 차례대로 콜 스택에 들어왔다가 비동기 작업임을 확인하고 WebAPIs 영역으로 이동하여 각각 타이머와 비동기 처리를 시작합니다.
  3. WebAPIs에서 작업이 완료된 순서대로, Promise의 콜백은 마이크로태스크 큐에, setTimeout의 콜백은 태스크 큐에 줄을 섭니다.
  4. 이벤트 루프는 콜 스택이 비었음을 확인하고 우선순위가 높은 마이크로태스크 큐의 Promise 콜백을 실행합니다.
  5. 콜백이 다시 비워지면, 매크로태스크 큐의 setTimeout의 콜백을 실행합니다.

결국 JS는 Java와 다르게 싱글 스레드이기 때문에 ‘JS에서의 동시성’은 실제로 동시로 일어나는 것처럼 보이는 효율적인 연산 시스템입니다. 이러한 Java의 싱글 스레드 구조는 데드락(Deadlock)같은 복잡한 동기화 문제가 있기 때문에 이를 해결해준다는 점이 싱글 스레드의 대표적인 장점입니다. 아무리 서버에서 보안을 철저하게 한다고 하더라도 결제, 금융, 개인정보에 민감한 도메인의 경우 race-condition, deadlock 등 다양한 문제를 방지해야하기 때문에 도움이 되는 내용이라고 생각합니다.

  • #배움일기
  • #JavaScript