들어가기에 앞서
리액트를 쓰다 보면 문득 이런 생각이 듭니다. "잠깐, 브라우저는 자바스크립트만 이해한다며? 그런데 왜 나는 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의 미묘한 차이:
| HTML | JSX | 이유 |
|---|---|---|
class | className | class는 자바스크립트 예약어 |
<br> | <br /> | 모든 태그는 반드시 닫아야 함 |
for | htmlFor | for는 반복문 예약어 |
| 속성값 문자열 | 속성값 중괄호 {} | 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>
왜 좋은가?
- 동적 데이터: 변수를 그대로 꽂아 넣을 수 있음
- 계산 가능: 표현식 안에서 바로 연산
- 조건부 렌더링: 삼항 연산자, && 연산자 활용
- 함수 호출:
{formatDate(date)}같은 것도 가능 - 타입 안전: 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
전기 신호 (전구 깜빡임)
↓
화면에 픽셀로 표시
구체적인 과정:
-
기계어: CPU가 이해하는 0과 1의 나열
10110000 01100001 // 'a' 문자를 레지스터에 로드 -
전기 신호: 트랜지스터를 켜고 끔
- 1 = 전압 높음 (5V)
- 0 = 전압 낮음 (0V)
-
픽셀 렌더링: 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가 픽셀로 그리기
최종 비유: 택배 배송 과정
- JSX 작성: 온라인 쇼핑몰에서 주문서 작성 (우리가 원하는 것 명시)
- 컴파일러: 주문서를 창고 직원이 이해할 수 있는 피킹 리스트로 변환
- 번들러: 여러 상품을 하나의 박스에 효율적으로 포장
- V8 엔진: 물류 센터에서 배송 경로 최적화
- 기계어: 배송 기사가 실제로 운전대를 조작하고 페달을 밟음
- 최종 결과: 고객 앞 현관에 택배 도착 (우리가 보는 웹사이트)
결론
"우리는 수십 명의 전문가들이 만든 시스템 위에서 코딩하고 있다"
마치 택배를 시킬 때 주문서만 작성하면, 창고 직원, 포장 담당자, 물류 관리자, 배송 기사가 각자의 역할을 완벽하게 수행해서 문 앞까지 배달해주는 것처럼요.
우리가 작성한 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
질문 체크리스트:
- ✅ 어느 파일? → App.jsx
- ✅ 몇 번째 줄? → 15번
- ✅ 어느 단계? → Syntax Error = 컴파일 단계
- ✅ 뭐가 문제? → '<' 토큰 예상 못함 = JSX 문법 오류
비유: 택배 배송 실패 메시지
- "경기도 성남시 XX아파트 101동" (위치)
- "물류 센터 단계에서 실패" (단계)
- "주소 불명확" (원인)
마무리 핵심 3줄 요약
- JSX는 설탕이고, 컴파일러는 통역사다
- 모든 추상화에는 비용이 있지만, 인간의 시간이 더 비싸다
- 에러를 이해하려면 "어느 단계"에서 났는지 파악하라
이제 npm run build 를 실행할 때, 그 뒤에서 수십 개의 도구들이 협력해서 우리 코드를 사용자에게 전달하고 있다는 걸 알게 됐습니다. 우리는 단순히 코드를 쓰는 게 아니라, 이 거대한 시스템과 대화하고 있는 겁니다.