[FE] 프론트엔드 실행 환경과 스레드 아키텍처
Part 1. 프론트엔드 실행 환경과 스레드 아키텍처
API 통신을 구현하다보니, 프론트엔드에서 비동기 처리가 정확히 어떤 환경 위에서 돌아가는지 궁금해졌다. 백엔드의 Java Spring 환경에서는 Tomcat이 요청마다 멀티 스레드를 할당하여 처리하는데, 자바스크립트는 싱글 스레드라고 한다. 단 하나의 스레드가 수 초가 걸리는 API 통신을 기다리면서 동시에 화면의 렌더링까지 처리한다는 것은 물리적으로 불가능해 보였다.
자바스크립트 엔진과 웹 브라우저의 내부 아키텍처를 찾아보자.
1. 프론트엔드 코드는 누가 컴파일하는가?
Java 환경에서는 개발자가 코드를 작성하면 컴파일러(javac)가 이를 바이트코드로 변환하고, 최종적으로 운영체제에 설치된 JVM이 이를 실행한다. 반면, 우리가 작성하는 TypeScript와 React 코드는 웹 브라우저가 직접 읽고 이해할 수 없다. 브라우저는 오직 순수한 JavaScript, HTML, CSS만 해석할 수 있기 때문이다. 따라서 프론트엔드의 실행 과정은 크게 두 단계의 변환을 거친다.
첫 번째는 개발 환경에서의 트랜스파일이다. 코드 배포 전, Webpack이나 Vite 같은 프론트엔드 빌드 도구가 브라우저가 읽을 수 없는 TypeScript 코드를 순수한 JavaScript 코드로 변환해 준다. 이는 Java의 첫 컴파일 단계와 유사하다.
두 번째는 브라우저 환경에서의 JIT 컴파일이다. 변환된 순수 JavaScript 코드가 사용자의 브라우저로 다운로드되면, 브라우저 내부에 탑재된 자바스크립트 엔진(예: 크롬의 V8 엔진)이 역할을 넘겨받는다. 엔진은 코드를 미리 다 번역해 두는 것이 아니라, 코드가 실행되는 바로 그 순간순간 실시간으로 컴퓨터가 알아들을 수 있는 0과 1의 기계어로 변환하며 코드를 실행한다. 프론트엔드에서는 이 V8 엔진이 Java의 JVM과 같은 핵심적인 실행기 역할을 수행하는 것이다.
2. 싱글 스레드의 한계와 메인 스레드의 진짜 의미
가장 큰 차이는 코드를 실행하는 Thread의 운용 방식에 있다. 백엔드의 Tomcat은 스레드 풀을 가지고 있어, 사용자 A가 요청하면 1번 스레드를, 사용자 B가 요청하면 2번 스레드를 꺼내어 병렬로 작업을 처리하는 멀티 스레드 방식을 취한다.
하지만 브라우저 내부의 자바스크립트 엔진은 오직 단 하나의 스레드만을 사용하여 코드를 실행한다. 이 유일한 스레드가 자바스크립트 코드의 연산만 처리하는 것이 아니라, HTML/CSS를 계산하고 화면에 픽셀을 칠하는 UI 렌더링 작업까지 혼자서 모두 독점하여 처리해야 한다는 것이다.
그렇다면 어차피 일꾼이 하나뿐인데 왜 굳이 메인 스레드라고 명확히 구분해서 부를까? 웹 기술이 발전하면서 대용량 엑셀 파일 파싱이나 복잡한 이미지 처리 같은 무거운 연산을 싱글 스레드에서 돌리면 화면이 완전히 멈춰버리는 문제가 발생했다. 이를 해결하기 위해 개발자가 명시적으로 백그라운드에서 코드를 실행할 수 있는 Web Worker라는 기술이 도입되어 별도의 워커 스레드들을 생성할 수 있게 되었다.
그러나 브라우저는 충돌을 막기 위해 DOM 조작과 화면을 그리는 UI 렌더링 권한만큼은 오직 단 하나의 메인 스레드에게만 독점적으로 부여했다. 즉, 보조 일꾼들이 존재할 수는 있지만, 애플리케이션의 핵심 로직과 화면 제어는 오직 메인 스레드라는 하나의 통제탑을 거쳐야만 하는 엄격한 구조인 것이다.
3. Web API와 비동기 위임: 유일한 스레드가 멈추지 않는 이유
메인 스레드가 유일한 렌더링 일꾼이라면, 논리적으로 API 통신을 위해 서버의 응답을 기다리는 1~2초 동안 브라우저 화면은 완전히 하얗게 멈춰야(Freeze) 한다. 메인 스레드가 통신 응답을 기다리느라 묶여있기 때문이다. 하지만 실제로는 통신 중에도 화면의 로딩 스피너가 부드럽게 돌아가고, 사용자가 다른 버튼을 클릭해도 정상적으로 반응한다.
이것이 가능한 이유는 단일 스레드의 한계를 극복하게 해주는 Web API 덕분이다. 자바스크립트 엔진 자체는 싱글 스레드지만, 코드가 실행되는 터전인 웹 브라우저 자체는 C++로 작성된 거대한 멀티 스레드 프로그램이다.
Web API는 자바스크립트 언어 자체의 기능이 아니라, 브라우저가 “우리가 백그라운드에서 돌리는 멀티 스레드 기능들을 네가 호출해서 쓸 수 있게 해줄게”라며 열어준 창구(인터페이스)다. 네트워크 통신(fetch), 타이머(setTimeout), DOM 이벤트 리스너 등이 모두 Web API에 해당한다.
결과적으로 자바스크립트 코드에서 API 통신을 지시하면, 메인 스레드는 자신이 직접 통신을 기다리지 않는다. 통신 작업 자체를 브라우저 내부의 네트워크 스레드로 위임해 버린다. 작업을 넘긴 메인 스레드는 즉시 자유로워져서 다음 줄의 코드를 읽거나, 화면의 로딩 애니메이션을 그리는 본연의 임무를 멈춤 없이 계속 수행한다.
[ 백엔드 Tomcat 아키텍처 : 멀티 스레드 할당 ]
요청 A ──→ (스레드 풀) ──→ [스레드 1 할당] ──→ 데이터베이스 조회 등 무거운 로직 직접 처리
요청 B ──→ (스레드 풀) ──→ [스레드 2 할당] ──→ 데이터베이스 조회 등 무거운 로직 직접 처리
[ 브라우저 프론트엔드 아키텍처 : 싱글 스레드의 비동기 위임 ]
[API 호출 코드 만남] ────┐
↓ (위임)
[UI 화면 업데이트] ←── [단일 메인 스레드] [ Web API (브라우저 백그라운드 영역) ]
(로딩 스피너 등) ↑ (클릭 감지) - 네트워크 스레드 (실제 서버 통신 수행)
│ - 타이머 스레드 등
[사용자 클릭 이벤트] ───┘
결국 프론트엔드의 비동기 처리는 자바스크립트 혼자서 하는게 아니라, 싱글 스레드인 엔진과 멀티 스레드인 브라우저가 협력하여 무거운 작업을 백그라운드로 넘기고 화면의 반응성을 유지하는 결과물이다.