ToolPal
React JSX 문법이 표시된 어두운 모니터의 코드

HTML to JSX: 모든 React 입문자가 걸려 넘어지는 변환

📷 Pexels / Pexels

HTML to JSX: 모든 React 입문자가 걸려 넘어지는 변환

HTML을 React JSX로 변환하는 건 단순한 찾기-바꾸기가 아닙니다. 무엇이 바뀌고, 왜 중요한지, 흔한 실수를 어떻게 피하는지 알아봅니다.

2026년 3월 28일9분 소요

React를 배우는 개발자라면 누구나 한 번쯤 겪는 순간이 있습니다. 디자인 파일에서 받은 HTML이든, 예전 프로젝트에서 복사한 코드든, 온라인에서 찾은 템플릿이든 — 그걸 React 컴포넌트에 그대로 붙여넣는 순간 오류 메시지가 쏟아지기 시작합니다.

Warning: Invalid DOM property `class`. Did you mean `className`?

그리고 또.

Warning: Invalid DOM property `for`. Did you mean `htmlFor`?

그러다가 닫히지 않은 <img> 태그에서 파싱 에러까지 발생합니다. 여섯 번째 경고를 보고 있을 즈음에는 "왜 React는 그냥 HTML을 받아들이지 않는 거야?"라고 생각하게 됩니다. 사실 충분히 이해할 수 있는 반응입니다.

JSX가 그냥 HTML이 아닌 이유

짧게 말하면: JSX는 HTML처럼 생겼지만 실제로는 JavaScript입니다. Babel이 컴포넌트를 처리할 때, <div className="wrapper"> 같은 코드는 React.createElement('div', { className: 'wrapper' })로 변환됩니다. 이 변환 덕분에 JSX가 강력한 것이지만, 동시에 HTML을 그대로 복사할 수 없는 이유이기도 합니다.

HTML은 마크업 언어입니다. 브라우저는 HTML 파싱에 있어 꽤 관대합니다. 반면 JSX는 HTML을 닮은 JavaScript 문법입니다. JavaScript 파서는 관대하지 않습니다. 예약어가 있고, 엄격한 구문 규칙이 있으며, 모호함을 허용하지 않습니다.

HTML의 유연함과 JavaScript의 엄격함 사이의 이 긴장감이 바로 모든 변환 오류의 근본 원인입니다.

알아야 할 변환 규칙들

1. classclassName

이건 대부분 경고 메시지를 보고 처음 배우게 됩니다.

<!-- HTML -->
<div class="container main-content">
  <p class="text-gray">안녕하세요</p>
</div>
// JSX
<div className="container main-content">
  <p className="text-gray">안녕하세요</p>
</div>

이유는 간단합니다. class는 JavaScript에서 예약어입니다 (ES6 클래스를 정의할 때 쓰는 바로 그 class입니다). JSX에서 class를 속성으로 허용하면 JavaScript 파서가 혼란을 겪습니다. className은 이 충돌을 피하기 위한 우회 방법으로, DOM의 className 프로퍼티에 직접 매핑됩니다. React가 실제 DOM으로 렌더링할 때 className 프로퍼티를 설정하고, 이는 HTML의 class 속성에 해당합니다. 결과는 동일하고 문법만 다릅니다.

2. forhtmlFor

같은 논리입니다. for도 JavaScript 예약어입니다 (for 루프에 쓰는 바로 그 for입니다). 폼에서 레이블을 입력 필드에 연결할 때 for 속성을 사용합니다:

<!-- HTML -->
<label for="email">이메일 주소</label>
<input type="email" id="email" />
// JSX
<label htmlFor="email">이메일 주소</label>
<input type="email" id="email" />

className은 기억해도 htmlFor는 깜빡하는 경우가 많습니다. 덜 자주 언급되지만 놓치면 똑같이 경고가 뜹니다.

3. void 요소의 셀프 클로징

HTML에서 자식을 가질 수 없는 void 요소들은 닫는 슬래시가 없어도 됩니다. 브라우저는 이를 잘 처리합니다:

<!-- 유효한 HTML -->
<img src="photo.jpg" alt="사진">
<input type="text" name="username">
<br>
<hr>
<meta charset="UTF-8">

JSX에서는 모든 요소가 명시적으로 닫혀야 합니다. > 앞에 닫는 슬래시를 추가하거나, 별도의 닫는 태그를 써야 합니다:

// JSX - 반드시 셀프 클로징
<img src="photo.jpg" alt="사진" />
<input type="text" name="username" />
<br />
<hr />
<meta charSet="UTF-8" />

마지막 예제에서 charsetcharSet으로 바뀐 것도 주목하세요. 이건 다음에 설명할 camelCase 패턴입니다.

4. camelCase 속성명

HTML 속성은 대소문자를 구분하지 않습니다. JSX 속성명은 DOM 프로퍼티에 매핑되고, DOM 프로퍼티는 camelCase 규칙을 따릅니다. 자주 마주치는 것들:

HTMLJSX
onclickonClick
onchangeonChange
tabindextabIndex
maxlengthmaxLength
readonlyreadOnly
autocompleteautoComplete
autofocusautoFocus
crossorigincrossOrigin
charsetcharSet

이벤트 핸들러가 가장 자주 마주치는 경우입니다. HTML 이벤트 속성은 모두 소문자입니다:

<!-- HTML -->
<button onclick="handleClick()">클릭하기</button>
<input onchange="handleChange(event)" />

JSX에서는 camelCase를 사용하고 함수 참조를 전달합니다 (문자열이 아닙니다):

// JSX
<button onClick={handleClick}>클릭하기</button>
<input onChange={handleChange} />

마지막 부분 — 문자열 호출이 아닌 함수 참조를 전달한다는 점 — 이 중요한 차이입니다. HTML onclick에서 값은 실행할 JavaScript 문자열입니다. JSX onClick에서 값은 JavaScript 표현식(보통 함수 참조)입니다. 그래서 JSX는 값 주위에 따옴표 대신 중괄호 {}를 사용합니다.

5. 인라인 스타일을 JavaScript 객체로

처음 접하면 의아한 부분입니다. HTML 인라인 스타일은 문자열입니다:

<!-- HTML -->
<div style="color: red; font-size: 16px; margin-top: 8px;">
  스타일된 텍스트
</div>

JSX 인라인 스타일은 JavaScript 객체입니다. 즉:

  • 이중 중괄호 (하나는 JSX 표현식용, 하나는 객체 리터럴용)
  • camelCase 속성명
  • 문자열 또는 숫자로 된 값 (픽셀 값은 선택적으로 순수 숫자도 가능)
// JSX
<div style={{ color: 'red', fontSize: '16px', marginTop: '8px' }}>
  스타일된 텍스트
</div>

여기서도 camelCase 규칙이 적용됩니다: font-sizefontSize, margin-topmarginTop, background-colorbackgroundColor가 됩니다. 하이픈이 들어간 CSS 프로퍼티는 모두 camelCase로 바뀝니다.

6. 주석

HTML에서는 <!-- 주석 -->을 씁니다. JSX에서 HTML 스타일 주석은 동작하지 않습니다. JSX 표현식 안에 JavaScript 주석을 사용해야 합니다:

<!-- HTML 주석 -->
<div>
  <!-- 이것은 주석입니다 -->
  <p>내용</p>
</div>
{/* JSX 주석 */}
<div>
  {/* 이것은 주석입니다 */}
  <p>내용</p>
</div>

중괄호 내부나 일반 JavaScript 코드에서는 // 단일 행 주석도 사용할 수 있지만, JSX 마크업 안에 직접 쓰는 건 안 됩니다.

잘 알려지지 않은 함정들

불리언 속성의 차이

HTML에서 불리언 속성은 속성이 있으면 true를 의미합니다:

<!-- HTML - 이것들은 모두 동일합니다 -->
<input disabled>
<input disabled="true">
<input disabled="disabled">

JSX에서 불리언 속성은 단축 표기가 가능하지만 (HTML처럼), 명시적인 형태가 다릅니다:

// JSX - 이것들은 모두 동일합니다
<input disabled />
<input disabled={true} />

JSX에서 disabled="true" (따옴표 사용)는 피해야 합니다. 불리언 true 대신 문자열 "true"를 전달하기 때문입니다. React가 올바르게 렌더링할 수도 있지만, 관용적이지 않고 일부 컴포넌트에서 문제를 일으킬 수 있습니다.

리스트의 key prop

HTML 리스트를 JSX로 변환하고 데이터를 매핑할 때, React는 각 요소에 key prop이 필요합니다. 이건 엄밀히 HTML-to-JSX 변환 문제는 아닙니다 — 요소를 동적으로 생성할 때만 해당됩니다 — 하지만 경고를 만나기 전에 알아두는 게 좋습니다:

// key 없이는 React가 경고를 표시합니다
{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

SVG 속성

SVG는 자체적인 속성 명명 규칙이 있습니다. HTML/SVG에서는 fill-opacity, stroke-width, clip-path라고 쓸 수 있습니다. JSX에서는 각각 fillOpacity, strokeWidth, clipPath가 됩니다. 아이콘 라이브러리나 디자인 도구에서 인라인 SVG를 붙여넣는다면 약간의 정리가 필요할 것입니다.

tabindex vs tabIndex

놓치기 쉬운 부분입니다. 접근성 있는 컴포넌트를 만들면서 tabindex를 수동으로 설정한다면, JSX에서는 tabIndex가 된다는 걸 기억하세요. 이걸 놓쳐도 항상 에러가 나진 않지만 — React가 그대로 렌더링할 수도 있습니다 — 동작에 영향을 줄 수 있는 조용한 차이입니다.

자동화 변환기를 사용할 때

대용량 HTML 블록이 있다면 — 디자인 시스템 컴포넌트, React가 아닌 코드베이스에서 마이그레이션 중인 템플릿, 또는 온라인 소스에서 복사한 스니펫 — 변환기를 사용하는 게 실용적인 선택입니다. 모든 classclassName으로, 모든 하이픈 이벤트 핸들러를 수동으로 바꾸는 건 지루하고 오류가 발생하기 쉽습니다.

ToolPal HTML to JSX 변환기는 표준 변환을 자동으로 처리합니다: classclassName, forhtmlFor, 스타일 문자열 → 스타일 객체, void 요소 셀프 클로징, camelCase 속성. HTML을 붙여넣으면 유효한 JSX가 반환되고, 바로 작업을 이어갈 수 있습니다.

자동화 변환기가 도움을 줄 수 없는 부분은 구조적 결정입니다. HTML 500줄을 단일 JSX return 문으로 변환해도 잘 설계된 컴포넌트가 되는 게 아니라 변환된 덩어리만 생길 뿐입니다. 상당한 규모의 코드라면, 변환기가 문법을 처리하지만 결과를 더 작은 컴포넌트로 어떻게 나눌지, 하드코딩된 값 대신 어디에 prop을 써야 하는지, 어떤 부분을 동적으로 만들어야 하는지는 여전히 직접 결정해야 합니다.

기계적인 변환은 변환기에게, 아키텍처는 직접 하세요.

JSX를 처음부터 작성할 때

새 컴포넌트를 만들고 있고 디자인이 머릿속에 있다면 (또는 목업이 있다면), JSX를 처음부터 직접 작성하는 게 종종 더 빠릅니다. 처음부터 className을 쓸 것이고, void 요소를 닫는 걸 잊지 않을 것이며, 변환이 필요한 스타일 문자열도 생기지 않습니다.

변환이 실제로 도움이 되는 경우:

React가 아닌 프로젝트에서 마이그레이션할 때. 기존 HTML 템플릿이 있다면 — 순수 HTML/CSS 사이트, Rails 앱, Jinja 템플릿 — 변환 도구가 실제 시간을 절약해 줍니다.

디자인 핸드오프 작업 시. 디자이너는 종종 HTML 마크업을 내보내거나 스니펫을 제공합니다. 이를 변환기에 돌리면 수동 편집 없이 바로 사용 가능한 JSX를 얻을 수 있습니다.

서드파티 스니펫 붙여넣기 시. 문서, Stack Overflow 답변, 튜토리얼의 코드 예제는 종종 HTML로 작성됩니다. 빠른 변환으로 수동 편집 없이 사용 가능한 JSX를 얻을 수 있습니다.

대용량 폼. 수십 개의 입력 필드, 레이블, 필드셋이 있는 폼은 손으로 작성하기 번거롭습니다. HTML 구조를 붙여넣고 변환한 다음, 하드코딩된 값을 prop과 state로 교체하면 됩니다.

실제 변환 예시

다음은 HTML로 된 작은 카드 컴포넌트입니다:

<div class="card" id="product-card">
  <img src="/product.jpg" alt="상품 사진" class="card-image">
  <div class="card-body">
    <h2 class="card-title">상품 이름</h2>
    <p class="card-description" style="color: #666; font-size: 14px;">
      상품에 대한 짧은 설명입니다.
    </p>
    <label for="quantity">수량</label>
    <input type="number" id="quantity" min="1" max="99" readonly>
    <button class="btn btn-primary" onclick="addToCart()">장바구니에 추가</button>
  </div>
</div>

변환 후:

<div className="card" id="product-card">
  <img src="/product.jpg" alt="상품 사진" className="card-image" />
  <div className="card-body">
    <h2 className="card-title">상품 이름</h2>
    <p className="card-description" style={{ color: '#666', fontSize: '14px' }}>
      상품에 대한 짧은 설명입니다.
    </p>
    <label htmlFor="quantity">수량</label>
    <input type="number" id="quantity" min="1" max="99" readOnly />
    <button className="btn btn-primary" onClick={addToCart}>장바구니에 추가</button>
  </div>
</div>

모든 변경 사항을 확인해 보세요:

  • 모든 classclassName
  • <img><input>/> 로 셀프 클로징됨
  • style 문자열 → camelCase 프로퍼티를 가진 스타일 객체
  • forhtmlFor
  • readonlyreadOnly
  • onclick="addToCart()"onClick={addToCart} (괄호 없는 함수 참조)

마지막 이벤트 핸들러 부분은 강조할 가치가 있습니다. HTML 버전에서 onclick="addToCart()"는 평가되는 문자열입니다. JSX에서 실수로 onClick="addToCart()"를 쓰면 (따옴표 사용), React는 이벤트 핸들러가 함수여야 한다는 오류를 던집니다. 그리고 onClick={addToCart()}를 쓰면 (괄호 포함), 클릭 시가 아니라 렌더링 시에 즉시 함수가 호출됩니다. 둘 다 흔한 초보자 실수입니다.

변환기가 처리할 수 없는 것들

동적 콘텐츠. 변환기는 상품 이름{product.name}이 되어야 한다는 걸 모릅니다. 구조는 변환해주지만 정적 값을 prop과 state로 교체하는 건 직접 해야 합니다.

이벤트 핸들러 로직. onclick="addToCart()"onClick={addToCart}로 변환되지만, addToCart는 여전히 컴포넌트 어딘가에 정의되어야 합니다. 변환기는 속성 문법을 줘도 함수 자체는 여러분의 몫입니다.

조건부 렌더링. HTML에는 "X가 참일 때만 이 요소를 보여줘"라는 개념이 없습니다. 이 패턴은 컴포넌트화 단계에서 추가하는 것이지, 변환기가 정적 마크업에서 추론할 수 있는 게 아닙니다.

여러 루트 요소. HTML 스니펫의 최상위에 두 개의 형제 요소가 있다면, 변환된 JSX에도 두 개의 형제 요소가 생깁니다 — 이건 컴포넌트 return 문에서 유효하지 않습니다. <div><>...</> Fragment로 감싸야 합니다.

자주 하는 실수 정리

경험상 가장 많이 실수하는 패턴들을 정리해 보면:

이벤트 핸들러에 함수를 즉시 호출하는 실수:

// 잘못됨 - 렌더링 시 즉시 실행됨
<button onClick={handleClick()}>클릭</button>

// 올바름 - 클릭 시 실행됨
<button onClick={handleClick}>클릭</button>

// 인수가 필요한 경우 화살표 함수 사용
<button onClick={() => handleClick(id)}>클릭</button>

스타일 값에 단위 빠뜨리기:

// 애매한 경우 - React는 일부 숫자 값에 px를 자동 추가하지만
<div style={{ fontSize: 16 }}>텍스트</div>

// 명확한 표현 권장
<div style={{ fontSize: '16px' }}>텍스트</div>

리스트 렌더링에서 key 누락:

// 경고 발생
{items.map(item => <li>{item.name}</li>)}

// 올바름
{items.map(item => <li key={item.id}>{item.name}</li>)}

HTML-to-JSX 변환은 처음에는 귀찮게 느껴지지만 금방 익숙해집니다. React를 몇 주 쓰다 보면 className을 자동으로 타이핑하게 되고, <img />를 반사적으로 닫게 됩니다. 하지만 그 근육 기억이 형성되기 전까지, ToolPal HTML to JSX 변환기 같은 도구가 전환을 훨씬 빠르고 덜 불편하게 만들어 줍니다.

진짜 실력은 문법 차이를 외우는 데 있지 않습니다 — 그 차이가 왜 존재하는지 이해하는 것입니다. JSX가 JavaScript라는 걸 이해하고 나면, 규칙들이 더 이상 임의적으로 느껴지지 않고 자연스럽게 납득이 됩니다.

자주 묻는 질문

이 글 공유하기

XLinkedIn

관련 글