리액트를 처음 보면 자바스크립트 안에 HTML이 들어가 있어 당황하게 돼요. 그게 바로 JSX예요. 이번 장에서 "이게 뭔지, 왜 이렇게 생겼는지, 진짜 HTML과 뭐가 다른지"를 하나씩 뜯어봐요.
React.createElement(...)로 변환된다는 걸 알아요.{} 안에 식(expression)만 넣을 수 있음을 이해해요.className, htmlFor, 카멜케이스 이벤트, 태그 닫기, style 객체 — 을 구분해요.<>…</> Fragment를 쓸 수 있음을 알아요.JSX(JavaScript XML)는 자바스크립트의 확장 문법이에요. 화면에 그릴 모양을 HTML처럼 생긴 태그로 자바스크립트 코드 안에 바로 적을 수 있게 해줍니다. 아래를 보면 "이게 HTML인가 자바스크립트인가?" 싶은데, 둘 다예요 — 자바스크립트인데 HTML 모양을 빌려 쓴 거죠.
const element = <h1>안녕, 리액트!</h1>;
// 변수(element)에 "화면 조각"을 담았어요. 이 <h1>…</h1> 부분이 바로 JSX예요.
document.createElement('h1')처럼 요소를 하나하나 만들고 붙이는 지루한 코드를 써야 했어요. JSX는 그 과정을 우리가 눈으로 보는 HTML 모양 그대로 적게 해줘서, "코드를 보면 결과 화면이 그려지는" 큰 장점이 있어요.
JSX는 "필수"는 아니에요. 없이도 리액트를 쓸 수 있지만, 거의 모든 사람이 JSX를 씁니다. 훨씬 읽기 쉬우니까요.
그런데 여기서 아주 중요한 사실 하나. 브라우저는 JSX를 직접 읽지 못해요. <h1>이 자바스크립트 문법에는 없거든요. 그래서 Babel(바벨)이라는 변환기가 JSX를 진짜 자바스크립트 함수 호출로 바꿔줍니다. 아래처럼요.
// 우리가 쓴 JSX
const element = <h1 className="title">안녕</h1>;
// Babel이 변환한 실제 자바스크립트 (개념)
const element = React.createElement(
"h1", // 태그 이름
{ className: "title" },// 속성(props)
"안녕" // 자식(children)
);
React.createElement(...) 호출 코드로 변환해주는 것뿐이에요. "HTML을 쓰는 게 아니라, HTML처럼 생긴 자바스크립트를 쓴다"는 감각이 핵심입니다.| 구분 | 정체 | 예 |
|---|---|---|
| JavaScript | 언어(밑바탕) | 변수·함수·if·.map()·=> — 어디서나 돌아요 |
| JSX | 문법 확장 | JS 안에 <button>클릭</button> 같은 태그를 쓰게 해줘요(Babel이 React.createElement로 변환) |
| React | 라이브러리(도구) | useState·props·컴포넌트·화면 렌더링·onClick 연결 |
function App() { // ← 그냥 JavaScript (함수)
function handleClick() { // ← 그냥 JavaScript
alert('버튼이 눌렸어요!'); // ← 그냥 JavaScript (브라우저 기능)
}
return (
<button onClick={handleClick}>클릭</button> // ← 여기서만 섞임
);
}
마지막 줄 <button onClick={handleClick}>클릭</button> 안에서: <button>…</button> 태그 모양은 JSX, {handleClick}로 JS 값을 꽂는 건 JSX 문법(안의 handleClick은 JS), onClick이 실제 클릭에 연결돼 화면에 그려지는 건 React예요.const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // [2,4,6] — 100% 자바스크립트
· JSX가 섞임 (<h1> 태그 등장)
const el = <h1>안녕</h1>; // const el = 은 JS, <h1>…</h1> 이 JSX
· React 고유 (훅·컴포넌트)
const [n, setN] = useState(0); // useState = React, [n, setN] 구조분해 = JS
④ 빠른 구별법<태그>가 보이면 → JSX (그 태그만 지우면 순수 JS예요)..map()·=> 같은 로직 → 그냥 JavaScript.useState·useEffect·props·컴포넌트·렌더링·onClick 연결 → React.function f() { ... } // { } = 자바스크립트 (함수 몸통)
const p = { name: '철수' }; // { } = 자바스크립트 (객체)
<h1>{name}</h1> // { } = JSX 문법 (JSX 안에 JS 값 넣기)
<button onClick={handleClick}> // { } = JSX 문법
JSX 안(태그 사이·속성)의 { }는 "여기 JS 값을 넣어라"는 JSX 문법이고, 그냥 코드의 { }(함수 몸통·객체)는 그냥 JS예요. 어느 쪽도 리액트는 아니에요.const [n, setN] = useState(0); // use로 시작하는 훅 → React
useEffect(() => { ... }, []); // 훅 → React
function App() { return <div/> } // 대문자 컴포넌트 + JSX 반환 → React
<Button label="확인" /> // 대문자 태그(내 컴포넌트) + props → React
즉 리액트를 알아보는 표시는 ①use~로 시작하는 훅, ②대문자로 시작하는 컴포넌트, ③props — 중괄호랑은 상관없어요.{ }를 열면, 그 안은 다시 자바스크립트 세상이에요.JSX는 HTML 모양이지만, 중간에 중괄호 { }를 열면 그 안에서 자바스크립트 값을 꺼내 쓸 수 있어요. 변수, 계산식, 객체 속성, 함수 호출 결과 등 "값이 되는 것"이면 무엇이든 들어갈 수 있어요.
const name = "지훈";
const user = { age: 20 };
const element = (
<div>
<p>안녕, {name}!</p> {/* 변수 */}
<p>1 + 2 = {1 + 2}</p> {/* 계산식 */}
<p>나이: {user.age}살</p> {/* 객체 속성 */}
<p>대문자: {name.toUpperCase()}</p> {/* 함수 호출 결과 */}
</div>
);
10 → 10, 1 + 2 → 계산하면 3, "안녕" → 안녕.name 은요? 이렇게 생각하면 쉬워요:const name = "지훈"; 이라고 정해두면, 코드 속 name 은 컴퓨터가 속으로 "지훈" 으로 바꿔서 봐요. 즉 name 도 결국 값 하나("지훈")가 되는 거예요.if (x > 0) { ... } → "조건 보고 필요하면 실행해"라는 명령일 뿐(값 없음).let a = 10; → "a라는 변수를 만들어"라는 명령(값 없음).if (10 > 5) { "YES" } 안엔 "YES"가 있잖아요?" — 좋은 질문! 하지만 여기서 "YES"는 if의 결과값으로 쓰이는 게 아니라, 그냥 안에 적힌 코드 한 줄일 뿐이에요. if문 자체는 값을 만들어 내는 문법이 아니거든요. 그래서 const b = if (...) 처럼 값 자리에 쓰면 에러가 나요.<h1>{1 + 2}</h1> 를 보면 리액트는 속으로 이렇게 처리해요:화면에 넣을 값 = 1 + 2 → 화면에 넣을 값 = 3 → <h1>3</h1><h1>{name}</h1> 은 name이 "지훈"이 되니까 <h1>지훈</h1> 이 돼요. ✅<h1>{ if (x>0){...} }</h1> 는? "화면에 넣을 값 = ???" — if는 값을 만들지 않으니 채울 게 없어요. → 그래서 안 돼요. ❌
🍽️ 비유: 중괄호는 "접시에 담을 완성된 요리(값)"를 놓는 자리예요. "요리를 하라"는 명령(문)은 접시에 담을 수 없죠.
| 넣을 수 있는 것 (식) | 넣을 수 없는 것 (문) |
|---|---|
{name} 변수 | {if (x) {...}} if 문 ❌ |
{1 + 2} 계산식 | {for (...) {...}} for 문 ❌ |
{cond ? 'A' : 'B'} 삼항 연산 | {let a = 1} 변수 선언 ❌ |
{isOk && <p>OK</p>} 논리곱 | {switch(...)} switch 문 ❌ |
{list.map(...)} 배열 메서드 |
if문 대신 삼항 연산자 조건 ? A : B나 &&를, 반복은 .map()을 써요. 이들은 전부 "계산하면 값이 되는 식"이라 중괄호 안에 들어갈 수 있어요. (자세한 활용은 뒤 챕터에서!)JSX는 결국 자바스크립트예요. 그래서 자바스크립트에서 이미 예약된 단어와 겹치거나, 문법 규칙에 맞춰 HTML과 다르게 써야 하는 것들이 있어요. 대표적인 다섯 가지를 봅시다.
class는 자바스크립트에서 이미 예약어(class 문법)라서 그대로 못 써요. 그래서 CSS 클래스를 줄 땐 className을 씁니다.
// HTML: <div class="box">
// JSX:
const el = <div className="box">내용</div>;
class가 대체 뭐예요? — 펼쳐 보기class는 프로그래밍의 "클래스(class)" 개념이 아니라, HTML의 속성(attribute)이에요. 이름만 같고 완전히 다른 거예요.<div class="box">내용</div> 를 조각내면:div → 태그class="box" → 속성(attribute)내용 → 태그 안의 내용<div class="box"> 는 "이 div에 box라는 이름표를 붙여라"라는 뜻이에요..box { color: red; font-size: 30px; }box 이름표가 붙은 요소네?" 하고 그 글자를 빨간색·30px로 꾸며줘요. 그러니까 class는 "이 요소가 어떤 그룹인지 알려주는 이름표"인 셈이죠.축구부라는 표시를 붙이죠. HTML도 똑같아요 — <div class="soccer"> 라고 붙이면 CSS .soccer { color: blue; } 가 그 그룹만 골라 꾸며줘요.class를 못 쓸까?class Person {} 처럼 class라는 문법이 존재해요. 이름이 겹쳐 충돌하니까, React는 대신 className을 쓰는 거예요. 둘은 완전히 같은 역할이에요.<div className="box"> 라고 쓰면, React가 최종적으로 브라우저에는 <div class="box"> 를 만들어 줘요. 즉 작성할 때는 className, 브라우저가 받는 HTML은 class 예요.class는 "CSS가 이 요소를 찾도록 붙이는 이름표(속성)"이고, JSX에선 자바스크립트의 class 문법과 겹쳐서 className을 쓴다.
<label for="...">의 for 역시 자바스크립트의 for 반복문과 겹쳐서, JSX에선 htmlFor로 씁니다.
// HTML: <label for="email">
// JSX:
const el = <label htmlFor="email">이메일</label>;
onclick처럼 전부 소문자지만, JSX는 카멜케이스(camelCase)로 onClick, onChange, onMouseOver처럼 써요. 그리고 값으로 함수(문자열이 아니라!)를 넘겨요.
// HTML: <button onclick="doSomething()">
// JSX:
const el = <button onClick={handleClick}>클릭</button>;
// 속성값이 문자열 "..."이 아니라, 중괄호 안의 함수라는 점에 주목!
<img>, <br>, <input>처럼 닫지 않아도 됐지만, JSX(XML 규칙)에선 모든 태그를 닫아야 해요. 자식이 없는 태그는 스스로 닫는 형태 />로 마무리합니다.
// HTML: <img src="a.png"> <br> <input type="text">
// JSX (반드시 닫기):
const el = (
<div>
<img src="a.png" />
<br />
<input type="text" />
</div>
);
style="color:red"처럼 문자열을 줬지만, JSX는 자바스크립트 객체를 줘요. 그래서 중괄호가 두 겹이에요 — 바깥 { }는 "JSX 안에서 JS를 연다", 안쪽 { }는 "객체"예요. 속성 이름도 background-color 대신 카멜케이스 backgroundColor를 씁니다.
// HTML: <div style="color:red; background-color:yellow">
// JSX (객체로!):
const el = <div style={{ color: 'red', backgroundColor: 'yellow' }}>내용</div>;
// ▲바깥 {}=JS ▲안쪽 {}=객체
| HTML | JSX | 이유 |
|---|---|---|
class | className | class는 JS 예약어 |
for | htmlFor | for는 JS 반복문 키워드 |
onclick | onClick | JSX는 카멜케이스 이벤트 |
<br> | <br /> | 모든 태그는 닫아야 함 |
style="..." | style={{...}} | 문자열이 아니라 객체 |
class="box"라고 쓰면 리액트가 콘솔에서 경고를 띄우고 CSS가 안 먹을 수 있어요. HTML 습관 때문에 나오는 실수 1위가 바로 이거예요. className을 기억하세요!JSX 한 덩어리는 최상위 요소가 반드시 하나여야 해요. 아래처럼 나란히 두 개를 반환하면 에러가 납니다.
// ❌ 에러! 최상위가 <h1>과 <p> 두 개
const el = (
<h1>제목</h1>
<p>본문</p>
);
React.createElement(...) 하나의 값으로 변환돼요. 함수가 값을 돌려줄 때 return은 하나만 돌려줄 수 있죠. 마치 상자에 물건을 담아 건네줄 때 상자 하나로 묶어야 하는 것과 같아요. 그래서 여러 요소를 부모 하나로 감싸야 합니다.
React.createElement 얘기는 사실 지금 단계에선 오히려 헷갈려요. 아직 createElement가 뭔지 안 배웠으니까요. 그것 없이도 이해할 수 있는 훨씬 쉬운 출발점이 있어요: "함수는 값을 하나만 반환(return)한다."function test() {
return 10;
}
// 이 함수의 결과 → 10
return "안녕" 이면 결과는 안녕 이에요. 여기까진 쉽죠.function test() {
return 10;
return 20; // 이 줄은 실행조차 안 돼요
}
결과가 10 과 20 둘 다일까요? 아니에요. 함수는 첫 번째 return을 만나는 순간 그 자리에서 끝나요. 그래서 10 하나만 돌려줘요. 이게 핵심이에요 — 함수가 돌려주는 값은 언제나 딱 하나.function App() {
return (
<h1>안녕하세요</h1>
);
} // ✅ <h1> 하나를 반환 → OK
그런데 이렇게 형제 둘을 나란히 두면?function App() {
return (
<h1>제목</h1>
<p>본문</p>
);
} // ❌ 에러
리액트 입장에선 이래요: "잠깐, 뭘 반환하라는 거야? <h1>? <p>? 둘 다?" 함수는 값을 하나만 줄 수 있는데 형제가 둘이라 애매해진 거예요.function App() {
return (
<div>
<h1>제목</h1>
<p>본문</p>
</div>
);
}
이제 반환되는 건 <div> 딱 하나예요. 그 안에 h1과 p가 들어있는 거죠.<div>로 감싸면 리액트가 보는 건 뿌리 하나짜리 나무 하나예요:
<h1>과 <p>를 그냥 나란히 두면 나무가 둘이 돼서, 리액트가 "어느 나무를 반환해야 하지?" 하고 막혀요.<div>로 묶는 경우 <div>는 하나다."
<div> 하나로 감싼다.
<div>일 필요는 없어요. 아래 해결 방법 2 — Fragment <>…</>는 화면에 태그를 하나도 안 남기면서 묶어주는 "투명한 상자"예요.<section>
<h1>제목</h1>
<p>내용</p>
</section>
<main>
<h1>제목</h1>
<p>내용</p>
</main>
<>
<h1>제목</h1>
<p>내용</p>
</>
즉, 부모 요소가 하나만 있으면 된다.
function Profile() {
return (
<h2>이름</h2>
<p>소개</p>
); // ❌ 에러! 최상위 요소가 2개
}
이걸 해결하려고 예전에는 흔히 `<div>`로 감쌌습니다.
<div>가 "의미 없이 껍데기만 추가"한다는 것<div>는 레이아웃상 필요해서 넣은 게 아니라, 그냥 React 문법 규칙을 맞추려고 억지로 넣은 것뿐이에요. 그런데 이게 쌓이면:<div>가 계속 중첩됩니다.<table>) 안에서 컴포넌트를 쓸 때, 엉뚱한 <div>가 <tr>/<td> 사이에 끼어들면 HTML 구조 자체가 깨지는 문제가 생기기도 합니다.flex나 grid 레이아웃을 짤 때도, 이 의미 없는 <div> 때문에 자식 요소 간격이나 정렬이 예상과 다르게 나올 수 있습니다.function Profile() {
return (
<>
<h2>이름</h2>
<p>소개</p>
</>
); // ✅ 실제 HTML엔 <div> 없이 <h2>, <p>만 남음
}
<>...</>는 "여러 요소를 묶긴 하는데, 실제 화면에는 아무 태그도 남기지 마"라는 뜻입니다. React 문법(최상위 요소 1개)은 만족시키면서, 결과물 HTML에는 껍데기가 안 남는 거예요.| 방식 | 실제 HTML에 남는 것 |
|---|---|
<div>로 감싸기 | <div><h2>...</h2><p>...</p></div> |
<>...</>(Fragment) | <h2>...</h2><p>...</p> (div 없음) |
해결 방법 1 — <div>로 감싸기. 가장 쉬운 방법이에요.
// ✅ div 하나로 감싸기
const el = (
<div>
<h1>제목</h1>
<p>본문</p>
</div>
);
해결 방법 2 — Fragment <>…</>. <div>로 감싸면 실제 화면에 불필요한 태그가 하나 더 생겨요. 그게 싫을 때, 화면에 아무 태그도 남기지 않고 묶어주는 Fragment(프래그먼트)를 씁니다. 빈 태그 <> … </> 모양이에요.
// ✅ Fragment로 감싸기 (실제 DOM에 여분의 태그가 안 생겨요)
const el = (
<>
<h1>제목</h1>
<p>본문</p>
</>
);
// <>…</> 는 <React.Fragment>…</React.Fragment> 의 짧은 형태예요.
<div>에 className이나 style이 필요하면 <div>를, 순전히 "묶기"만 필요하고 여분의 태그를 남기고 싶지 않으면 Fragment <></>를 쓰면 돼요.JSX 태그 안쪽에 주석을 달 때는 {/* … */} 형태를 써요. 중괄호로 자바스크립트 세상을 열고, 그 안에 자바스크립트 주석 /* */을 넣는 구조예요. HTML 주석 <!-- -->은 JSX 안에서 그대로 쓰면 안 됩니다.
const el = (
<div>
{/* 이건 JSX 안의 주석이에요 */}
<h1>안녕</h1>
{/* 여러 줄
주석도 가능 */}
</div>
);
&&)과 리스트 렌더링(.map())은 실전에서 아주 많이 쓰여요. 이건 뒤 챕터에서 예제와 함께 제대로 다룹니다. 지금은 "중괄호 안엔 식만, if·for는 못 쓴다"는 원리만 확실히 챙기면 충분해요.
다음 CH 03에서는 JSX 덩어리를 재사용 가능한 부품(컴포넌트)으로 만들고, 데이터를 props로 넘기는 법을 배워요.
createElement로 변환 → 중괄호엔 식만 → HTML과 다른 규칙 몇 가지 → 하나로 감싸기." 이 흐름이 머릿속에 그려지면 이번 장은 통과예요. 아래 문제로 확인해봐요.React.createElement(...) 호출로 변환해요. 결국 자바스크립트!{ } 안엔 식(expression)만 — if·for 같은 문(statement)은 못 넣어요. 삼항·&&·.map()은 OK.class→className, for→htmlFor, onclick→onClick(카멜케이스), 모든 태그 닫기(/>), style은 객체({{}}).<div> 또는 Fragment <></>로 감싸기.{/* … */} 형태로.