React 기초2편

JSX — HTML처럼 생겼지만 사실은 JavaScript

React가 화면을 적는 문법 JSX의 정체를 짚습니다. HTML과 닮았지만 왜 다른지, 중괄호로 무엇을 하는지, class 대신 className을 쓰는 이유까지 웹 경험에 빗대어 정리합니다.

지난 글에서 컴포넌트는 “화면을 그리는 설계도를 반환하는 함수”라고 했습니다. 그런데 그 설계도, 즉 return 뒤에 오는 HTML처럼 생긴 그것의 정체가 궁금했을 겁니다. 함수 안에 태그가 들어가 있는 모습은 처음 보면 분명 낯서니까요. 그 문법의 이름이 바로 JSX입니다.

JSX는 웹 경험이 있는 사람에게 양날의 검입니다. HTML과 닮아서 진입장벽을 낮춰주지만, 딱 그만큼 “HTML이겠거니” 하고 넘어갔다가 헷갈리게 만들기 때문입니다. 그래서 오늘은 JSX가 HTML과 무엇이 같고 무엇이 다른지를 정확히 갈라보겠습니다.


JSX는 HTML이 아니라 JavaScript다

JSX는 JavaScript XML의 줄임말입니다. 이름에 XML이 붙어 마크업처럼 보이지만, 핵심은 앞의 JavaScript에 있습니다. JSX는 별도의 마크업 언어가 아니라, JavaScript 안에 마크업을 쓸 수 있게 해주는 문법 확장입니다.

const element = <h1>안녕하세요</h1>;

이 한 줄은 브라우저가 그대로 이해하는 게 아닙니다. 빌드 과정에서 아래처럼 평범한 JavaScript 함수 호출로 변환됩니다.

const element = React.createElement('h1', null, '안녕하세요');

그러니까 우리가 태그를 쓰는 건 사실 함수를 부르는 일이고, JSX는 그걸 보기 좋게 적도록 도와주는 껍데기인 셈입니다. “HTML을 JS에 붙였다”가 아니라 “JS가 마크업처럼 보이는 문법을 빌려왔다”가 정확한 이해입니다. 이 점을 붙잡고 있으면 뒤에 나올 차이들이 전부 자연스럽게 설명됩니다.

참고로 React 17부터는 이 변환 방식이 바뀌어, JSX만 쓰려고 파일 상단에 import React를 적지 않아도 됩니다. 예전 코드에서 그 import를 보게 되는 이유입니다.


중괄호 {} — 마크업에서 JavaScript로 나가는 문

JSX가 JavaScript이기 때문에, 마크업 한가운데에서 다시 JavaScript로 빠져나올 수 있습니다. 그 출입구가 중괄호 {}입니다.

const name = "카리나";
const element = <h1>안녕하세요, {name}님</h1>;

{} 안에는 변수뿐 아니라 계산식, 함수 호출 등 값을 만들어내는 표현식(expression)이면 무엇이든 들어갈 수 있습니다.

<p>{1 + 2}</p>              {/* 3 */}
<p>{user.isAdmin ? '관리자' : '회원'}</p>

단, 들어갈 수 있는 건 “값이 되는 표현식”까지입니다. if 문이나 for 문 같은 문장(statement)은 넣을 수 없습니다. 이 구분은 처음엔 걸리적거리지만, “JSX 안의 {}는 결국 하나의 값을 기대하는 자리”라고 생각하면 금방 익숙해집니다.


HTML과 미묘하게 다른 것들

닮은 만큼 어긋나는 지점들이 있습니다. 이유는 하나같이 “JSX는 JavaScript다”로 귀결됩니다.

HTMLJSX이유
class="box"className="box"class는 JavaScript 예약어라 못 씀
for="id"htmlFor="id"for도 예약어(반복문)
onclick="..."onClick={...}속성은 camelCase, 값은 함수
style="color:red"style={{ color: 'red' }}문자열이 아니라 객체
<br><br />모든 태그는 반드시 닫아야 함

특히 style을 눈여겨보세요. 바깥 {}는 “JavaScript로 나가는 문”, 안쪽 {}는 “객체”라서 중괄호가 두 겹입니다. background-color처럼 하이픈이 든 속성은 JavaScript 객체 키로 쓸 수 없어 backgroundColor처럼 camelCase로 적습니다. 웹의 스타일과 닮았지만, 어디까지나 JavaScript 객체라는 점이 다릅니다.


반환은 반드시 하나의 덩어리로

컴포넌트는 화면 설계도를 return하는데, 이때 최상위 요소는 하나여야 합니다. 함수가 값 하나를 반환하듯, JSX도 결국 하나의 결과로 모여야 하기 때문입니다.

// ❌ 형제 요소 두 개를 나란히 반환할 수 없음
return (
  <h1>제목</h1>
  <p>본문</p>
);

그렇다고 의미도 없는 <div>로 감싸 마크업을 지저분하게 만들긴 아깝습니다. 이때 쓰라고 있는 것이 프래그먼트(Fragment), 빈 태그 <>...</>입니다.

return (
  <>
    <h1>제목</h1>
    <p>본문</p>
  </>
);

<>는 실제 화면에 아무 요소도 남기지 않으면서 여러 형제를 하나로 묶어줍니다.


왜 굳이 이렇게 만들었을까

마크업과 로직을 굳이 한 파일, 한 함수 안에 섞는 이 방식은 처음엔 어색합니다. 오랫동안 우리는 HTML은 HTML끼리, JavaScript는 JavaScript끼리 파일을 나누는 걸 미덕으로 배웠으니까요.

하지만 지난 글에서 이야기한 컴포넌트의 발상을 떠올리면 이야기가 달라집니다. 버튼 하나를 만들 때 그 버튼의 생김새(마크업)행동(로직)은 사실 떼어놓기 어려운 한 덩어리입니다. JSX는 “화면과 그 화면을 만드는 논리는 원래 한 몸”이라는 관점을 문법으로 옮긴 것입니다. 파일 종류가 아니라 관심사(하나의 컴포넌트)를 기준으로 코드를 모으는 셈이지요.

이제 화면을 어떻게 적는지는 감이 잡혔습니다. 다음 글에서는 이 컴포넌트들을 조합할 때 부모가 자식에게 값을 건네는 방법, Props를 살펴보겠습니다.