JSX의 마법: 우리가 짠 코드는 어떻게 기계어가 되는가?

2026년 1월 16일
8 min read
43 views
React
JavaScript
Deep Dive
CS

들어가기에 앞서

리액트를 쓰다 보면 문득 이런 생각이 듭니다. "잠깐, 브라우저는 자바스크립트만 이해한다며? 그런데 왜 나는 HTML처럼 생긴 걸 쓰고 있지?"

우리가 무심코 작성한 <div /> 한 줄이 실제로 컴퓨터를 뜨겁게 달구는 0과 1로 바뀌기까지, 그 사이에는 놀라운 기술의 여정이 숨어있습니다.


근본적인 질문

"텍스트 쪼가리에 불과한 코드가 어떻게 살아 움직이는 애플리케이션이 되는가?"

우리가 VS Code에 타이핑하는 건 그저 영어 단어들일 뿐입니다. 이 텍스트가 어떻게 브라우저를 거쳐 CPU에게 명령을 내리는지, 그 과정을 4단계의 변신 으로 나누어 살펴보겠습니다.


변신 1단계: JSX의 탄생과 본질

"어차피 하나의 목표로 일하는데 분리하지말자"

JSX 탄생 배경: HTML과 JS의 재결합

초기 웹 개발은 HTML(뼈대)과 JS(로직)가 철저히 분리되어 있었습니다. 이를 '관심사의 분리'라 불렀죠.

구체적인 문제:

  • 버튼 하나를 만들어도 HTML 파일, CSS 파일, JS 파일을 왔다갔다
  • "이 버튼 클릭하면 뭐가 실행되지?" 찾으려면 3개 파일을 뒤져야 함
  • 컴포넌트 하나 수정하는데 여러 파일을 동시에 수정

하지만 리액트 팀은 깨달았습니다. "어차피 UI랑 로직은 한 몸인데, 파일만 따로 둔다고 분리가 되나? 그냥 합치자!"

그래서 자바스크립트의 문법을 확장(Extension)해서 HTML을 품어버린 것, 그것이 바로 JSX(JavaScript XML) 입니다.

JSX의 정체: 달콤한 설탕 (Syntactic Sugar)

JSX는 사실 자바스크립트 객체 를 만들기 위한 구문적 설탕 일 뿐입니다.

비유로 이해하기: 커피 자판기를 생각해보세요.

  • 우리가 누르는 것: "아메리카노" 버튼 → 간단하고 직관적
  • 실제 작동: 원두 분쇄, 물 온도 조절, 압력 조절, 추출 시간 계산
  • 결과: 커피 한 잔

JSX도 마찬가지입니다:

// 우리가 보는 것 (편한 버튼)
<h1 className="title">안녕하세요</h1>

// 실제로 변환되는 것 (복잡한 기계 작동)
React.createElement('h1', {className: 'title'}, '안녕하세요')

HTML과 JSX의 미묘한 차이:

HTMLJSX이유
classclassNameclass는 자바스크립트 예약어
<br><br />모든 태그는 반드시 닫아야 함
forhtmlForfor는 반복문 예약어
속성값 문자열속성값 중괄호 {}JS 표현식 사용 가능

비유: HTML은 연필로 쓴 편지, JSX는 타자기로 친 편지입니다. 내용은 같지만 규칙이 조금 달라요.

JSX 표현식의 강력한 장점

중괄호 {}정적인 HTML 벽에 뚫어놓은 차원의 문 입니다.

const userName = "철수";
const points = 1500;
const isVIP = true;

<div>
  <h1>{userName}님 환영합니다</h1>
  <p>보유 포인트: {points * 2}점</p>
  <p>등급: {points >= 2000 ? 'VIP' : '일반'}</p>
  {isVIP && <VIPBadge />}
</div>

왜 좋은가?

  1. 동적 데이터: 변수를 그대로 꽂아 넣을 수 있음
  2. 계산 가능: 표현식 안에서 바로 연산
  3. 조건부 렌더링: 삼항 연산자, && 연산자 활용
  4. 함수 호출: {formatDate(date)} 같은 것도 가능
  5. 타입 안전: TypeScript와 함께 쓰면 실수 방지

비유:

  • HTML: 인쇄된 신문 (내용 고정, 변경 불가)
  • JSX 표현식: 해리포터 움직이는 신문 (실시간으로 내용 변경 가능)

이 창문을 통해 우리는 변수, 함수, 삼항 연산자 등 자바스크립트의 모든 능력을 UI 내부에 직접 주입할 수 있습니다. 이것이 템플릿 언어와 차별화되는 JSX만의 강력함이죠.


변신 2단계: 컴파일러의 통역

"브라우저는 JSX를 모른다"

브라우저(Chrome, Safari 등)는 JSX를 이해하지 못합니다. 그래서 컴파일러(바벨, SWC) 가 등장해 이 코드를 순수 자바스크립트로 번역해줘야 합니다.

비유: 한국어만 아는 사람에게 영어로 말해봤자 못 알아듣죠? 통역사가 필요합니다.

컴파일러의 3단계 번역 과정

이 과정은 마치 외국어 번역 과정과 똑같습니다.

1단계: 토큰화 (Tokenization) - 단어 자르기

<h1>안녕</h1>

이걸 의미 있는 조각으로 나눕니다:

  • < (여는 괄호)
  • h1 (태그 이름)
  • > (닫는 괄호)
  • 안녕 (텍스트 내용)
  • </h1> (닫는 태그)

비유: "나는밥을먹는다" → ["나는", "밥을", "먹는다"]로 띄어쓰기하는 것

2단계: 구문 분석 (Parsing) - 문장 구조 파악

토큰들을 모아 AST(추상 구문 트리) 라는 구조도를 그립니다.

Element
├─ tag: "h1"
├─ props: null
└─ children
   └─ text: "안녕"

비유: "고양이가 쥐를 잡았다"를 보고

  • 주어: 고양이
  • 목적어: 쥐
  • 동사: 잡다

라고 파악하는 것

3단계: 코드 생성 (Code Generation) - 새 언어로 번역

구조도(AST)를 보고 브라우저가 이해할 수 있는 React.createElement 코드로 다시 써내려갑니다.

React.createElement('h1', null, '안녕')

비유: 한국어 문장 구조를 파악한 후 → 영어 문법에 맞게 다시 작성

JSX 프라그마 (Pragma): 번역 스타일 지정

"JSX를 어떤 함수로 바꿀 것인가?"를 결정하는 서명이 바로 프라그마 입니다.

/** @jsx jsx */  // "이모션 라이브러리 스타일로 번역해줘"
/** @jsxFrag Fragment */  // "Fragment는 이걸로 변환해줘"
  • React: React.createElement
  • Emotion: jsx
  • Preact: h

비유: 번역가에게 "이 소설은 구어체로 번역해주세요" 또는 "격식체로 해주세요"라고 요청하는 것

컴파일러 춘추전국시대

Babel (바벨)

  • 가장 유명하고 생태계가 가장 넓음
  • 꼼꼼하고 호환성 좋음 (ES5까지 지원)
  • 플러그인이 7,000개 이상 - 없는 게 없음
  • 하지만 속도는 느린 편
  • 복잡한 커스터마이징 이 필요하면 여전히 1순위

언제 바벨을 쓸까?

  • 구형 브라우저(IE11 등)까지 지원해야 할 때
  • 특수한 문법 변환이 필요할 때 (실험적 기능, 커스텀 문법)
  • 기존 프로젝트가 이미 바벨 기반일 때 (갈아타기 비용)

SWC (Speedy Web Compiler)

  • Rust로 작성되어 엄청나게 빠름
  • Babel보다 20~70배 빠름
  • Next.js가 기본으로 채택
  • 하지만 플러그인 생태계는 아직 빈약
  • 바벨 플러그인을 완벽하게 대체하지는 못함

esbuild

  • Go 언어로 작성
  • 번개같이 빠름 (Babel보다 100배)
  • 번들링도 함께 처리
  • Vite가 개발 모드에서 사용
  • 단점: TypeScript 타입 체크를 안 함 (그냥 지워버림)
  • 단점: 번들 최적화 기능이 제한적 (프로덕션에는 아직 Rollup 사용)

비유:

  • Babel: 동네 철물점 (느리지만 없는 게 없음, 특수한 부품도 다 있음)
  • SWC: 대형 마트 (빠르고 일반적인 건 다 있지만, 특수한 건 없을 수도)
  • esbuild: 편의점 (초고속이지만 품목이 제한적, 간단한 것만)

현실:

  • 새 프로젝트 + 모던 브라우저만: SWC나 esbuild
  • 구형 브라우저 지원 필요: Babel
  • 특수한 플러그인 필요: Babel
  • 개발 속도가 최우선: esbuild (개발) + Rollup (배포)
  • 이미 바벨 쓰는 레거시: 그냥 바벨 유지 (갈아타는 게 더 비쌈)

트렌드:

  • 2020년: 거의 모두가 Babel
  • 2023년: Next.js → SWC, Vite → esbuild+Rollup
  • 2025년: 혼용 시대 (프로젝트 특성에 따라 선택)

변신 3단계: 런타임과 엔진

"텍스트가 메모리에 올라가는 순간"

번역된 자바스크립트 파일은 이제 실행 환경(런타임)으로 넘어갑니다.

실행 무대 (Runtime)

브라우저

  • 크로미움(Chromium) 기반 브라우저들은 V8 엔진 탑재
  • Chrome, Edge, Brave 등이 모두 V8 사용
  • 전 세계 브라우저 점유율 70% 이상

서버

  • Node.js: 원조 서버 사이드 JS 런타임
  • Bun: 신흥 강자, 모든 게 통합되어 있고 미친 속도
  • Deno: 보안 중심, TypeScript 기본 지원

에지 (Edge)

  • 클라우드플레어 워커스: 전 세계 300개 도시에 작은 서버 배치
  • 사용자와 가장 가까운 곳에서 V8 엔진 실행
  • 서울 사용자는 서울 에지에서, 뉴욕 사용자는 뉴욕 에지에서

비유:

  • 브라우저: 영화관 (사용자가 직접 방문)
  • 서버: 중앙 방송국 (한 곳에서 모두에게 송출)
  • 엣지: 동네마다 있는 편의점 (가까운 곳에서 빠르게 제공)

V8 엔진의 소화 과정 (JIT Compilation)

자바스크립트 엔진은 텍스트를 받아서 어떻게 실행할까요?

비유로 이해하기: 요리사가 레시피를 보고 요리하는 과정

1. 파싱 (Parsing) - 레시피 읽기

function add(a, b) { return a + b; }

이 텍스트를 읽고 의미를 파악합니다.

2. 이그니션 (Ignition) - 재료 손질 코드를 바이트코드(Bytecode) 로 변환합니다.

  • 기계어보다는 추상적
  • 하지만 실행 가능한 상태
  • 중간 단계의 언어

3. 터보팬 (TurboFan) & JIT - 자주 쓰는 요리는 외워서 초고속으로 여기가 핵심입니다!

코드 실행 중 관찰
  ↓
"이 함수 자주 쓰이네?" 감지
  ↓
실행 중에(Just-In-Time) 최적화된 기계어로 컴파일
  ↓
다음부터는 초고속 실행

구체적인 예시:

// 처음에는 바이트코드로 실행 (느림)
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);  // add 함수가 계속 호출됨
}

// V8이 감지: "add가 뜨겁네(Hot)? 최적화하자!"
// → add 함수를 기계어로 컴파일
// → 이후부터는 초고속 실행

비유:

  • 처음: 레시피 보며 천천히 요리
  • 자주 만들면: 레시피 외워서 눈감고도 뚝딱 (JIT 최적화)

변신 4단계: 기계어

"결국은 전기 신호다"

0과 1의 세계

최종적으로 JIT 컴파일러가 뱉어낸 기계어는 CPU의 명령 집합(Instruction Set)에 따라 전기 신호로 변합니다.

전체 번역 체인:

우리 코드 (한글 편지)
  ↓ 컴파일러
자바스크립트 (영어 편지)
  ↓ V8 엔진
바이트코드 (약어, 줄임말)
  ↓ JIT 컴파일러
기계어 (모스 부호)
  ↓ CPU
전기 신호 (전구 깜빡임)
  ↓
화면에 픽셀로 표시

구체적인 과정:

  1. 기계어: CPU가 이해하는 0과 1의 나열

    10110000 01100001  // 'a' 문자를 레지스터에 로드
    
  2. 전기 신호: 트랜지스터를 켜고 끔

    • 1 = 전압 높음 (5V)
    • 0 = 전압 낮음 (0V)
  3. 픽셀 렌더링: GPU가 메모리의 값을 읽어 화면에 색을 칠함

    • RGB 값을 계산
    • 각 픽셀에 빛을 쏨

최종 비유:

JSX 작성 (카페에서 "아메리카노 주세요" 주문)
  ↓
컴파일러 (바리스타가 주문서 확인)
  ↓
번들링 (원두, 물, 컵 준비)
  ↓
V8 엔진 (에스프레소 머신 작동)
  ↓
기계어 (압력, 온도, 시간 제어)
  ↓
화면 렌더링 (완성된 커피 제공)

이 전기 신호가 수십억 개의 트랜지스터를 통제하고, 픽셀을 쏘아 올려, 비로소 우리 눈앞에 "안녕하세요!" 라는 글자가 렌더링되는 것이죠.


전체 흐름 정리

우리가 무심코 작성한 JSX 한 줄은, 컴파일러의 번역과 엔진의 최적화를 거쳐 기계어가 되는 긴 여정을 거칩니다.

진화의 요약

1. JSX 작성
   → 개발자의 의도를 선언적으로 표현 (HTML 같은 JS)

2. Transpiling
   → 바벨/SWC가 순수 JS로 번역 (AST 변환)

3. Bundling
   → 웹팩/Vite가 파일들을 하나로 포장

4. Execution
   → V8 엔진이 바이트코드로 변환
   → JIT가 hot 코드를 기계어로 가속

5. Rendering
   → CPU가 연산
   → GPU가 픽셀로 그리기

최종 비유: 택배 배송 과정

  1. JSX 작성: 온라인 쇼핑몰에서 주문서 작성 (우리가 원하는 것 명시)
  2. 컴파일러: 주문서를 창고 직원이 이해할 수 있는 피킹 리스트로 변환
  3. 번들러: 여러 상품을 하나의 박스에 효율적으로 포장
  4. V8 엔진: 물류 센터에서 배송 경로 최적화
  5. 기계어: 배송 기사가 실제로 운전대를 조작하고 페달을 밟음
  6. 최종 결과: 고객 앞 현관에 택배 도착 (우리가 보는 웹사이트)

결론

"우리는 수십 명의 전문가들이 만든 시스템 위에서 코딩하고 있다"

마치 택배를 시킬 때 주문서만 작성하면, 창고 직원, 포장 담당자, 물류 관리자, 배송 기사가 각자의 역할을 완벽하게 수행해서 문 앞까지 배달해주는 것처럼요.

우리가 작성한 JSX 한 줄은:

  • 컴파일러 라는 번역가가 브라우저 언어로 통역하고
  • 번들러 라는 포장 전문가가 효율적으로 묶고
  • V8 엔진 이라는 물류 관리자가 최적 경로를 찾고
  • CPU 라는 배송 기사가 실제로 실행합니다

이 흐름을 이해한다면, 리액트가 뱉어내는 에러 메시지가 더 이상 외계어처럼 보이지 않을 것입니다.

"배송이 지연되었습니다" 메시지를 받으면 우리가 물류 센터, 배송 기사, 교통 상황 중 어디에 문제가 있는지 짐작할 수 있는 것처럼, 이제 우리는 에러가 컴파일 단계에서 난 건지, 런타임에서 난 건지 구분할 수 있게 되는 거죠.

핵심 개념 정리

1. JSX는 "문법 확장"이지 "새로운 언어"가 아니다

JSX는 자바스크립트의 문법적 설탕 이다.

// 이 둘은 완전히 동일합니다
<div>안녕</div>
React.createElement('div', null, '안녕') -> 이렇게 개발하고싶으면 jsx 안써도 좋습니다.

핵심: JSX를 쓰든 안 쓰든 결과는 같습니다. JSX는 단지 우리가 편하게 쓰려고 만든 표기법일 뿐이죠.

비유:

  • "오늘 날씨 어때?" = "26년 1월의 3주차 목요일의 기상 상황은 어떠한가?"
  • 의미는 같지만 표현이 다를 뿐

2. 컴파일 타임 vs 런타임의 명확한 구분

이 구분을 이해하면 에러 메시지를 읽는 능력이 급상승합니다.

컴파일 타임 (빌드할 때)

// ❌ Syntax Error: JSX 문법 오류
<div>  // 닫는 태그 없음

// ❌ Type Error: 타입스크립트 오류
const num: number = "문자열";

→ 코드를 실행하기 전에 잡힘

런타임 (실행할 때)

// ✅ 컴파일은 성공
const user = null;
console.log(user.name);  // ❌ 실행하면 에러

→ 브라우저에서 실행하다가 터짐

비유:

  • 컴파일 타임: 요리하기 전 레시피 검토 (재료 확인, 순서 확인)
  • 런타임: 실제로 요리하다가 문제 발생 (불 조절 실수, 재료 상함)

3. 소스 코드 변환의 불가역성

한 번 변환된 코드는 원래대로 되돌릴 수 없습니다.

JSX (우리가 작성)
  ↓ 컴파일 (불가역적 변환)
JavaScript (브라우저가 실행)
  ↓ 압축 (불가역적 변환)
난독화된 JavaScript

소스맵이 중요한 이유: 변환 과정을 "역추적"할 수 있는 유일한 수단이기 때문입니다.

비유:

  • 밀가루 → 빵 만들기 (불가역적)
  • 소스맵 = 레시피 (이 빵이 어떤 재료로 만들어졌는지 알려줌)

4. 추상화 레벨의 계층 구조

높은 추상화 (인간 친화적)
    JSX
     ↓
  JavaScript
     ↓
  바이트코드
     ↓
   기계어
     ↓
  전기 신호
낮은 추상화 (기계 친화적)

위로 갈수록:

  • 읽기 쉽고
  • 작성하기 편하고
  • 실행은 느림

아래로 갈수록:

  • 읽기 어렵고
  • 작성하기 힘들고
  • 실행은 빠름

핵심: 우리는 높은 추상화에서 코딩하지만, 컴퓨터는 낮은 추상화에서 실행합니다.


5. JIT 컴파일의 핵심 원리

핵심 아이디어: 모든 코드를 미리 최적화하지 말고, 자주 쓰는 코드만 실행 중에 최적화 하자.

// Cold 코드 (가끔 실행)
function rareFunction() {
  // 바이트코드로 실행 (느림)
}

// Hot 코드 (자주 실행)
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);  // V8이 감지 → 기계어로 컴파일 (빠름)
}

왜 이게 혁명적인가?

  • 초기 실행 속도 빠름 (모든 걸 미리 컴파일 안 함)
  • 최종 실행 속도도 빠름 (hot 코드는 최적화됨)
  • 메모리 효율적 (안 쓰는 코드는 최적화 안 함)

비유:

  • 전통 컴파일: 요리책 전체를 외우기
  • JIT 컴파일: 자주 만드는 요리만 외우기

6. 개발 환경과 프로덕션 환경의 근본적 차이

구분개발 환경프로덕션 환경
목표빠른 피드백최고 성능
컴파일러esbuild (빠름)Rollup (최적화)
소스맵포함 (디버깅)제거 (보안)
압축안 함
에러 메시지자세함간략함
파일 크기작음

왜 둘로 나눌까?

  • 개발: "빨리 에러를 보고 고치자"
  • 프로덕션: "사용자에게 최고 경험을 주자"

비유:

  • 개발: 시험 공부 (문제집에 풀이 다 적혀있음)
  • 프로덕션: 실전 시험 (정답만 있음)

7. 트랜스파일링 vs 컴파일링 vs 번들링

이 셋을 혼동하면 안 됩니다.

트랜스파일링 (Transpiling)

같은 추상화 레벨에서 언어만 바꾸기
JSX → JavaScript (둘 다 고수준 언어)
TypeScript → JavaScript

컴파일링 (Compiling)

낮은 추상화로 변환
JavaScript → 바이트코드 → 기계어

번들링 (Bundling)

여러 파일을 하나로 합치기
App.js + Header.js + Footer.js → bundle.js

실제 과정:

1. 트랜스파일: JSX → JS (Babel/SWC)
2. 번들링: 여러 JS → 하나의 JS (Webpack/Vite)
3. 컴파일: JS → 기계어 (V8 엔진)

8. 브라우저는 생각보다 똑똑하다.

  • V8 엔진은 초고도 최적화 컴파일러
  • 우리 코드를 분석하고 예측하고 최적화함
  • JIT 컴파일로 실행 중에도 계속 개선
// 우리가 쓴 코드
function add(a, b) {
  return a + b;
}

// V8이 최적화한 기계어
// → 레지스터 직접 조작
// → 불필요한 체크 제거
// → CPU 파이프라인 최적화

비유:

  • 우리: 레시피 작성자
  • V8: 미슐랭 셰프 (내가 만든 레시피도 흑백요리사 나가게 해줌)

9. 모든 변환 과정에는 비용이 있다

"추상화는 공짜가 아니다"

JSX (편함)
  ↓ 컴파일 시간 소요
JavaScript (덜 편함)
  ↓ 파싱 시간 소요
바이트코드 (불편함)
  ↓ 실행 시간 소요
기계어 (매우 불편함)

하지만 결국 이득:

  • 개발 시간 90% 단축
  • 유지보수 비용 80% 감소
  • 실행 시간 10% 증가

→ 인간의 시간이 컴퓨터 시간보다 비쌉니다


10. 에러 읽는 법의 핵심

에러 메시지를 보면:

SyntaxError: Unexpected token '<'
  at App.jsx:15

질문 체크리스트:

  1. ✅ 어느 파일? → App.jsx
  2. ✅ 몇 번째 줄? → 15번
  3. ✅ 어느 단계? → Syntax Error = 컴파일 단계
  4. ✅ 뭐가 문제? → '<' 토큰 예상 못함 = JSX 문법 오류

비유: 택배 배송 실패 메시지

  • "경기도 성남시 XX아파트 101동" (위치)
  • "물류 센터 단계에서 실패" (단계)
  • "주소 불명확" (원인)

마무리 핵심 3줄 요약

  1. JSX는 설탕이고, 컴파일러는 통역사다
  2. 모든 추상화에는 비용이 있지만, 인간의 시간이 더 비싸다
  3. 에러를 이해하려면 "어느 단계"에서 났는지 파악하라

이제 npm run build 를 실행할 때, 그 뒤에서 수십 개의 도구들이 협력해서 우리 코드를 사용자에게 전달하고 있다는 걸 알게 됐습니다. 우리는 단순히 코드를 쓰는 게 아니라, 이 거대한 시스템과 대화하고 있는 겁니다.