JS는 싱글스레드 방식: 한 번에 하나의 작업만 처리할 수 있다는 의미다.
하지만 우리가 실제로 웹 애플리케이션을 만들 때는 여러 작업이 동시에 실행되는 것처럼 보이는데, 예를 들어 데이터를 불러오면서 동시에 애니메이션도 실행되고, 사용자 입력도 받을 수 있다.
이 과정에 대해서 알아보자
브라우저는 기본적으로 이벤트 루프라는 매커니즘으로 구성되어있다.
콜 스택: 현재 실행중인 코드가 쌓이는 곳.
자바스크립트는 이 스택에 있는 코드를 하나씩 실행됨
Web API: 브라우저가 제공하는 API로, setTimeout이나 fetch 같은 비동기 작업을 처리.
콜백 큐: Web API에서 처리가 끝난 작업들이 대기하는 곳. 콜 스택이 비어있을 때 이곳의 작업들이 실행됨.
console.log('1')
setTimeout(() => console.log('2'), 0)
console.log('3')
// 1 -> 3 -> 2
여기서 Promise 등장이후의 발전된 모델은 큐가 분리되어 아래 두개로 대체됨 .
마이크로태스크 큐: Promise의 콜백이 들어감 (더 높은 우선순위)
매크로태스크 큐 (= 기존 콜백 큐): setTimeout 등의 콜백, JavaScript 실행 Style 계산 Layout 계산 Paint 같은 랜더링 단계도 포함됨
console.log('시작');
setTimeout(() => {
console.log('타이머1');
Promise.resolve().then(() => console.log('타이머1의 프로미스'));
}, 0);
Promise.resolve().then(() => {
console.log('프로미스1');
setTimeout(() => console.log('프로미스1의 타이머'), 0);
});
console.log('끝');
// 시작 - 끝 - 프로미스1 - 타이머1 - 타이머1 프로미스- 프로미스1 타이머
async/await은 Promise를 더 동기적인 코드처럼 작성할 수 있게 해주는 문법이지만, 내부적으로는 동일하게 마이크로태스크로 처리됨
또한, try/catch를 통해 에러 처리가 더욱 직관적이다.
console.log('시작');
async function asyncFunc() {
console.log('async 함수 시작');
await Promise.resolve();
console.log('await 이후'); // 마이크로태스크 큐로 이동
}
asyncFunc();
console.log('끝');
// "시작" -> "async 함수 시작" -> "끝" -> "await 이후"
// 블로킹 예시
function heavyCalculation() {
for (let i = 0; i < 10000000000; i++) {
// 무거운 계산
}
}
console.log('시작');
heavyCalculation(); // 여기서 몇 초간 UI가 멈춤
console.log('끝');
위 코드와 같이 블로킹이 발생한다면 UI가 멈추거나, 스크롤, 애니메이션등이 제대로 작동하지 않아 전체적인 사용자 경험을 저하시킨다
1. 청크단위 처리
대표적으로 무한 스크롤 구현시, 청크 단위로 랜더링을 통해 최적화
데이터는 한번에 받아도 화면에서는 나눠서 그림
2. requestAnimationFrame
스무스 스크롤, 차트 애니메이션 등 데이터 시각화 기능에 적합한 최적화 방식
3. WEB worker 활용
WEB worker는 별도의 스레드를 통해 무거운 작업을 실행하고 그에대한 결과값을 메인스레드에 보내줌.
단, DOM에 접근불가하고, window 객체를 사용할 수 없어 UI 관련된 작업은 할 수 없음.
이는 이미지 처리나 대량 데이터 가공 같은 무거운 작업을 메인 스레드를 블로킹하지 않고 처리할 수 있게 해줍니다.