메인 스레드는 당신 것이 아니다
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line and all formatting exactly as you requested.
메인 스레드는 사용자의 리소스입니다
사용자가 사이트나 앱을 방문하면 브라우저는 단일 스레드를 할당합니다:
- 여러분의 JavaScript 실행,
- 사용자와의 상호작용 처리, 그리고
- 화면에 보이는 것을 그리기.
이것이 메인 스레드이며, 여러분의 코드와 이를 사용하는 사람 사이의 직접적인 연결 고리입니다.
개발자는 종종 중급 스마트폰부터 고성능 게이밍 PC까지 다양한 사용자의 기기를 고려하지 않고 메인 스레드를 사용합니다. 메인 스레드는 우리에게 속한 것이 아니라 사용자에게 속합니다.
“우리는 사용자의 메인‑스레드 예산을 마치 무료인 것처럼 소모하고, 인터페이스가 깨진 듯 느껴질 때 놀라워합니다.”
JavaScript를 실행하는 매 밀리초는 브라우저가 다음을 수행할 수 없는 매 밀리초와 같습니다:
- 클릭에 응답하기,
- 스크롤 위치 업데이트하기,
- 키 입력을 인식하기.
코드가 오래 실행되면, 추상적인 “지연”을 일으키는 것이 아니라, 여러분에게 말을 걸려는 사람을 무시하는 것입니다.
메인 스레드는 한 번에 하나의 작업만 수행할 수 있기 때문에, 여러분의 JavaScript가 실행되는 동안 다른 모든 작업은 대기합니다:
- 클릭이 대기열에 쌓이고,
- 스크롤이 멈추며,
- 키 입력이 여러분이 곧 끝내기를 바라며 쌓입니다.
코드가 50 ms 정도 걸리면 눈에 띄지 않지만, 500 ms가 되면 인터페이스가 느리게 느껴지고, 몇 초가 지나면 브라우저가 페이지를 완전히 종료하겠다는 제안을 할 수도 있습니다.
사용자는 여러분의 코드가 실행되는 모습을 보지 못합니다. 그들은 단지 깨진 경험을 보고 스스로를 탓하고, 이어서 브라우저를 탓하고, 마지막으로 여러분을 탓하게 됩니다.
인간 인지와 200 ms 예산
Browser vendors have spent years studying how humans perceive responsiveness. The research converged on a clear threshold:
| 지연 시간 | 인지된 경험 |
|---|---|
| ≤ 100 ms | 즉시 |
| 100 – 200 ms | 눈에 띄는 지연 |
| > 200 ms | 불량 |
The industry formalized this as the Interaction to Next Paint (INP) metric—anything over 200 ms is considered poor and now influences search rankings.
That 200 ms budget isn’t just for your JavaScript. The browser also needs time for style calculations, layout, and painting, so your code gets what’s left—roughly ≈ 50 ms per interaction before the experience starts to feel sluggish. That’s the allocation you have from a resource you don’t own.
좋은 게스트가 되도록 도와주는 API들
웹 플랫폼은 메인 스레드에서 벗어나도록 진화해 왔습니다. 이러한 API들은 브라우저 엔지니어들이 개발자들이 불필요하게 스레드를 차단하는 모습을 보며 지쳤기 때문에 존재합니다.
Web Workers
완전히 별도의 스레드에서 JavaScript를 실행합니다. 대용량 데이터 파싱, 이미지 처리, 복잡한 계산 등 무거운 연산을 워커에서 수행하면 메인 스레드가 전혀 차단되지 않습니다.
// Main thread: delegate work and stay responsive
const worker = new Worker('heavy-lifting.js');
// Send a large dataset from the main thread to the worker
// The worker then processes it in its own thread
worker.postMessage(largeDataset);
// Receive results back and update the UI
worker.onmessage = (e) => updateUI(e.data);
Workers는 DOM에 접근할 수 없습니다, 하지만 이 제약은 의도된 것으로, “작업”과 “상호작용” 사이에 명확한 분리를 강제합니다.
requestIdleCallback
브라우저에 여유가 있을 때만 코드를 실행합니다. (WebKit 버그로 인해 Safari 지원은 아직 진행 중입니다.) 사용자가 활발히 상호작용하고 있을 때는 콜백이 대기하고, 상황이 조용해지면 코드가 실행됩니다.
requestIdleCallback((deadline) => {
// Process tasks from a queue you created earlier.
// deadline.timeRemaining() tells you how much time you have left.
while (tasks.length && deadline.timeRemaining() > 0) {
processTask(tasks.shift());
}
// If there are tasks left, schedule another idle callback to finish later.
if (tasks.length) {
requestIdleCallback(processRemainingTasks);
}
});
분석, 프리페치, 백그라운드 업데이트와 같은 비긴급 작업에 이상적입니다.
navigator.scheduling.isInputPending
(현재는 Chromium 전용.) 이 API를 사용하면 작업 중간에 누군가가 입력을 기다리고 있는지를 확인할 수 있습니다.
function processChunk(items) {
// Process items from a queue one at a time.
while (items.length) {
processItem(items.shift());
// Check if there’s pending input from the user.
if (navigator.scheduling?.isInputPending()) {
// Yield to the main thread to handle user input,
// then resume processing after.
setTimeout(() => processChunk(items), 0);
// Stop processing for now.
return;
}
}
}
“누군가 내 주의를 끌려고 하는가?” 라고 명시적으로 물어보고, 답이 ‘예’라면 작업을 멈추고 그들에게 기회를 줍니다.
미묘한 메인‑스레드 범죄
명백한 범죄—무한 루프, 100 000개의 테이블 행 렌더링—는 쉽게 눈에 띈다. 미묘한 범죄는 무해해 보인다.
- 큰 API 응답에 대한
JSON.parse()는 파싱이 완료될 때까지 메인 스레드를 차단한다. 개발자 머신에서는 즉시 처리되는 것처럼 느껴지지만, CPU가 제한되고 다른 탭이 경쟁하는 중급 스마트폰에서는 300 ms가 걸릴 수 있으며, 그 동안 사용자의 모든 상호작용을 무시한다.
메인 스레드는 점진적으로 성능이 떨어지지 않는다; 반응성이 있거나 없거나이며, 사용자는 여러분이 아마도 테스트하지 않은 환경에서 코드를 실행하고 있다.
Source: …
관리할 수 없는 것을 측정하기
측정하지 못하면 관리할 수 없습니다. Chrome DevTools의 Performance 패널은 메인 스레드 시간이 어디에 쓰이는지 정확히 보여줍니다—어디를 봐야 할지만 알면 됩니다.
- Performance 패널을 열고 세션을 기록합니다.
- “Main” 트랙을 찾습니다.
- JavaScript 실행이 긴 노란색 블록으로 표시된 부분을 확인합니다.
- 50 ms 를 초과하는 작업은 빨간색 음영으로 표시되어 초과된 부분을 강조합니다.
가이드가 필요하면 Insights 창을 사용해 긴 작업을 자동으로 찾아볼 수 있습니다.
performance.measure() 로 정밀 계측
// 무거운 작업의 시작을 표시
performance.mark('parse-start');
// 측정하려는 작업
const data = JSON.parse(hugePayload);
// 작업의 끝을 표시
performance.mark('parse-end');
// 지속 시간 측정
performance.measure('JSON parse', 'parse-start', 'parse-end');
이제 Performance 패널에서 해당 작업에 소요된 정확한 시간을 확인할 수 있습니다.
요약
메인 스레드를 사용자의 제한된 예산이라고 생각하세요.
Workers, requestIdleCallback, 그리고 isInputPending을 활용해 UI를 반응성 있게 유지하고, 숨겨진 차단 호출을 피하며, 항상 영향을 측정하세요. 그 예산을 존중하면 경험이 즉각적으로 느껴지고, 사용자는 만족하며, 사이트는 실제 환경과 검색 순위 모두에서 더 좋은 성능을 보입니다.
// Measure for later analysis
performance.measure('json-parse', 'parse-start', 'parse-end');
Web Vitals 라이브러리는 프로덕션 환경에서 주요 브라우저 전반에 걸쳐 실제 사용자들의 INP 점수를 캡처할 수 있습니다; 스파이크가 보이면 어디부터 조사해야 할지 알 수 있습니다.
애플리케이션 코드가 한 줄이라도 실행되기 전에, 프레임워크는 이미 초기화, 하이드레이션, 가상 DOM 조정 등에 사용자의 메인 스레드 예산을 일부 사용하고 있습니다. 이는 프레임워크를 비난하기 위한 것이 아니라, 무엇에 비용을 쓰고 있는지를 이해하라는 의미입니다. 200 ms가 걸리는 하이드레이션을 하는 프레임워크는 아무 작업도 하지 않은 상태에서 이미 상호작용당 예산의 네 배를 소비한 것이며, 이는 우연이 아니라 의식적인 선택이어야 합니다.
몇몇 프레임워크는 이 문제를 심각하게 다루기 시작했습니다:
- Qwik – 재개 가능성(resumability) 덕분에 하이드레이션을 완전히 피합니다.
- React – 동시성 기능을 통해 렌더링이 사용자 입력에 양보하도록 합니다.
이 모든 접근은 같은 근본적인 제약에서 비롯됩니다: 메인 스레드는 유한하며, 우리는 이를 부주의하게 사용해 왔습니다. 기술적인 해결책도 중요하지만, 이는 관점의 전환에서 비롯됩니다. 메인 스레드가 내 것이 아니라 사용자에게 속한다는 사실을 완전히 내면화했을 때, 내 선택도 달라지기 시작했습니다.
성능은 코드가 얼마나 빠르게 실행되는가가 아니라 코드가 실행되는 동안 인터페이스가 얼마나 반응성을 유지하는가가 됩니다. 메인 스레드를 차단하는 것은 구현 세부 사항이 아니라, 내 것이 아닌 것을 빼앗는 느낌이 됩니다.
브라우저는 우리에게 단일 실행 스레드를 제공했고, 그 스레드를 사용자에게도 동일하게 제공했습니다. 우리가 최소한 해야 할 일은 그 스레드를 공정하게 공유하는 것입니다.