JSX 완전 이해
JSX란?
JSX(JavaScript XML)는 JavaScript 파일 안에 HTML과 유사한 문법을 작성할 수 있게 해주는 구문 확장입니다. JSX는 브라우저가 직접 이해하지 못하므로, Babel 또는 SWC가 일반 JavaScript 코드로 변환합니다.
// JSX 작성
const element = <h1 className="title">안녕하세요!</h1>;
// Babel 변환 후 (React 17 이전)
const element = React.createElement(
'h1',
{ className: 'title' },
'안녕하세요!'
);
// Babel 변환 후 (React 17+ 자동 JSX Transform)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { className: 'title', children: '안녕하세요!' });
React 17+에서는 import React from 'react'를 작성하지 않아도 됩니다.
React.createElement 이해
JSX는 결국 React.createElement(type, props, ...children) 호출입니다.
// JSX
function Welcome() {
return (
<div className="container">
<h1>환영합니다</h1>
<p>React 학습 중</p>
</div>
);
}
// 변환 결과 (개념적으로)
function Welcome() {
return React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, '환영합니다'),
React.createElement('p', null, 'React 학습 중')
);
}
이 구조를 이해하면 JSX의 제약 조건들이 이해됩니다.
표현식 삽입
중괄호 {} 안에 JavaScript 표현식을 삽입합니다. 문장(statement)은 불가합니다.
const name = '김철수';
const today = new Date();
const isAdmin = true;
function Profile() {
return (
<div>
{/* 변수 */}
<h1>안녕하세요, {name}님!</h1>
{/* 함수 호출 */}
<p>오늘: {today.toLocaleDateString('ko-KR')}</p>
{/* 삼항 연산자 */}
<span>{isAdmin ? '관리자' : '일반 사용자'}</span>
{/* 계산식 */}
<p>1 + 1 = {1 + 1}</p>
{/* 문자열 템플릿 */}
<p className={`badge ${isAdmin ? 'admin' : 'user'}`}>배지</p>
</div>
);
}
조건부 렌더링
1. 삼항 연산자
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<h1>다시 오셨군요!</h1>
) : (
<h1>로그인해 주세요.</h1>
)}
</div>
);
}
2. 논리 AND 연산자 (&&)
function Notification({ hasUnread, count }) {
return (
<div>
<h1>메시지함</h1>
{/* hasUnread가 true일 때만 렌더링 */}
{hasUnread && <span className="badge">{count}</span>}
</div>
);
}
주의: count가 0이면 0 && ...은 0을 렌더링합니다.
// ❌ count가 0이면 "0"이 출력됨
{count && <span>{count}개</span>}
// ✅ Boolean으로 명시적 변환
{count > 0 && <span>{count}개</span>}
{!!count && <span>{count}개</span>}
3. null 반환으로 숨기기
function Alert({ type, message }) {
if (!message) return null; // 렌더링 없이 종료
return (
<div className={`alert alert-${type}`}>
{message}
</div>
);
}
4. 조기 반환 패턴 (Early Return)
function UserDashboard({ user, isLoading, error }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <p>사용자를 찾을 수 없습니다.</p>;
// 메인 내용
return (
<div>
<h1>{user.name}의 대시보드</h1>
</div>
);
}
리스트 렌더링
map과 key
const products = [
{ id: 1, name: '노트북', price: 1200000 },
{ id: 2, name: '마우스', price: 35000 },
{ id: 3, name: '키보드', price: 89000 },
];
function ProductList() {
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} — {product.price.toLocaleString()}원
</li>
))}
</ul>
);
}
key 선택 규칙
// ✅ 데이터의 고유 ID
<li key={item.id}>
// ✅ 안정적인 고유 값
<li key={item.slug}>
// ❌ 배열 인덱스 (항목 재정렬 시 버그 발생)
<li key={index}>
// ❌ Math.random() (매 렌더링마다 바뀜)
<li key={Math.random()}>
중첩 리스트
function CategoryList({ categories }) {
return (
<div>
{categories.map(category => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
))}
</div>
);
}
JSX 규칙
1. 단일 루트 엘리먼트
// ❌ 여러 루트 엘리먼트 반환 불가
function Bad() {
return (
<h1>제목</h1>
<p>내용</p>
);
}
// ✅ 방법 1: div로 감싸기
function Good1() {
return (
<div>
<h1>제목</h1>
<p>내용</p>
</div>
);
}
// ✅ 방법 2: Fragment 사용 (DOM에 추가 노드 없음)
function Good2() {
return (
<>
<h1>제목</h1>
<p>내용</p>
</>
);
}
// ✅ 방법 3: Fragment (key가 필요할 때)
function Good3({ items }) {
return items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
));
}
2. className과 htmlFor
HTML 속성 중 JavaScript 예약어와 충돌하는 것들은 다른 이름을 사용합니다.
// HTML → JSX
// class → className
// for → htmlFor
<div class="container"> // ❌ HTML 방식
<div className="container"> // ✅ JSX 방식
<label for="email"> // ❌
<label htmlFor="email"> // ✅
3. camelCase 속성
// HTML → JSX
// tabindex → tabIndex
// maxlength → maxLength
// onclick → onClick
// onchange → onChange
// style 값도 camelCase
<input tabIndex={1} maxLength={50} />
<div style={{ backgroundColor: '#fff', fontSize: '16px' }} />
4. 태그 닫기
HTML에서 self-closing이 없는 태그도 JSX에서는 반드시 닫아야 합니다.
// ❌ HTML처럼 열린 태그
<input type="text">
<br>
<img src="photo.jpg">
// ✅ JSX 방식
<input type="text" />
<br />
<img src="photo.jpg" />
5. JavaScript 예약어 주의
// ❌
<button class="btn" for="input">
// ✅
<button className="btn">
// 기타 주의 사항
// onclick → onClick
// onkeydown → onKeyDown
실전 예제: 상품 카드 컴포넌트
function ProductCard({ product, onAddToCart }) {
const { id, name, price, image, inStock, rating } = product;
return (
<div className={`product-card ${inStock ? '' : 'out-of-stock'}`}>
<img src={image} alt={name} />
<div className="product-info">
<h3>{name}</h3>
<div className="rating">
{'★'.repeat(Math.floor(rating))}
{'☆'.repeat(5 - Math.floor(rating))}
<span>({rating})</span>
</div>
<p className="price">{price.toLocaleString('ko-KR')}원</p>
{inStock ? (
<button
className="btn-add"
onClick={() => onAddToCart(id)}
>
장바구니 추가
</button>
) : (
<button disabled className="btn-disabled">
품절
</button>
)}
</div>
</div>
);
}
// 사용 예시
function App() {
const products = [
{ id: 1, name: '무선 키보드', price: 89000, image: '/kb.jpg', inStock: true, rating: 4.5 },
{ id: 2, name: '마우스 패드', price: 15000, image: '/pad.jpg', inStock: false, rating: 4.0 },
];
function handleAddToCart(id) {
console.log(`상품 ${id} 장바구니 추가`);
}
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
);
}
고수 팁
1. JSX는 표현식이다
JSX를 변수에 저장하거나 함수에서 반환할 수 있습니다.
const loadingUI = <Spinner size="large" />;
const errorUI = error && <ErrorBanner message={error.message} />;
return (
<div>
{isLoading ? loadingUI : errorUI}
</div>
);
2. 렌더 프롭(Render Prop) 미리 맛보기
// 조건부 렌더링을 함수로 추출
function ConditionalRender({ condition, children, fallback }) {
return condition ? children : (fallback ?? null);
}
// 사용
<ConditionalRender condition={isAdmin} fallback={<AccessDenied />}>
<AdminPanel />
</ConditionalRender>
3. 스타일 객체 외부에 정의
// ❌ 매 렌더링마다 새 객체 생성
<div style={{ color: 'red', fontWeight: 'bold' }}>
// ✅ 컴포넌트 외부에 정의
const errorStyle = { color: 'red', fontWeight: 'bold' };
function ErrorText({ text }) {
return <div style={errorStyle}>{text}</div>;
}