입문

React 강좌 02강 — JSX 완전 정복: HTML과 JavaScript의 결합

🎯 학습 목표

  • JSX가 JavaScript로 변환되는 과정을 이해한다
  • Fragment, null/false/undefined 렌더링 규칙을 익힌다
  • JSX와 HTML의 속성 차이(className, htmlFor, style)를 정확히 구분한다
  • && 단락 평가의 pitfall을 알고 안전하게 사용한다
  • JSX 표현식 안에서 JavaScript를 활용하는 패턴을 익힌다

📖 핵심 개념 1 — JSX는 문법 설탕(Syntactic Sugar)

JSX는 JavaScript 확장 문법입니다. 브라우저는 JSX를 직접 이해하지 못하므로, Babel(또는 Vite의 경우 esbuild/swc)이 JSX를 순수 JavaScript로 변환합니다.

// 개발자가 작성하는 JSX
function Greeting({ name }) {
  return (
    <div className="greeting">
      <h1>안녕하세요, {name}님!</h1>
      <p>React 강좌에 오신 것을 환영합니다.</p>
    </div>
  );
}

// Babel이 변환한 실제 JavaScript
function Greeting({ name }) {
  return React.createElement(
    'div',
    { className: 'greeting' },
    React.createElement('h1', null, '안녕하세요, ', name, '님!'),
    React.createElement('p', null, 'React 강좌에 오신 것을 환영합니다.')
  );
}

이 변환 과정을 알면 JSX의 제약 조건이 이해됩니다. React.createElement()는 단 하나의 루트 요소만 반환할 수 있기 때문에, JSX도 항상 하나의 루트 요소로 감싸야 합니다.

📖 핵심 개념 2 — Fragment

루트 요소로 감싸야 하는 제약 때문에 불필요한 <div>가 생기는 문제가 있었습니다. Fragment로 해결합니다.

// ❌ 불필요한 div가 DOM에 추가됨
function List() {
  return (
    <div>  {/* 이 div는 레이아웃에 영향을 줄 수 있음 */}
      <dt>React</dt>
      <dd>UI 라이브러리</dd>
    </div>
  );
}

// ✅ Fragment 사용 — DOM에 추가 요소 없음
function List() {
  return (
    <>  {/* 단축 문법 */}
      <dt>React</dt>
      <dd>React는 UI 라이브러리입니다</dd>
    </>
  );
}

// key prop이 필요할 때는 명시적 Fragment 사용
function ItemList({ items }) {
  return items.map(item => (
    <React.Fragment key={item.id}>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </React.Fragment>
  ));
}

단축 문법 <>...</>은 key prop을 사용할 수 없습니다. 리스트 렌더링처럼 key가 필요한 경우에는 <React.Fragment key={...}>를 사용해야 합니다. 패널에서 이 차이를 모르고 에러를 만나는 초보자가 많다고 지적했습니다.

📖 핵심 개념 3 — null, false, undefined 렌더링 규칙

JSX에서 null, false, undefined는 아무것도 렌더링하지 않습니다. 조건부 렌더링에서 자주 활용됩니다.

function Example() {
  return (
    <div>
      {null}       {/* 아무것도 출력 안 함 */}
      {false}      {/* 아무것도 출력 안 함 */}
      {undefined}  {/* 아무것도 출력 안 함 */}
      {0}          {/* ⚠️ 주의! 숫자 0은 "0"으로 렌더링됨 */}
      {""}         {/* 빈 문자열은 아무것도 출력 안 함 */}
    </div>
  );
}

⚠️ && 단락 평가 pitfall

const items = []; // 빈 배열

// ❌ 잘못된 패턴 — 화면에 "0"이 출력됨
function BadExample() {
  return (
    <div>
      {items.length && <ItemList items={items} />}
      {/* items.length가 0이면 0 && ... = 0 → "0"이 렌더링됨! */}
    </div>
  );
}

// ✅ 올바른 패턴 1 — Boolean으로 변환
function GoodExample1() {
  return (
    <div>
      {items.length > 0 && <ItemList items={items} />}
    </div>
  );
}

// ✅ 올바른 패턴 2 — 삼항 연산자
function GoodExample2() {
  return (
    <div>
      {items.length ? <ItemList items={items} /> : null}
    </div>
  );
}

이 pitfall은 경험 많은 개발자도 자주 실수합니다. 패널에서 “조건부 렌더링 시 항상 비교 연산자(> 0)를 쓰거나 삼항 연산자를 쓰는 것을 팀 컨벤션으로 정하라”고 권고했습니다.

📖 핵심 개념 4 — JSX 속성 차이

JSX는 HTML과 거의 같아 보이지만, 몇 가지 속성 이름이 다릅니다. JavaScript 예약어와 충돌을 피하기 위해서입니다.

// HTML vs JSX 속성 차이
function FormExample() {
  return (
    <form>
      {/* class → className (class는 JS 예약어) */}
      <div className="form-group">

        {/* for → htmlFor (for는 JS 예약어) */}
        <label htmlFor="email">이메일</label>

        {/* 이벤트 핸들러는 camelCase */}
        <input
          id="email"
          type="email"
          onChange={(e) => console.log(e.target.value)}
        />
      </div>

      {/* style은 문자열이 아닌 객체 — CSS 속성도 camelCase */}
      <button
        style={{
          backgroundColor: '#0070f3', // background-color → backgroundColor
          fontSize: '16px',           // font-size → fontSize
          borderRadius: '4px',        // border-radius → borderRadius
          color: 'white',
          padding: '8px 16px',
          cursor: 'pointer',
        }}
      >
        제출
      </button>
    </form>
  );
}

💻 코드 예제 — JSX 표현식 활용

import { useState } from 'react';

// JSX 안에서 다양한 JavaScript 표현식 활용
function JsxExpressions() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const user = { name: '김개발', role: 'admin' };
  const scores = [85, 92, 78, 95];

  return (
    <div className="container">
      {/* 변수 */}
      <h1>{user.name}님의 대시보드</h1>

      {/* 삼항 연산자 */}
      <p>상태: {isLoggedIn ? '로그인됨' : '로그아웃됨'}</p>

      {/* 함수 호출 */}
      <p>평균 점수: {(scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)}점</p>

      {/* 배열 렌더링 */}
      <ul>
        {scores.map((score, index) => (
          <li key={index}>{index + 1}회차: {score}점</li>
        ))}
      </ul>

      {/* 조건부 렌더링 */}
      {user.role === 'admin' && (
        <div className="admin-panel">관리자 패널</div>
      )}

      <button onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? '로그아웃' : '로그인'}
      </button>
    </div>
  );
}

export default JsxExpressions;

⚠️ 흔한 실수 (よくあるミス)

  • JSX 안에서 if 문 사용: JSX 중괄호 {} 안에는 표현식(expression)만 올 수 있습니다. if 문은 명령문(statement)이므로 사용할 수 없습니다. 삼항 연산자나 &&를 사용하세요.
  • style에 문자열 전달: style="color: red"는 HTML에서는 되지만 JSX에서는 오류입니다. 반드시 객체로 전달해야 합니다: style={{ color: 'red' }}
  • 자기 닫는 태그 누락: HTML에서는 <br>이 유효하지만 JSX에서는 <br />처럼 반드시 닫아야 합니다. <input>, <img>, <hr> 등도 마찬가지입니다.
  • 숫자 0의 렌더링: 위에서 설명한 && pitfall. items.length && 대신 items.length > 0 &&를 사용하세요.
  • camelCase 잊기: onclick 대신 onClick, onchange 대신 onChange. JSX 이벤트 핸들러는 모두 camelCase입니다.

💡 실무 팁

  • JSX를 반환하는 함수는 괄호로 감싸기: 멀티라인 JSX는 return (...)으로 감싸지 않으면 ASI(자동 세미콜론 삽입)로 인해 return undefined가 될 수 있습니다.
  • 복잡한 JSX는 변수로 분리: JSX 표현식이 길어지면 별도 변수에 저장하여 가독성을 높이세요. const header = <h1>...</h1>;
  • Prettier 설정: JSX 포매팅을 자동화하면 팀 전체 코드 스타일이 일관됩니다. .prettierrc"singleQuote": true를 추가하는 것을 권장합니다.
  • eslint-plugin-react-hooks 설치: JSX/React 관련 ESLint 규칙을 설정하면 흔한 실수를 코딩 단계에서 잡을 수 있습니다.