싱글 스레드 언어의 미스터리
자바스크립트는 하나의 작업이 완료된 후 다음 작업을 순차 실행하는 싱글 스레드(Single Thread) 언어입니다. 하지만 웹브라우저 상에서 타이머를 돌리고, 마우스 클릭을 감지하며, 서버에서 네트워크 패킷을 병렬로 받아오는 행위가 동시에 원활하게 일어납니다. 자바스크립트 엔진과 브라우저 런타임 환경이 협업하는 핵심 원리를 알아봅니다.
1. 런타임 주요 구성 요소
- Call Stack: 자바스크립트 코드가 실행되며 쌓이는 단일 호출 스택.
- Web APIs: 타이머(`setTimeout`), DOM 이벤트 청취, AJAX 요청 등 브라우저가 관리하는 멀티스레드 기반 처리 대행소.
- Task Queue (Callback Queue): 비동기 연산 완료 후 자바스크립트 실행 스레드로 넘겨주기 위해 대기하는 콜백 함수 대기 장소.
- Event Loop: 실시간 감시자로, Call Stack이 완전히 비어 있을 때만 Task Queue 속의 대기 콜백들을 스택으로 밀어 넣는 스레드 조율자.
2. 마이크로태스크 큐(Microtask Queue)의 우선순위
모든 비동기 대기열이 동일한 것은 아닙니다. `Promise` 객체의 `.then()`이나 `async/await` 등으로 파생되는 마이크로태스크는 일반 `setTimeout` 콜백(매크로태스크)보다 항상 최우선적으로 먼저 스택에 주입되어 처리됩니다. 비동기 동작의 정교한 실행 순서를 완벽히 파악하기 위해서는 이 우선순위를 반드시 파악해야 합니다.
이벤트 루프를 체감하는 문제 상황
이벤트 루프 지식은 면접용 개념이 아니라 UI 멈춤, 클릭 지연, Promise 실행 순서 문제를 해결하는 도구입니다. 특히 긴 동기 작업이 Call Stack을 오래 점유하면 브라우저는 사용자 입력을 처리하지 못해 화면이 멈춘 것처럼 보입니다.
개선 방법
- 큰 배열 처리나 파싱 작업은 작은 단위로 나누어 메인 스레드 점유 시간을 줄입니다.
- 렌더링 직후 처리할 작업과 사용자 입력에 즉시 반응해야 하는 작업을 구분합니다.
- 무거운 계산은 Web Worker로 옮겨 UI 스레드를 비워둡니다.
비동기 코드는 단순히 빠르게 실행되는 코드가 아닙니다. 언제 실행되고 무엇을 막는지 이해해야 안정적인 사용자 경험을 만들 수 있습니다.
Frontend Note 실무 노트
이벤트 루프는 Promise 순서를 외우기 위한 개념이 아니라 화면이 멈추는 이유를 찾는 도구입니다. 브라우저는 JavaScript 실행, 스타일 계산, 레이아웃, 페인트를 한 메인 스레드에서 처리합니다. 긴 동기 작업이 스레드를 오래 점유하면 클릭과 입력이 밀리고 사용자는 사이트가 멈춘 것처럼 느낍니다.
button.addEventListener("click", () => {
heavyList.forEach(expensiveWork);
setMessage("done");
});
위처럼 큰 배열을 클릭 핸들러 안에서 한 번에 처리하면 INP가 나빠질 수 있습니다. 작업을 작은 단위로 나누거나 Web Worker로 옮겨 UI 스레드를 비워야 합니다. Promise는 마이크로태스크 큐에서 처리되므로 setTimeout보다 먼저 실행됩니다. 이 우선순위를 모르면 로그 순서와 화면 업데이트 타이밍을 오해하기 쉽습니다.
실무에서는 Performance 탭에서 Long Task를 찾고, 해당 작업이 사용자 입력 직후에 발생했는지 확인합니다. 렌더링 직전 꼭 필요한 작업과 나중에 해도 되는 작업을 나누면 체감 반응성이 좋아집니다.
로그 순서로 이해하는 이벤트 루프
이벤트 루프를 처음 배울 때 가장 헷갈리는 부분은 Promise와 setTimeout의 순서입니다. 동기 코드는 즉시 실행되고, Promise의 then은 마이크로태스크 큐로 들어가며, setTimeout은 태스크 큐로 들어갑니다. 현재 콜 스택이 비면 마이크로태스크가 먼저 비워지고 그다음 태스크가 실행됩니다.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// A, D, C, B
이 순서를 이해하면 화면 업데이트 타이밍도 더 잘 보입니다. 많은 마이크로태스크가 연속으로 쌓이면 브라우저가 렌더링할 틈을 얻지 못해 화면이 늦게 갱신될 수 있습니다. 무거운 작업을 Promise 체인 안에 계속 넣는다고 UI가 자동으로 부드러워지는 것은 아닙니다.
사용자 입력 직후 무거운 계산이 필요하다면 작업을 잘게 나눕니다. 목록 필터링, 정렬, 문서 파싱처럼 오래 걸리는 작업은 Web Worker를 검토합니다. 화면에 꼭 필요한 최소 결과를 먼저 보여주고, 나머지는 다음 프레임이나 idle 시간에 처리하는 전략도 실무에서 자주 씁니다.
확인 방법
- Performance 탭에서 Long Task가 입력 직후 발생하는지 본다.
- Promise 체인이 불필요하게 길게 이어지는지 확인한다.
- 렌더링 전 필요한 작업과 나중에 해도 되는 작업을 나눈다.
- 반복문 안에서 DOM 읽기와 쓰기를 섞지 않는다.
참고 자료와 업데이트 기준
Frontend Note의 글은 실무 적용 관점에서 작성하며, 관련 기술의 공식 문서와 브라우저 지원 현황이 바뀌면 내용을 다시 점검합니다. 특히 프레임워크 버전, API 정책, 성능 측정 방식은 시간이 지나며 달라질 수 있으므로 적용 전에는 최신 문서를 함께 확인하는 것을 권장합니다.
- MDN Web Docs에서 웹 표준과 브라우저 동작을 확인합니다.
- web.dev에서 성능, 접근성, 사용자 경험 기준을 확인합니다.
- React 공식 문서에서 React 관련 API와 권장 패턴을 확인합니다.
화면 멈춤을 줄이는 작업 단위
이벤트 루프를 이해하는 가장 실용적인 이유는 UI가 멈추는 순간을 줄이기 위해서입니다. 긴 배열 정렬, 대량 DOM 업데이트, 복잡한 JSON 파싱이 한 번에 실행되면 브라우저는 클릭과 렌더링을 처리할 시간을 얻지 못합니다. 사용자는 기능이 틀렸다고 느끼기보다 화면이 얼었다고 느낍니다. 그래서 큰 작업은 가능한 한 작게 나누고, 화면에 먼저 필요한 결과와 나중에 처리해도 되는 결과를 분리해야 합니다.
디버깅할 때는 Performance 패널의 Long Task 표시를 먼저 확인합니다. 긴 작업이 보이면 그 안에서 JavaScript 실행, 스타일 계산, 레이아웃, 페인트 중 무엇이 시간을 쓰는지 나눠 봅니다. Promise를 사용했다고 해서 자동으로 작업이 병렬화되는 것은 아닙니다. 마이크로태스크가 계속 이어지면 렌더링 기회가 늦어질 수 있습니다. 사용자 입력 직후에는 비싼 계산을 미루고, 목록 렌더링은 가상화하거나 페이지 단위로 나누며, 꼭 필요한 경우 Web Worker로 계산을 분리하는 식의 선택지가 있습니다.
실무 적용 전 최종 점검
이 글의 내용을 프로젝트에 적용하기 전에는 현재 코드의 목적과 사용자 흐름을 먼저 확인해야 합니다. 같은 패턴이라도 관리자 화면, 공개 랜딩 화면, 입력이 많은 폼 화면에서는 우선순위가 다릅니다. 관리자 화면은 반복 작업 속도와 오류 복구가 중요하고, 공개 화면은 초기 로딩과 접근성이 중요하며, 폼 화면은 검증 메시지와 상태 보존이 중요합니다. 따라서 예제 코드를 그대로 붙이기보다 어떤 파일에서 책임을 나눌지, 어떤 값이 외부 입력인지, 실패했을 때 사용자가 무엇을 보게 되는지를 함께 검토하는 것이 좋습니다.
작업 후에는 변경 전후를 비교할 수 있는 작은 기록을 남깁니다. 수정한 컴포넌트, 확인한 브라우저, 실패 케이스, 되돌리는 방법을 적어 두면 다음 배포에서 같은 문제를 빨리 찾을 수 있습니다. 특히 React와 TypeScript 코드는 타입 오류가 사라져도 런타임 흐름이 틀릴 수 있고, CSS 수정은 특정 화면 폭에서만 깨질 수 있습니다. 그래서 로컬 확인, 빌드 확인, 실제 화면 확인을 분리해 진행하는 습관이 필요합니다. Frontend Note의 예시는 이 과정을 돕기 위한 출발점이며, 팀의 코드 스타일과 배포 방식에 맞게 작게 조정해서 사용하는 편이 안전합니다.
작게 적용하는 연습 방법
처음 적용할 때는 전체 화면을 한 번에 바꾸지 말고 작은 컴포넌트 하나를 골라 실험하는 편이 좋습니다. 변경 범위를 좁히면 원인을 추적하기 쉽고, 기존 사용자 흐름에 주는 영향도 줄일 수 있습니다. 예제 코드를 적용한 뒤에는 정상 케이스뿐 아니라 빈 데이터, 긴 텍스트, 느린 네트워크, 모바일 화면을 함께 확인합니다. 이런 조건에서 문제가 없다면 같은 패턴을 다른 화면으로 확장해도 유지보수 부담이 적습니다.