-
Notifications
You must be signed in to change notification settings - Fork 1
⚙비동기와 같은 작업들은 어떻게 처리했는가?
📚 목차
비동기와 같은 작업들은 useEffect를 적극 활용하고, 비동기와 관련된 코드들은 모두 trigger와 같은 버튼들로 처리하려 노력했다. 그 예로 가장 대표적인 것은 useFetchAPI와 Button.Component에서 prop으로 event 함수 또는 일반 함수들을 넘겨 받아 동작하게 했다.
- 🔗 ShortCut.component - ShortCut.component PR
- 🔗 useFetchAPI - useFetchAPI PR
하지만 useFetchAPI를 보면 Promise로 구성되어 있지만, 만약 useFetchAPI가 동시에 2개가 동작해야 한다면 이 또한 작업의 우선순위를 정하는 것이 중요해진다. 우리는 useFetchAPI를 구현하기 위해 다음과 같은 개념들을 학습하고 스터디를 했다.
멀티 스레드는 하나의 프로그램에서 둘 이상의 작업을 동시에 실행하는 기술로, CPU의 활용도를 극대화하기 위한 방법이다. 멀티 스레드가 항상 더 좋은 선택처럼 보이지만, 실제로는 장단점과 함께 각 기술에 따른 기회비용이 존재한다. 상황에 따라 싱글 스레드가 더 적합한 경우도 있다.
멀티 스레드의 작동은 Context Switching(문맥 교환)을 통해 이루어진다. 이는 하나의 스레드에서 다른 스레드로 전환되는 과정으로 전환 속도가 매우 빨라 사용자에게는 실시간으로 동시에 실행되는 것처럼 보이게 한다.
장점:
- 응답성 향상: 여러 작업이 동시에 실행되므로, 하나의 작업에서 오류가 발생하더라도 다른 스레드로 빠르게 전환되어 사용자 경험(UX)에 미치는 영향을 최소화할 수 있다.
- 병렬 처리: CPU 코어를 최대한 활용하여 대규모 작업을 병렬로 처리할 수 있다.
단점:
-
스레드 생성의 오버헤드:
- 새로운 스레드를 생성하고 관리하는 데 드는 비용이 높다.
- 작업이 간단하다면, 멀티 스레드가 싱글 스레드보다 느릴 수 있다.
-
동기화 문제:
- 스레드는 데이터와 힙 메모리 영역을 공유하기 때문에, 동시에 동일한 자원에 접근할 경우 데이터의 무결성이 손상될 수 있다.
- 이를 방지하려면 동기화 처리가 필요하며, 이는 추가적인 복잡성과 비용을 발생시킨다.
-
낭비되는 리소스:
- 작업이 간단하거나 병렬 처리가 필요 없는 경우, 멀티 스레드는 불필요하게 많은 리소스를 사용하게 된다.
싱글 스레드는 하나의 프로세스에서 오직 하나의 스레드로 작업을 실행한다. 이는 Context Switching이 필요 없으므로 멀티 스레드보다 단순하고 효율적일 수 있다.
장점:
-
낮은 오버헤드:
- Context Switching이 없으므로 추가적인 CPU 작업이 발생하지 않는다.
- 자원의 낭비가 적고, 필요한 작업만 효율적으로 처리한다.
-
동기화 작업 불필요:
- 스레드 간의 자원을 공유하지 않으므로 동기화 문제를 걱정할 필요가 없다.
- 구현이 간단하고 유지보수가 용이하다.
단점:
-
응답성 문제:
- 하나의 작업이 오래 걸릴 경우, 다른 작업이 대기 상태에 놓여 프로그램이 멈춘 것처럼 느껴질 수 있다 (예: 블로킹 I/O 문제).
-
병렬 처리 불가:
- CPU의 멀티코어 환경을 제대로 활용하지 못한다.
- 고성능이 요구되는 작업(예: 데이터 분석, 대규모 계산)에서는 비효율적이다.
-
Runtime:
- Runtime이란 특정 언어를 실행할 수 있는 환경을 말하며 js 엔진을 포함해 Web API와 Task Queue, Event Loop 등의 요소들이 있다. 따라서, js runtime은 브라우저와 Node.js를 기반으로한 환경에서 동작하고 있다.
-
Compiler:
- 자바스크립트는 컴파일이 없는 언어라고 종종 오해되고는 한다. 실제로는 자바스크립트도 컴파일 과정을 거치며 전통적인 언어(Java, C++ 등)의 명시적 컴파일 방식과는 다르다. 자바스크립트의 컴파일은 런타임 환경(예: 브라우저, Node.js)에서 이루어지며, 이를 JIT(Just-In-Time) 컴파일이라고 한다. JIT는 V8엔진 (또는 SpiderMonkey)에서 수행된다.
- 코드가 실행이 될 때 마다 쌓이는 공간을 의미한다. 따라서 작업, 명령을 한번에 하나의 작업만 수행 할 수 있다. 즉, 하나의 작업이 끝나야 하기 때문에 중간에 다른 작업이 끼어들 수 없다. Call Stack은 LIFO의 구조로 마지막에 들어온 것 부터 나가는 구조이다.
let first = () => {
console.log("First");
};
let last = () => {
first();
console.log("Last");
};
last();
호출하는 Stack의 구조를 표로 구현하면 다음과 같다.
Call 1 | Call 2 | Call 2' | Call 2'' | Call 3 | Call 3' |
---|---|---|---|---|---|
last(); |
*first() ⇒ console.log("First"); first(); last(); |
*first() ⇒ console.log("First"); POP first(); last(); |
first(); POP last(); |
*last() ⇒ console.log("Last"); last(); |
*last() ⇒ console.log("Last"); POP last(); |
될 수 있으면 Stack의 동작을 이해하고 있으면 좋다. 이렇게 우리가 작성한 모든 코드가 위에서 아래로 Parsing을 하면서 Call Stack의 구조로 동작하고 있다.
-
Call Queue는 이벤트 발생 후 호출되어야 할 콜백 함수들이 기다리는 공간이다. 여기서 말하는 Event란 무엇인가?
JavaScript에서 말하는 Event(이벤트)는 특정 상황이나 동작이 발생했음을 나타내는 신호를 의미하며
우리가 흔히 사용하는
onClick(e)
의 Event와도 연결 지을 수 있다. 하지만 코드에서의 Event가 아닌 전체적인 흐름으로 보아, Event는 조금 더 폭 넓은 개념이며 이를 이해해야 한다. -
큰 흐름에서 발생하는 Event는 다음과 같이 나눌 수 있다.
- 사용자 행동에 의해 발생하는 이벤트
- 브라우저 동작에 의해 발생하는 이벤트
- 비동기 작업의 완료로 발생하는 이벤트
- 커스텀 이벤트 (Custom Events)
-
앞서 말했던
onClick
과 같은 Event는 ‘사용자 행동에 의해 발생하는 이벤트’이다. 여기서 모든 내용들을 담아 낼 수 없으니 ‘비동기’와 연관이 되어 있는 부분만 짚어서 보자면 다음과 같다. -
브라우저 동작에 의해 발생하는 이벤트:
- DOM이 로드되었거나, 창 크기가 변경되었을 때를 의미한다. DOM이 로드되었다는 것은 웹 페이지의 구조와 내용을 브라우저가 이해할 수 있도록 만든 트리 형태의 지도가 완성되었음을 의미한다. 즉, HTML이 모두 로드되었다는 것인데, 이때 HTML뿐 아니라 script 태그로 연결된 JavaScript도 모두 로드되었음을 의미한다.
-
비동기 작업의 완료로 발생하는 이벤트:
- 네트워크 요청이 완료되거나 타이머가 만료되었을 때를 의미하며, 일반적으로 Web API에서 제공하는 setTimeout과 같은 요청의 완료를 말한다.
-
지금까지 Call Queue와 Event 동작에 대해 설명했다. Call Stack과 Call Queue는 역할이 별개이지만 서로
영향을 주고받는다. 예를 들어 Call Stack에
onClick
과 같은 Event가 발생하면 Call Queue가 이를 받아 처리한다.
- JavaScript의 Event-Loop는 Callback Queue에서 수행해야 할 작업이 있는지 반복적으로 확인하며, Call Stack이 비워졌을 때 작업을 Call Stack으로 이동시킨다. 이 과정에서 비동기 작업이 동기적인 처리 흐름을 방해하지 않도록 관리된다.
- 결론적으로 Event란 Call Queue에 등록된 상태까지를 말하며, Event-Loop는 Call Stack으로 작업을 전달하여 실행시키는 역할을 한다.
-
Event를 등록하는 Call Queue 파헤쳐보기:
- 앞서 Call Queue의 역할을 알아보았다. 하지만 Event의 동작 과정을 알아보기 위해 첨부한 GIF에서는 두 개의 Queue가 존재한다. MicroTask Queue와 MacroTask Queue이다. 이것들은 누구이며 무엇인가?
MicroTask Queue는 Job Queue라고 불리기도 한다. Promise, async/await와 같은 비동기 호출의 콜백 함수가 담겨 있다. MicroTask Queue에 등록된모든 콜백 함수가 처리될 때까지 계속 수행하는 것이 특징이다. 물론 then 절 또한 연쇄적으로 연쇄적으로 수행되며 모든 작업이 완료되어야 MicroTask Queue가 끝난다.
사용자가 스크롤을 이동하거나, 요소를 클릭하는 등 화면을 갱신해야 할 때 (requestAnimationFrame API를 사용했을 때)와 같이 브라우저 렌더링과 관련된 Task를 받는 Queue이다.
setTimeout, setInterval, onClick과 같은 비동기 호출의 콜백 함수가 담겨 있다. MicroTask Queue와 다르게 콜백 함수를 하나씩 실행한다. 즉, 콜백 함수 하나를 실행하면 이벤트 루프를 놓아주어 다른 동작을 수행할 수 있도록 한다.
만약 코드에서 Promise, requestAnimationFrame, setTimeout을 모두 실행시키면, 이벤트 루프가 각 Queue에 있는 Task를 다음과 같은 순서로 처리하도록 할 것이다:
- 처음에 Call Stack에 쌓여 있는 Task를 모두 처리.
- MicroTask Queue에 쌓여 있는 Task를 모두 처리.
- Animation Frames에 쌓여 있는 Task를 처리.
- MacroTask Queue에 쌓여 있는 Task를 하나씩 처리.
따라서 처리 순서는 MicroTask Queue → Animation Frames → MacroTask Queue가 된다.
- 동기와 비동기는 작업의 순서를 정하는 시스템이다. 즉, 작업 처리를 직렬로 처리하는지 병렬로 처리하는지의 차이이다.
- 스레드와 멀티 스레드는 작업의 환경이며, 동시에 작업을 처리하는지 아니면 하나씩 작업을 처리하는 환경인지의 차이이다. 따라서 JavaScript는 싱글 스레드이기 때문에 작업 환경상 하나씩 작업을 처리한다.
- 따라서 동기와 스레드의 개념과 크기는 애초부터 다른 것이며 혼동되어서는 안 된다.