2025-09-22 23:49
-
JSX는 JavaScript의 확장 문법으로, UI 구조를 HTML과 유사하게 작성하여 가독성과 개발 경험을 향상시킨다.
-
JSX 코드는 브라우저가 직접 실행할 수 없으며, Babel과 같은 트랜스파일러를 통해
React.createElement()함수 호출로 변환된다. -
이 변환 과정은 최종적으로 JavaScript 객체(리액트 엘리먼트)를 생성하며, 이는 가상돔(Virtual DOM)을 구성하는 기본 단위가 된다.
JSX 완벽 정복 핸드북 자바스크립트를 HTML처럼
웹 개발의 세계에 발을 들였다면, 특히 리액트(React)를 다루어 보았다면 JSX라는 용어를 피할 수 없다. HTML처럼 생겼지만 실제로는 JavaScript인 이 독특한 문법은 처음에는 낯설게 느껴질 수 있다. 하지만 JSX의 탄생 배경과 작동 원리를 이해하는 순간, 왜 이것이 현대 프론트엔드 개발의 판도를 바꾸었는지 깨닫게 될 것이다.
이 핸드북은 JSX의 모든 것을 담았다. 왜 만들어졌는지, 어떤 구조로 이루어져 있는지, 어떻게 마법처럼 동작하는지, 그리고 실전에서 프로처럼 활용하는 방법까지 상세히 안내한다. 이 글을 끝까지 읽는다면, 당신은 더 이상 JSX를 두려워하지 않고 자유자재로 다루는 개발자로 거듭날 것이다.
1. JSX, 왜 만들어졌을까? 불편함에서 시작된 혁신
모든 위대한 발명은 불편함에서 시작된다. JSX 역시 마찬가지다. JSX가 등장하기 전, 웹 개발 세계는 ‘관심사의 분리(Separation of Concerns)‘라는 원칙이 지배하고 있었다.
과거의 UI 개발: 구조, 스타일, 동작의 엄격한 분리
전통적인 웹 개발에서는 다음과 같이 역할을 명확히 나누었다.
-
HTML: 웹 페이지의 구조와 콘텐츠를 담당
-
CSS: 디자인과 레이아웃 등 시각적인 표현을 담당
-
JavaScript: 사용자와의 상호작용 및 동적인 기능을 담당
이는 각자의 역할에만 집중할 수 있어 명확하고 관리하기 편하다는 장점이 있었다. HTML 파일에서 JavaScript 코드를 보거나, JavaScript 파일 안에서 복잡한 HTML 문자열을 만드는 것은 ‘나쁜 습관’으로 여겨졌다.
JavaScript
// 과거 방식: JavaScript로 DOM 요소 직접 생성
const root = document.getElementById('root');
const element = document.createElement('div');
element.className = 'greeting';
const heading = document.createElement('h1');
heading.textContent = 'Hello, World!';
const paragraph = document.createElement('p');
paragraph.textContent = 'This is created by vanilla JavaScript.';
element.appendChild(heading);
element.appendChild(paragraph);
root.appendChild(element);
위 코드를 보면 간단한 UI를 만드는 데도 코드가 길고 복잡하며, 최종 결과물이 어떻게 생겼을지 한눈에 파악하기 어렵다.
컴포넌트 시대의 도래: 새로운 패러다임
하지만 웹 애플리케이션이 점점 더 복잡해지면서 문제가 발생했다. 페이스북의 뉴스피드처럼 수많은 기능이 얽힌 UI를 생각해보자. ‘좋아요’ 버튼 하나만 해도, 이 버튼의 모양(HTML), 스타일(CSS), 그리고 클릭했을 때의 동작(JavaScript)은 서로 멀리 떨어져 있는 것이 아니라 하나의 기능 단위로 강하게 결합되어 있다.
이러한 배경 속에서 컴포넌트(Component) 기반 아키텍처가 대두되었다. 관련된 기능들을 하나의 독립적인 부품(컴포넌트)으로 묶어서 관리하자는 아이디어다. ‘좋아요 버튼 컴포넌트’는 자신의 구조, 스타일, 로직을 모두 스스로 책임진다.
JSX의 탄생: 마크업과 로직의 자연스러운 결합
리액트는 바로 이 컴포넌트 기반 사고방식을 극대화한 라이브러리다. 그리고 리액트 팀은 깨달았다. 특정 UI 조각을 렌더링하는 로직은 그 UI의 구조(마크업)와 본질적으로 결합되어 있다는 것을. 이 둘을 억지로 분리하는 것이 오히려 개발을 더 복잡하게 만든다고 생각했다.
그래서 그들은 JavaScript 안에서 UI를 가장 직관적으로 표현할 방법을 고민했고, 그 결과물이 바로 **JSX (JavaScript XML)**다.
JavaScript
// JSX 방식: 직관적인 UI 작성
const element = (
<div className="greeting">
<h1>Hello, World!</h1>
<p>This is created by JSX.</p>
</div>
);
// 실제 렌더링은 ReactDOM이 담당
ReactDOM.render(element, document.getElementById('root'));
두 코드를 비교해보면 JSX가 얼마나 직관적이고 선언적인지 명확히 알 수 있다. 개발자는 ‘어떻게’ 그릴지를 명령하는 대신, ‘무엇을’ 그릴지에 집중할 수 있게 되었다. 이것이 바로 JSX가 탄생한 이유다.
2. JSX의 해부학: 구조와 핵심 문법
JSX는 HTML과 매우 유사해 보이지만, 몇 가지 중요한 규칙을 따르는 JavaScript의 확장 문법이다. 그 핵심적인 규칙들을 해부해보자.
HTML을 닮은 외모
가장 큰 특징은 역시 HTML 태그와 유사한 문법을 사용한다는 점이다. <div>, <h1>, <p> 등의 태그를 그대로 사용하여 UI의 구조를 시각적으로 명확하게 표현할 수 있다.
JavaScript
const element = <h1>Hello, JSX!</h1>;
자바스크립트로의 관문 {}
JSX의 가장 강력한 기능 중 하나는 중괄호 {}를 사용하여 JSX 내부 어디에서든 JavaScript 표현식을 사용할 수 있다는 점이다. 변수, 함수 호출, 계산 결과 등을 동적으로 렌더링할 수 있다.
JavaScript
const name = 'Geek';
const user = { firstName: 'Jane', lastName: 'Doe' };
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const element = (
<div>
{/* 변수 사용 */}
<h1>Hello, {name}!</h1>
{/* 함수 호출 결과 사용 */}
<h2>It's good to see you, {formatName(user)}.</h2>
{/* 간단한 계산 */}
<p>2 + 2 = {2 + 2}</p>
</div>
);
단, {} 안에는 **표현식(Expression)**만 올 수 있다. if문이나 for문 같은 **문장(Statement)**은 직접 사용할 수 없다.
속성(Attribute) 지정의 규칙: className과 camelCase
JSX는 HTML이 아니라 JavaScript에 더 가깝기 때문에 속성을 지정할 때 몇 가지 차이점이 있다.
-
class대신className:class는 JavaScript의 예약어(클래스를 선언할 때 사용)이므로, HTML의class속성은className으로 사용해야 한다. -
camelCase명명 규칙:onclick,tabindex와 같은 HTML 속성들은 JSX에서onClick,tabIndex와 같이 카멜 케이스(camelCase)로 작성해야 한다. 이는 JavaScript의 DOM API 명명 규칙을 따르는 것이다.
JavaScript
// 잘못된 예시
// <div class="container" onclick="handleClick()">Click me</div>
// 올바른 JSX 예시
const element = <div className="container" onClick={handleClick}>Click me</div>;
하나의 부모 요소와 프래그먼트 <>...</>
JSX 표현식은 반드시 하나의 부모 요소로 감싸져 있어야 한다. 여러 개의 형제 요소를 반환하려고 하면 오류가 발생한다.
JavaScript
// 잘못된 예시: 부모 요소가 없음
// const element = (
// <h1>Title</h1>
// <p>Paragraph</p>
// );
// 올바른 예시: div로 감싸기
const element = (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
하지만 불필요한 div 태그를 DOM에 추가하고 싶지 않을 때가 있다. 이때 **프래그먼트(Fragment)**를 사용하면 된다. 프래그먼트는 <React.Fragment> 또는 축약형인 <>...</>를 사용하여 여러 요소를 그룹화할 수 있다.
JavaScript
// 프래그먼트 사용 예시
const element = (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
주석과 빈 태그
-
주석: JSX 내부의 주석은
{/* ... */}와 같은 형태로 작성해야 한다. -
빈 태그 (Self-Closing Tag):
<img>,<br>,<input>처럼 닫는 태그가 없는 요소는 반드시 끝에/를 붙여주어야 한다 (<img src="..." />).
3. 보이지 않는 마법: JSX는 어떻게 코드가 될까?
우리가 작성한 JSX 코드는 사실 브라우저가 직접 이해할 수 없다. 브라우저는 오직 순수한 JavaScript, HTML, CSS만 해석할 수 있다. 그렇다면 JSX는 어떻게 화면에 그려지는 걸까? 그 비밀은 트랜스파일(Transpile) 과정에 있다.
브라우저는 JSX를 모른다: 트랜스파일러의 역할
개발자가 JSX 코드를 작성하고 저장하면, **바벨(Babel)**과 같은 트랜스파일러가 이 코드를 브라우저가 이해할 수 있는 일반 JavaScript 코드로 변환해준다. 이 과정은 우리가 영어를 한국어로 번역하는 것과 비슷하다. JSX라는 ‘고급 언어’를 JavaScript라는 ‘기계가 이해하는 언어’로 바꾸는 것이다.
Babel의 변환 과정: React.createElement의 등장
바벨은 JSX 문법을 만나면 이것을 React.createElement()라는 함수 호출로 변환한다.
우리가 작성한 JSX 코드:
JavaScript
const element = <h1 className="greeting">Hello, world</h1>;
바벨이 변환한 JavaScript 코드:
JavaScript
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world'
);
React.createElement() 함수는 세 가지 인자를 받는다.
-
type: 태그의 종류 ('h1','div'또는 다른 리액트 컴포넌트) -
props: 속성 (className,src등)을 담은 객체 -
children: 해당 요소의 자식 요소들 (텍스트 또는 다른 요소)
결국, 우리가 사용하는 모든 JSX는 React.createElement() 함수를 호출하는 **문법적 설탕(Syntactic Sugar)**에 불과하다. 더 편하고 보기 좋게 쓰기 위한 문법일 뿐, 본질은 함수 호출이다.
가상돔(Virtual DOM)을 위한 설계도: 리액트 엘리먼트
그렇다면 React.createElement() 함수는 무엇을 반환할까? 이 함수는 화면에 그려질 UI에 대한 정보를 담은 가벼운 JavaScript 객체를 반환한다. 이 객체를 **리액트 엘리먼트(React Element)**라고 부른다.
위 예시의 element 변수를 콘솔에 출력해보면 다음과 같은 객체를 볼 수 있다.
JSON
{
"type": "h1",
"props": {
"className": "greeting",
"children": "Hello, world"
},
// ... 기타 정보
}
이 리액트 엘리먼트는 일종의 ‘설계도’다. 리액트는 이 설계도들을 모아 메모리 상에 **가상돔(Virtual DOM)**이라는 것을 만든다. 그리고 상태가 변경될 때마다 새로운 설계도(리액트 엘리먼트)를 만들어 새로운 가상돔을 구축하고, 이전 가상돔과 비교하여 변경된 부분만 실제 DOM에 효율적으로 업데이트한다. JSX는 바로 이 가상돔을 만드는 첫 단계인 ‘설계도 작성’을 우아하게 만들어주는 도구인 셈이다.
4. 실전 JSX 활용법: 프로처럼 사용하기
기본 원리를 이해했으니 이제 JSX를 실전에서 효과적으로 사용하는 패턴들을 알아보자.
조건부 렌더링: 상황에 맞는 UI 보여주기
애플리케이션의 상태에 따라 다른 UI를 보여주는 것은 매우 흔한 일이다. JSX는 JavaScript이므로, JavaScript의 조건문 로직을 그대로 활용할 수 있다.
-
삼항 연산자 (
? :): 가장 흔하게 사용되는 방법이다.JavaScript
function Greeting({ isLoggedIn }) { return ( <div> {isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign up.</h1>} </div> ); } -
논리 AND 연산자 (
&&): 특정 조건이 참일 때만 요소를 렌더링하고 싶을 때 유용하다.JavaScript
function Mailbox({ unreadMessages }) { return ( <div> <h1>Hello!</h1> {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); }
배열 렌더링: map() 함수와 key의 중요성
배열에 담긴 데이터를 목록 형태로 렌더링할 때는 JavaScript의 map() 함수를 사용한다. map() 함수는 배열의 각 항목을 JSX 요소로 변환하여 새로운 배열을 반환한다.
JavaScript
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(<ul>{listItems}</ul>, document.getElementById('root'));
이때 매우 중요한 것이 바로 key prop이다. 리액트는 key를 사용하여 배열의 어떤 항목이 변경, 추가 또는 삭제되었는지 식별한다. key는 각 형제 요소들 사이에서 고유해야 한다.
JavaScript
const todos = [
{id: 'a', text: 'Learn React'},
{id: 'b', text: 'Build a project'},
{id: 'c', text: 'Deploy it'},
];
const todoList = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
key를 배열의 인덱스(index)로 사용하는 것은 피하는 것이 좋다. 배열의 순서가 바뀌거나 항목이 추가/삭제될 때 예기치 않은 동작이나 성능 저하를 유발할 수 있다. 항상 데이터의 고유한 ID를 key로 사용하자.
컴포넌트에 데이터 전달하기: Props
JSX를 사용하면 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 것이 매우 직관적이다. HTML 속성처럼 생긴 이 데이터 전달 방식을 **Props(Properties)**라고 한다.
JavaScript
// 자식 컴포넌트
function Avatar({ user, size }) {
return (
<img
className="avatar"
src={user.avatarUrl}
alt={user.name}
width={size}
height={size}
/>
);
}
// 부모 컴포넌트
function Profile() {
const userData = {
name: 'Son Heung-min',
avatarUrl: 'https://example.com/son.jpg'
};
return (
<div>
{/* Avatar 컴포넌트에 user와 size라는 props를 전달 */}
<Avatar user={userData} size={100} />
</div>
);
}
스타일링 기법
JSX에서 컴포넌트를 스타일링하는 방법은 여러 가지가 있다.
-
인라인 스타일:
style속성에 JavaScript 객체를 전달한다. 속성 이름은camelCase로 작성해야 한다.JavaScript
const divStyle = { color: 'blue', backgroundColor: 'lightgray', // background-color -> backgroundColor fontSize: '20px' // 단위를 문자열로 포함 }; function MyComponent() { return <div style={divStyle}>Styled Div</div>; } -
CSS-in-JS:
styled-components나Emotion과 같은 라이브러리를 사용하면 JavaScript 파일 내에서 CSS 문법을 사용하여 스타일이 적용된 컴포넌트를 직접 만들 수 있다.
5. 한 걸음 더 깊이: JSX 심화 탐구
JSX에 대해 더 깊이 알아보며 지식을 완성해보자.
JSX vs HTML: 무엇이 다른가?
| 구분 | JSX | HTML | 비고 |
|---|---|---|---|
| 속성 이름 | camelCase (예: onClick) | kebab-case (예: onclick) | JavaScript DOM API를 따름 |
| 클래스 지정 | className | class | class는 JavaScript 예약어 |
| 인라인 스타일 | 객체 ({ {color: 'red'} }) | 문자열 ("color: red;") | JavaScript 객체로 스타일을 정의 |
| 자식 요소 | 모든 JavaScript 표현식 가능 | 텍스트와 다른 HTML 요소만 가능 | {}를 통한 동적 렌더링 가능 |
| 닫는 태그 | 모든 태그는 닫혀야 함 (<br />) | 일부 태그는 닫지 않아도 됨 (<br>) | XML 문법의 영향을 받음 |
for 속성 | htmlFor | for | for는 JavaScript 예약어 |
리액트 없이 JSX 사용하기
JSX는 리액트 전용 문법이 아니다. JSX는 독립적인 문법 사양이며, 다른 라이브러리나 프레임워크에서도 사용할 수 있다. 예를 들어, Preact, SolidJS, 심지어 Vue에서도 플러그인을 통해 JSX를 사용할 수 있다.
이는 JSX가 특정 기술에 종속된 것이 아니라, UI를 선언적으로 작성하는 효과적인 패턴으로 인정받고 있음을 의미한다.
타입스크립트와 함께하는 JSX (.tsx)
타입스크립트(TypeScript)와 함께 JSX를 사용하면 강력한 타입 체크의 이점을 누릴 수 있다. 파일 확장자를 .tsx로 사용하면 타입스크립트 컴파일러가 JSX 문법을 해석할 수 있다.
이를 통해 컴포넌트가 받아야 할 props의 타입을 정의하여 개발 단계에서 실수를 방지하고, 코드의 안정성과 유지보수성을 크게 높일 수 있다.
TypeScript
// props의 타입을 정의
interface GreetingProps {
name: string;
messageCount?: number; // '?'는 선택적 prop임을 의미
}
// FC (Function Component) 타입을 사용하여 컴포넌트 정의
const Greeting: React.FC<GreetingProps> = ({ name, messageCount = 0 }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{messageCount > 0 && <p>You have {messageCount} new messages.</p>}
</div>
);
};
마치며: JSX는 단순한 문법 그 이상이다
JSX는 단순히 HTML을 JavaScript에 옮겨놓은 것이 아니다. 이것은 UI가 어떻게 렌더링되어야 하는지에 대한 로직과 그 UI의 구조를 한곳에 모아 응집도를 높이는 현대적인 개발 패러다임의 산물이다.
처음에는 ‘관심사의 분리’ 원칙을 위배하는 것처럼 보일 수 있지만, 컴포넌트 단위로 생각하면 이보다 더 자연스러운 ‘관심사의 결합’은 없다. JSX를 통해 우리는 더 직관적으로 코드를 읽고, 더 효율적으로 UI를 구축하며, 더 즐겁게 개발할 수 있게 되었다. 이제 당신도 이 강력한 도구를 손에 쥐고 멋진 웹 애플리케이션을 만들어나갈 준비가 되었다.