프로세스와 스레드 (+ 자바스크립트의 비동기 처리 메커니즘)
서론
컴퓨팅 환경에서 성능과 효율성을 극대화하기 위한 개념 중 하나는 바로 프로세스와 스레드이다. 이러한 개념은 운영체제에서 매우 핵심적인 역할을 한다. 본 글에서는 프로세스와 스레드의 개념부터 시작해, 자바스크립트의 이벤트 루프와 태스크 큐까지 아우르며 전반적인 실행 흐름을 정리하고자 한다.
1. 프로세스란?
프로세스는 프로그램이 운영체제 위에서 실제로 실행되는 상태, 즉 '작업 단위'를 의미한다. 여기서 프로그램이란 특정 작업을 수행하도록 작성된 코드 묶음으로, 하드디스크나 메모리에 저장되어 있다가 사용자의 요청이나 시스템 이벤트에 의해 실행된다. 프로그램은 단순히 하드디스크나 메모리에 저장된 정적인 명령어 모음이라면, 프로세스는 그 명령어들이 메모리에 올라가 CPU 제어를 받으며 동적으로 실행되는 상태이다. 운영체제는 각 프로세스에게 독립적인 메모리 공간을 할당하며, 이 공간에서 해당 프로그램이 실제로 동작하게 된다.
1-1. 프로세스에서 메모리 영역에 대한 설명
프로세스는 메모리 내에서 총 네 가지 주요 영역을 갖는다.
- 코드 (Code): 실행할 프로그램의 코드가 저장된다.
- 데이터 (Data): 전역 변수와 정적 변수가 저장된다.
- 힙 (Heap): 동적으로 할당된 메모리가 저장된다.
- 스택 (Stack): 함수 호출 시의 지역 변수, 매개변수 등이 저장된다.
1-2. Context Switching이란?
운영체제는 여러 프로세스를 동시에 다루기 위해 CPU를 빠르게 전환하며 실행한다. 이를 가능하게 하는 개념이 바로 컨텍스트 스위칭이다.
컨텍스트 스위칭은 CPU가 실행 중인 프로세스를 중단하고 다른 프로세스를 실행하기 위해 현재 상태(Context)를 저장하고, 이전에 저장된 상태를 복원하는 작업이다. 이는 멀티태스킹 환경에서 필수적으로 발생하며, 오버헤드가 존재한다.
이를 비유하자면, 여러 사람이 돌아가며 한 대의 컴퓨터로 작업하는 상황과 같다. 사용자가 바뀔 때마다 각자의 작업 환경을 저장하고 불러와야 하듯, CPU도 각각의 프로세스 상태를 저장하고 전환해야 한다.
1-3. 프로세스의 생명주기란?
프로세스는 다음과 같은 단계를 거친다:
- new (생성): 디스크에서 메모리로 프로그램이 올라가 실행 준비를 하는 상태
- ready (준비): 순서에 맞춰 처리를 기다리는 상태
- running (실행): 작업이 처리되고 있는 상태
- waiting (대기): 프로세스가 어떤 이벤트의 발생으로 기다리고 있는 상태
- terminated (종료): 실행이 완료되어 종료된 상태
1-4. 비선점 스케줄링과 선점 스케줄링이란?
스케줄링이란 CPU를 여러 프로세스에 효율적으로 분배하기 위한 운영체제의 핵심 메커니즘이다. 스케줄러는 대기 큐에 있는 여러 프로세스들 중 어떤 프로세스에 CPU를 할당할지 결정하며, 이 결정은 보통 프로세스의 우선순위, 도착 시간, 실행 시간 등을 기반으로 수행된다.
스케줄링의 목적은 다음과 같다:
- CPU의 활용률 최대화
- 프로세스의 대기 시간 최소화
- 전체 시스템의 처리량 최대화
이러한 목적을 달성하기 위해 운영체제는 다양한 스케줄링 알고리즘을 사용하며, 크게 비선점 스케줄링과 선점 스케줄링으로 나뉜다.
- 비선점 스케줄링은 한 번 CPU를 할당받은 프로세스가 스스로 종료하거나 대기 상태로 전환되기 전까지는 다른 프로세스에게 CPU를 양보하지 않는 방식이다. 주로 시스템이 단순하거나, 실시간 처리보다 안정적 순차 실행이 중요한 환경에서 적합하다. ex: 배치 처리 시스템, 내장형 시스템
- 선점 스케줄링은 운영체제가 일정 시간 혹은 우선순위 등의 조건에 따라 현재 실행 중인 프로세스를 중단시키고, 다른 프로세스에게 CPU를 할당할 수 있는 방식이다. 사용자 반응 속도가 중요한 데스크탑 환경, 모바일 운영체제, 실시간 시스템 등에서 많이 사용된다.
비유하자면, 스케줄링은 음식점에서 자리에 앉을 손님을 정해주는 매니저의 역할과 같다.
- 비선점 스케줄링은 한 손님이 자리에 앉으면 스스로 일어나기 전까지는 매니저가 개입하지 않는다.
- 선점 스케줄링은 손님이 너무 오래 앉아 있으면 매니저가 개입해 자리를 비우게 하고, 다른 손님에게 자리를 양보하게 한다.
즉, 비선점 방식은 공정하고 단순하지만 비효율적일 수 있고, 선점 방식은 효율적이지만 복잡한 제어가 필요한 구조이다.
2. 스레드란?
스레드는 프로세스 내에서 실행되는 작업의 흐름이다. 하나의 프로세스는 하나 이상의 스레드를 가질 수 있으며, 스레드들은 메모리 영역(코드, 데이터, 힙)을 공유하지만 스택 영역은 각각 독립적으로 가진다.
2-1. 멀티프로세스란?
멀티프로세스는 여러 개의 프로세스를 동시에 실행하는 방식이다. 각 프로세스는 독립적인 메모리 공간을 가지므로 안정성이 높지만, 프로세스 간 통신(IPC)이 복잡하고 비용이 크다.
2-2. 멀티스레드란?
멀티스레드는 하나의 프로세스 내에서 여러 스레드를 동시에 실행하는 방식이다. 메모리를 공유하기 때문에 통신 비용이 낮고 성능이 좋지만, 공유 자원 접근 시 동기화 문제가 발생할 수 있다.
2-3. 멀티스레드의 동기화 문제 (임계영역 문제)
여러 스레드가 동시에 하나의 자원에 접근할 경우, 자원의 상태가 예기치 않게 변경될 수 있다. 이를 임계 영역 문제라고 한다.
예시로, 다중 스레드 기반의 파일 압축 프로그램에서 두 스레드가 같은 파일 블록을 동시에 압축하려고 하면, 결과 파일이 손상되거나 압축률이 낮아질 수 있다.
3. 멀티프로세스와 멀티스레드의 비교
멀티프로세스와 멀티스레드는 모두 여러 작업을 동시에 처리하기 위한 구조이지만, 내부 동작 방식과 자원 관리 측면에서 큰 차이가 있다.
멀티프로세스는 각 프로세스가 독립적인 메모리 공간을 가지고 있어 하나의 프로세스에 문제가 생겨도 다른 프로세스에 영향을 미치지 않는다. 대신, 메모리를 별도로 할당받기 때문에 자원 소모가 크고, 프로세스 간 데이터를 주고받는 데 시간이 더 걸린다.
반면, 멀티스레드는 하나의 프로세스 안에서 여러 스레드가 메모리를 공유하기 때문에 자원을 효율적으로 사용할 수 있다. 하지만 공유 자원에 대한 동기화 문제가 발생할 수 있어 안정성 측면에서는 취약할 수 있다.
항목 | 멀티프로세스 | 멀티스레드 |
메모리 공간 | 독립적 | 공유함 |
안정성 | 높음 | 낮음 (공유 자원 충돌 가능) |
통신 속도 | 느림 | 빠름 |
자원 소모 | 큼 | 적음 |
4. 싱글스레드란?
싱글스레드는 하나의 스레드만으로 코드가 실행되는 환경을 말한다. 자바스크립트는 싱글스레드 언어로, 동시에 여러 작업을 처리하지 않고 하나씩 순차적으로 실행한다. 이러한 구조는 동기(Synchronous) 처리 방식에 기반한다.
- 동기(Synchronous): 작업이 직렬로 처리되며, 이전 작업이 완료되기 전까지 다음 작업이 시작되지 않는다. 예를 들어 서버에 데이터를 요청한 후, 응답을 받을 때까지 아무 일도 하지 않고 기다리는 구조이다.
- 비동기(Asynchronous): 작업이 병렬로 처리되며, 요청을 보낸 후 응답과 관계없이 다음 작업을 수행할 수 있다. 여러 작업이 동시에 실행될 수 있어 효율적인 처리가 가능하다.
4-1. 자바스크립트에서 싱글 스레드의 의미
자바스크립트는 단일 실행 컨텍스트를 가지며, 하나의 호출 스택에서 코드를 처리한다. 따라서 코드는 한 번에 하나씩 순차적으로 실행된다. 또한 자바스크립트는 하나의 함수가 실행되기 시작하면 해당 함수가 끝날 때까지 중단되지 않는다. 이를 run to completion이라 하며, 실행 중인 작업이 끝나기 전까지는 다른 작업이 끼어들 수 없다.
이 특성은 자바스크립트 개발자에게 동시성 문제를 복잡하게 고민하지 않아도 된다는 장점이 있다. 하지만 웹페이지 환경에서는 하나의 작업이 오래 걸릴 경우, 그동안 다른 작업이 실행되지 않아 사용자에게 웹페이지가 멈춘 것 같은 인상을 줄 수 있다.
4-2. 싱글 스레드와 멀티 스레드 비교
싱글 스레드는 병렬 작업 처리에는 한계가 있으며, 긴 작업이 하나 존재하면 전체 실행이 지연될 수 있다.
반면 멀티 스레드는 동시에 여러 작업을 처리할 수 있어 성능 측면에서 유리하다. 하지만 스레드 간에 메모리를 공유하기 때문에, 자원 충돌과 동기화 문제가 발생할 수 있으며, 구현 복잡성도 증가한다.
항목 | 싱글 스레드 | 멀티 스레드 |
실행 흐름 | 하나 | 여러 개 병렬 실행 |
자원 공유 | 공유 없음 | 메모리 공유 가능 |
구현 복잡성 | 낮음 | 높음 (동기화 필요) |
병렬 처리 성능 | 낮음 | 높음 |
5. 자바스크립트에서 비동기 코드를 처리할 수 있게 된 이유
자바스크립트는 싱글 스레드 구조이지만, 이벤트 루프와 태스크 큐(Callback Queue) 같은 비동기 메커니즘을 통해 병렬적인 코드 실행이 가능하다. 이러한 구조는 동기식 실행의 한계를 극복하고, 사용자와의 상호작용에서 끊김 없이 다양한 작업을 처리할 수 있도록 한다.
5-1. 이벤트 루프란?
이벤트 루프는 자바스크립트에서 비동기 작업을 관리하고 실행 순서를 조율하는 메커니즘이다. 자바스크립트는 싱글 스레드 기반 언어이기 때문에 동시에 여러 작업을 병렬로 실행할 수 없다. 그럼에도 불구하고 비동기적인 동작이 가능한 이유는 바로 이벤트 루프 덕분이다.
브라우저나 Node.js 환경에서는 사용자 이벤트, 타이머, 네트워크 요청 등의 작업을 백그라운드에서 처리하고, 그 결과를 자바스크립트 실행 흐름에 통합해야 한다. 이벤트 루프는 이러한 비동기 작업들이 적절한 시점에 실행되도록 호출 스택(Call stack)과 태스크 큐를 반복적으로 감시한다. 호출 스택이 비는 순간 태스크 큐에 쌓인 콜백을 꺼내 실행함으로써, 자바스크립트는 싱글 스레드 환경에서도 마치 병렬 처리가 가능한 것처럼 비동기 작업을 효과적으로 다룰 수 있다.
즉, 이벤트 루프의 핵심 역할은 다음과 같다:
- 호출 스택에 실행 중인 코드가 있는지 확인한다.
- 호출 스택이 비었으면, 태스크 큐에 대기 중인 작업이 있는지 확인한다.
- 태스크 큐에서 가장 오래된 작업을 꺼내 호출 스택에 넣어 실행한다.
이러한 루프가 무한히 반복되며 자바스크립트의 비동기 흐름을 만들어낸다.
5-2. 호출 스택
호출 스택(Call Stack)은 자바스크립트 엔진이 실행할 함수를 추적하고 관리하는 구조이다. 자바스크립트는 싱글 스레드로 동작하므로, 한 번에 하나의 함수만 실행할 수 있으며, 이 순서를 추적하기 위해 호출 스택을 사용한다.
호출 스택은 함수가 호출될 때 그 함수의 실행 정보를 스택에 쌓고, 실행이 끝나면 해당 정보를 제거하는 방식으로 작동한다. 이 구조는 후입선출(LIFO, Last In First Out) 방식으로, 가장 나중에 호출된 함수가 가장 먼저 실행을 마치고 빠져나가게 된다.
예를 들어 함수 A가 함수 B를 호출하고, 함수 B가 다시 함수 C를 호출하면, 호출 스택에는 A → B → C 순서로 쌓이게 되며, C가 실행을 마친 후 B, 그 다음 A 순서로 제거된다. 이렇게 호출 스택은 현재 실행 중인 함수와 그 실행 흐름을 정확히 파악하기 위한 핵심적인 메커니즘이다.
5-3. 태스크 큐
태스크 큐는 비동기 작업의 콜백 함수들이 실행 대기 상태로 들어가는 대기열이다. setTimeout, setInterval, DOM 이벤트 등으로 등록된 콜백 함수들은 비동기적으로 실행되며, 그 실행 타이밍이 도래했을 때 태스크 큐에 등록된다.
기본적으로 태스크 큐는 FIFO(First In First Out) 방식으로 동작한다. 일반적인 자료구조인 큐와 마찬가지로 먼저 들어온 작업이 먼저 처리되지만, 단순히 자료를 저장하는 것과 달리 이벤트 루프와 함께 동작하며 실행 타이밍을 제어한다는 점에서 일반 큐와 구분된다.
태스크 큐에 들어가는 태스크는, 특정 시점에 실행될 비동기 콜백 함수들이다. 예를 들어, 사용자가 버튼을 클릭했을 때 실행되도록 등록한 이벤트 핸들러나 setTimeout으로 등록한 콜백 함수가 이에 해당한다. 이러한 태스크는 호출 스택이 비워진 순간 이벤트 루프에 의해 호출 스택으로 이동되어 실행된다.
즉, 태스크 큐는 자바스크립트가 비동기 작업을 '언제 실행할지'를 결정하기 위한 대기 공간이며, 이벤트 루프와 협력하여 싱글 스레드 환경에서도 비동기 처리를 가능하게 만든 핵심 구성 요소 중 하나이다.
5-4. 호출 스택과 이벤트 루프가 코드를 처리하는 방식
자바스크립트는 함수 실행 흐름을 호출 스택이라는 자료구조를 통해 관리한다. 이벤트 루프는 이 호출 스택과 태스크 큐를 감시하며 적절한 타이밍에 비동기 작업을 실행하도록 돕는다. 아래의 두 가지 예제를 통해 동기 코드와 비동기 코드가 호출 스택과 이벤트 루프에서 어떻게 처리되는지 단계별로 살펴보자.
예시 1: 동기 코드 실행
function bar() {
console.log("bar");
}
function baz() {
console.log("baz");
}
function foo() {
console.log("foo");
bar();
baz();
}
foo();
- foo()가 호출되며 호출 스택에 foo가 쌓임
- console.log("foo")가 호출 스택에 쌓이고 실행됨 → "foo" 출력 // 아직 foo()는 존재
- bar()가 호출되며 호출 스택에 bar가 쌓임
- console.log("bar") 실행 → 출력 후 스택에서 제거
- bar() 실행 종료 → 호출 스택에서 제거
- baz()가 호출되며 호출 스택에 baz가 쌓임
- console.log("baz") 실행 → 출력 후 스택에서 제거
- baz() 실행 종료 → 호출 스택에서 제거
- foo() 실행 종료 → 호출 스택에서 제거
- 호출 스택 완전히 빈 상태
예시 2: 비동기 코드 실행 (setTimeout)
function foo() {
console.log("Start");
setTimeout(() => {
console.log("Async callback");
}, 0);
console.log("End");
}
foo();
- foo()가 호출되며 호출 스택에 foo가 쌓임
- console.log("Start")가 호출 스택에 쌓이고 실행됨 → "Start" 출력 → 호출 스택에서 제거
- setTimeout()이 호출됨 → Web API에 콜백 등록, 타이머 시작 → 호출 스택에서 제거
- console.log("End")가 호출 스택에 쌓이고 실행됨 → "End" 출력 → 호출 스택에서 제거
- foo() 실행 종료 → 호출 스택에서 제거
- 호출 스택이 완전히 비워짐
- 타이머가 만료되면 콜백 함수가 태스크 큐에 등록됨
- 이벤트 루프는 호출 스택이 비었음을 감지하고, 태스크 큐에서 콜백을 꺼냄
- console.log("Async callback")이 호출 스택에 쌓여 실행됨 → "Async callback" 출력 → 호출 스택에서 제거
출력 결과:
Start
End
Async callback
6. 태스크 큐와 마이크로 태스크 큐
6-1. 마이크로 태스크 큐란?
마이크로 태스크 큐(Microtask Queue)는 태스크 큐보다 우선순위가 높은 실행 대기 큐로, 이벤트 루프 사이클 내에서 호출 스택이 비워진 직후 바로 처리된다. 이 큐에는 주로 Promise를 사용해 등록된 콜백 함수들이 들어간다.
이벤트 루프는 호출 스택이 비면 먼저 마이크로 태스크 큐를 모두 비운 후, 태스크 큐에 들어 있는 작업을 순차적으로 처리한다. 이 우선순위 차이 때문에 마이크로 태스크는 태스크보다 빠르게 실행된다.
6-2. 태스크 큐와 마이크로 태스크 큐
태스크 큐(Task Queue)와 마이크로 태스크 큐(Microtask Queue)는 자바스크립트 런타임에서 비동기 작업을 처리하기 위해 사용하는 두 가지 주요 대기열이다.
- 태스크 큐(Task Queue)
- setTimeout
- setInterval
- setImmediate (Node.js)
- 사용자 인터랙션 이벤트 (클릭, 키보드 입력 등)
- 마이크로 태스크 큐(Microtask Queue)
- Promise.then
- Promise.catch
- Promise.finally
- queueMicrotask()
- MutationObserver
예시:
console.log("Start");
setTimeout(() => console.log("Task queue - Timeout"), 0);
Promise.resolve().then(() => console.log("Microtask queue"));
console.log("End");
출력:
Start
End
Microtask queue
Task queue - Timeout
이 예시를 통해 마이크로 태스크 큐의 작업이 태스크 큐보다 먼저 실행됨을 확인할 수 있다.
6-3. 렌더링은 언제 실행될까?
자바스크립트에서 DOM을 조작하는 코드가 실행되었다고 해서 즉시 렌더링이 발생하는 것은 아니다. 브라우저는 자바스크립트 코드 실행과 렌더링을 분리하여 처리하며, 렌더링은 이벤트 루프의 사이클 중 특정한 타이밍에만 발생한다. 렌더링은 일반적으로 1) 호출 스택이 비워지고 + 마이크로 태스크 큐의 모든 작업이 처리된 이후, 2) 태스크 큐의 작업을 실행하기 전에 수행된다.
먼저, 동기 코드의 경우에는 루프나 반복문을 통해 DOM을 조작하더라도 자바스크립트의 run-to-completion 특성상 해당 코드 블록이 모두 끝날 때까지 브라우저는 렌더링을 수행하지 않는다. 예를 들어 for 반복문 안에서 DOM 요소의 innerHTML을 반복적으로 수정하는 경우, 중간 과정은 모두 무시되고 최종 값만 렌더링된다. 이는 자바스크립트가 하나의 작업이 끝나기 전에는 어떤 작업도 끼어들 수 없도록 설계되어 있기 때문이다.
반면 setTimeout과 같은 API를 사용하면 콜백 함수가 태스크 큐에 등록되고, 호출 스택이 비워진 후 하나씩 실행된다. 각 콜백 함수가 실행된 뒤 브라우저는 렌더링을 수행할 기회를 얻기 때문에, DOM이 갱신되는 중간 과정을 시각적으로 확인할 수 있다. 이 방식은 수천 개의 DOM 변경 작업을 순차적으로 처리하면서도 사용자에게 점진적인 화면 반영을 제공할 수 있다는 장점이 있다.
마이크로 태스크 큐는 다르다. Promise.then 같은 작업은 호출 스택이 비워진 직후 곧바로 실행되며, 이벤트 루프는 마이크로 태스크 큐가 완전히 비워질 때까지 렌더링을 지연시킨다. 그 결과 수많은 DOM 조작이 마이크로 태스크로 등록되더라도, 브라우저는 해당 큐가 모두 처리된 이후에야 비로소 한 번 렌더링을 수행하게 된다. 따라서 마이크로 태스크를 사용한 대량의 DOM 갱신은 사용자 입장에서 중간 단계 없이 마지막 상태만 보이는 것처럼 느껴진다.
결국 렌더링이 일어나는 정확한 시점은 자바스크립트 코드의 실행 흐름과 큐의 우선순위, 그리고 브라우저의 렌더링 정책에 따라 달라진다. 동기 코드와 마이크로 태스크 큐는 렌더링을 지연시키는 반면, 태스크 큐는 렌더링을 개입시킬 수 있는 타이밍을 제공한다.
예시 1: 무한 마이크로 태스크 큐
Promise.resolve().then(function loop() {
queueMicrotask(loop);
});
이 경우 마이크로 태스크 큐가 계속 비워지지 않아 렌더링이 발생하지 않는다.
예시 2: 동기 코드, 태스크 큐, 마이크로 태스크 큐, 렌더링의 관계
<body>
<button id="sync">동기 코드: <span>0</span></button>
<button id="macro">태스크 큐: <span>0</span></button>
<button id="micro">마이크로 태스크 큐: <span>0</span></button>
<button id="both">모두 동시 실행: <span>0</span></button>
<script>
const sync = document.getElementById("sync");
const macro = document.getElementById("macro");
const micro = document.getElementById("micro");
const both = document.getElementById("both");
sync.addEventListener("click", () => {
for (let i = 0; i <= 10000; i++) {
sync.querySelector("span").innerHTML = i;
}
});
macro.addEventListener("click", () => {
for (let i = 0; i <= 10000; i++) {
setTimeout(() => {
macro.querySelector("span").innerHTML = i;
}, 0);
}
});
micro.addEventListener("click", () => {
for (let i = 0; i <= 10000; i++) {
Promise.resolve().then(() => {
micro.querySelector("span").innerHTML = i;
});
}
});
both.addEventListener("click", () => {
for (let i = 0; i <= 10000; i++) {
setTimeout(() => {
both.querySelector("span").innerHTML = i;
}, 0);
Promise.resolve().then(() => {
both.querySelector("span").innerHTML = i;
});
}
});
</script>
</body>
설명:
- 동기 코드: 버튼을 누르면 0부터 10000까지 반복하며 <span>에 바로 값을 입력하지만, 한 번에 처리되어 브라우저는 중간 과정을 렌더링하지 못하고 마지막 숫자만 보이게 된다.
- 태스크 큐 (setTimeout): 각각의 숫자 업데이트가 태스크 큐에 들어가므로 브라우저가 그 사이 렌더링을 수행할 수 있어 숫자가 실시간으로 변화하는 것처럼 보인다.
- 마이크로 태스크 큐 (Promise): 마이크로 태스크가 동기 코드 이후 바로 실행되므로 호출 스택과 마이크로 태스크 큐가 모두 소모된 후 렌더링이 수행되어 숫자 변화 없이 마지막 값만 보이게 된다.
- 모두 동시 실행: setTimeout과 Promise가 함께 쓰인 경우, Promise가 먼저 실행되므로 <span> 값은 마이크로 태스크로 인해 빠르게 최종값으로 바뀌며, 이후 setTimeout에서 같은 값으로 덮어쓴다. 대부분의 경우 마지막 숫자만 보이게 된다.
이러한 작업 순서는 브라우저에 다음 리페인트 전에 콜백 함수 호출을 가능하게 하는 requestAnimationFrame(브라우저가 다음 프레임을 렌더링하기 직전에 콜백을 실행할 수 있도록 해주는 API)을 통해서도 확인할 수 있다.
console.log('a'); // 동기
setTimeout(() => { console.log('b') }, 0); // 태스크
Promise.resolve().then(() => { console.log('c') });// 마이크로태스크
requestAnimationFrame(() => { console.log('d') }); // rAF
위 코드를 실행하면 a → c → d → b 순서로 출력된다.
결론적으로, 동기 코드는 물론이고 마이크로 태스크 또한 렌더링 타이밍에 영향을 줄 수 있다. 따라서 복잡한 연산이나 반복 작업을 렌더링과 분리할 수 있도록, 작업을 태스크 큐로 넘기거나 requestAnimationFrame을 활용하는 등의 전략이 필요하다.