
JavaScript는 단일 스레드기반의 프로그래밍 언어다.
JavaScript 엔진은 단일 호출 스택(Call Stack)을 통해 함수 호출과 실행 순서를 관리한다.
장점
- 코드 작성과 디버깅이 용이
- 순차적인 실행으로 동시성 이슈 최소화
단점
- 하나의 작업이 끝나기 전까지 다른 작업을 처리할 수 없음
- 무거운 작업이 Call Stack에 오래 머물면 전체 앱이 멈출 수 있음
브라우저 환경에서는 일부 병렬 처리를 위해 다음과 같은 API를 제공한다.
Web Worker API
Service Worker API
1️⃣ Call Stack
현재 실행 중인 함수, 실행 예정인 함수를 후입선출(Last In First Out, LIFO) 구조로 관리
2️⃣ Event Loop
무한 루프를 돌며 Call Stack과 Queue 상태를 체크
Call Stack이 비면, 대기 중인 작업(이벤트)을 가져와 실행
3️⃣ Queue
비동기 작업의 콜백 함수들이 대기하는 공간
두 종류의 Queue가 존재
- Microtask Queue
- 가장 높은 우선순위.
- Event Loop 순회마다 가장 먼저 실행 Promise.then(), await, queueMicrotask()
- Macro Task Queue (Task Queue)
- Microtask Queue 다음에 실행
- setTimeout(), setInterval(), setImmediate(), I/O, UI 이벤트
Call Stack 비우기
Microtask Queue → Call Stack으로 작업 이동 및 실행
Microtask Queue가 비면
Macro Task Queue에서 하나 꺼내 Call Stack으로 이동
작업 실행 후 1번으로 반복
Microtask Queue의 모든 작업이 먼저 실행된 후 Macro Task Queue 작업이 실행된다.
setTimeout, fetch 같은 비동기 함수는 즉시 실행되는 것이 아니라
-> Task Queue에 적재하고
-> Call Stack이 비워진 뒤
-> Event Loop에 의해 실행된다.
이로 인해 비동기 코드가 병렬로 처리되는 것처럼 느껴지는 효과를 볼 수 있다.
React Native는 브릿지를 통해 JavaScript 코드와 Native 코드를 연결하며, 3개의 주요 Thread를 사용한다.
지금까지는 마치 걷는 방법을 배우지 않고 길을 걸을 수 있듯이, 익숙해진 코드를 작성하고 있었다. 직접 로그를 찍고 실행해 보면서 '이런 순서로 동작하겠다~'라고 예상했다. 이번에 문제를 마주해서 이렇게 알아보기 전까지는 event loop의 동작, macro와 micro queue에 어떤게 적재되고 순서는 어떻게 되는지도 부끄럽지만 잘 모르고 있었다.
이 블로그의 첫 글에서도 적혀 있지만, 처음 시작할 때 비동기가 그렇게 어려웠는데 이런 개념을 알고 있었다면 달랐을 거 같다.
이번에 배운 이 thread 개념을 앞으로 앱 개발에 참고하여 동작 중에 blocking 현상이나 delay로 인한 불편함을 줄여보고 싶다. 어떤 함수가 속도에 영향을 줄 수 있을지, context switching 등으로 인한 문제로 문제가 발생할 가능성이 있지 않을지 고민하면서 코드를 적을 수 있을 거 같다.
아직도 참 부족하지만, 오늘도 사용자를 위한 성능 개선을 위해 어떤 방법이 있을 지 재료를 얻을 수 있었다. 앞으로도 사용자들을 위해서 내가 할 수 있는 방법이 어떤게 있을 지 빨리 알아내고 싶다.