Ray Book
프레임워크의 철학

템플릿 vs JSX, 뷰를 어떻게 기술하는가

HTML을 확장할 것인가, JavaScript를 확장할 것인가, 4가지 템플릿 문법의 철학과 트레이드오프

frameworkjsxtemplatesyntaxcompiler

핵심 질문

UI를 기술하는 문법이 왜 갈라졌는가?

모든 프레임워크는 결국 HTML을 만듭니다. 하지만 그 HTML을 어떤 문법으로 기술하는가 에서 근본적인 갈림길이 생깁니다. React는 JavaScript 안에 HTML을 넣었고, Vue·Angular·Svelte는 HTML 안에 지시어를 넣었습니다. 같은 목적지를 향해 정반대 방향으로 출발한 셈입니다.

두 가지 철학

크게 두 진영으로 나뉩니다.

  • JavaScript 확장 (React JSX), JavaScript 표현식 안에 마크업을 삽입합니다. "It's just JavaScript"가 핵심 원칙입니다.
  • HTML 확장 (Vue, Angular, Svelte), HTML에 지시어 (directive) 나 블록 문법을 추가합니다. 마크업이 중심이고, 로직이 마크업에 녹아듭니다.

이 선택은 단순한 취향이 아닙니다. 조건문, 반복문, 이벤트 바인딩, 양방향 바인딩, 모든 표현 방식에 영향을 미치고, 결과적으로 컴파일러 최적화의 범위까지 결정합니다.

JSX, JavaScript 안의 HTML

React의 JSX는 JavaScript의 표현식입니다. <div>를 쓰는 순간 그것은 jsx('div', ...) (React 17+ 자동 변환) 로 변환됩니다. 따라서 JavaScript의 모든 도구를 그대로 쓸 수 있습니다.

function Dashboard({ user, notifications }) {
  // 변수, 조건문, 반복, 전부 JavaScript
  const greeting = user ? `안녕하세요, ${user.name}님` : '로그인해주세요';
  const urgentCount = notifications.filter(n => n.urgent).length;

  return (
    <div>
      <h1>{greeting}</h1>
      {urgentCount > 0 && <Badge count={urgentCount} />}
      <ul>
        {notifications.map(n => (
          <li key={n.id}>{n.message}</li>
        ))}
      </ul>
    </div>
  );
}

조건문은 삼항 연산자나 &&로, 반복은 .map()으로 처리합니다. 새로운 문법을 배울 필요가 없습니다. JavaScript를 알면 JSX를 아는 것입니다.

장점 : 표현의 자유도가 높습니다. 복잡한 조건 분기, 동적 컴포넌트 선택, 고차 함수 패턴 등을 언어 수준에서 자연스럽게 구현할 수 있습니다.

단점 : HTML처럼 생겼지만 HTML이 아닙니다. class 대신 className, for 대신 htmlFor를 써야 하고, 모든 태그는 닫혀야 합니다. 디자이너가 바로 이해하기 어려울 수 있습니다.

템플릿, HTML 안의 지시어

Vue, Angular, Svelte는 HTML을 기반으로 합니다. HTML에 조건부·반복·바인딩 같은 기능을 지시어블록 문법 으로 추가합니다. 세 프레임워크의 접근이 조금씩 다릅니다.

  • Vue : v-if, v-for, v-model 같은 v- 접두사 지시어. HTML 속성처럼 보입니다.
  • Angular : @if, @for 같은 제어 흐름 블록 (Angular 17+). 기존의 *ngIf, *ngFor보다 직관적입니다.
  • Svelte : {#if}, {#each} 같은 블록 문법. 머스태시 ({}) 안에 로직을 넣습니다.

세 프레임워크 모두 "마크업을 보면 UI 구조가 바로 보인다"는 공통 이점이 있습니다.

조건부 렌더링 비교

같은 조건, "로그인한 사용자에게 이름을 보여주고, 관리자에게는 AdminPanel을 추가로 보여준다", 을 네 프레임워크로 구현하면 다음과 같습니다.

function UserGreeting({ user }) {
  return (
    <div>
      {user ? (
        <h1>안녕하세요, {user.name}님</h1>
      ) : (
        <h1>로그인해주세요</h1>
      )}
      {user?.isAdmin && <AdminPanel />}
    </div>
  );
}

React는 JavaScript의 삼항 연산자와 &&를 씁니다. "이것은 표현식이다"라는 원칙이 여기서도 적용됩니다. 나머지 세 프레임워크는 각자의 지시어로 분기를 표현합니다. Vue의 v-if/v-else, Angular의 @if/@else, Svelte의 {#if}/{:else}, 문법은 다르지만 "HTML에 조건을 붙인다"는 발상은 같습니다.

리스트 렌더링 비교

배열을 순회하며 리스트를 그리는 방법도 같은 구조로 갈립니다.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

React는 .map()을 씁니다. 배열을 변환하는 JavaScript의 기본 메서드입니다. Vue는 v-for, Angular는 @for, Svelte는 {#each}로 각각 반복을 표현합니다. 네 프레임워크 모두 key (또는 track) 를 지정해서 리스트 항목을 고유하게 식별합니다. 이 식별자가 있어야 항목의 추가·삭제·재정렬 시 효율적으로 DOM을 업데이트할 수 있습니다.

이벤트 바인딩

클릭 이벤트 하나를 연결하는 방법도 네 가지입니다.

React:    <button onClick={handleClick}>
Vue:      <button @click="handleClick">
Angular:  <button (click)="handleClick()">
Svelte:   <button onclick={handleClick}>

React는 onClick처럼 camelCase를 쓰고 중괄호로 함수 참조를 전달합니다. Vue는 @click이라는 약어 (v-on:click) 를 씁니다. Angular는 (click)으로 이벤트를 괄호로 감쌉니다. Svelte는 소문자 onclick에 중괄호를 씁니다, 사실상 표준 HTML 이벤트 속성과 같은 형태입니다.

양방향 바인딩

입력 필드의 값을 상태와 동기화하는 문제에서 철학의 차이가 가장 뚜렷합니다.

// React, 제어 컴포넌트 (명시적 단방향)
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
<!-- Vue, v-model (양방향 바인딩 sugar) -->
<input v-model="name" />
<!-- Angular, [(ngModel)] (Two-way binding) -->
<input [(ngModel)]="name" />
<!-- Svelte, bind:value -->
<input bind:value={name} />

React는 양방향 바인딩을 의도적으로 제공하지 않습니다 . value를 내려보내고 onChange로 다시 올려받는 제어 컴포넌트 패턴을 씁니다. 데이터 흐름이 항상 명시적입니다. 나머지 세 프레임워크는 양방향 바인딩을 문법 수준에서 지원합니다. v-model, [(ngModel)], bind:value는 모두 "값 전달 + 이벤트 수신"을 한 줄로 줄인 것이지만, 개발자가 작성하는 코드는 훨씬 간결합니다.

트레이드오프, 자유 vs 최적화

이 두 철학의 가장 중요한 트레이드오프는 컴파일러 최적화 입니다.

JSX는 자유도가 높은 대신, 컴파일러가 "이 코드가 무엇을 하려는지" 파악하기 어렵습니다. {someCondition && <Component />}를 봐도 someCondition이 어디서 오는지, 얼마나 자주 바뀌는지를 정적으로 알 수 없습니다. 그래서 React는 런타임에 Virtual DOM 비교에 의존합니다.

반면 템플릿은 제약이 곧 정보 입니다. v-if="user"를 보면 컴파일러는 "user가 바뀔 때만 이 블록을 토글하면 된다"는 사실을 빌드 타임에 알 수 있습니다. Vue의 컴파일러는 정적 노드를 끌어올리고 (static hoisting), 동적 부분에만 패치 플래그를 달아 VDOM 비교 범위를 줄입니다. Svelte는 한 발 더 나아가 VDOM 자체를 제거하고, 컴파일러가 변경된 상태와 DOM 노드를 직접 연결하는 코드를 생성합니다.

ReactVueAngularSvelte
문법JSXTemplate + SFCTemplateSvelte syntax
조건부{cond && ...} / 삼항v-if / v-else@if / @else{#if} / {:else}
반복.map()v-for@for{#each}
이벤트onClick={fn}@click="fn"(click)="fn()"onclick={fn}
양방향제어 컴포넌트v-model[(ngModel)]bind:value
컴파일러 최적화제한적강력강력매우 강력

React도 이 한계를 인식하고 있습니다. React Compiler (v1.0, 2025) 는 JSX를 정적 분석해 자동으로 메모이제이션을 적용합니다. useMemouseCallback을 수동으로 작성하지 않아도 컴파일러가 불필요한 렌더링을 제거해 줍니다. 하지만 범용 JavaScript를 정적 분석하는 것은 제약된 템플릿 문법을 분석하는 것보다 근본적으로 어려운 문제이며, Rules of React를 엄격히 따르는 코드에서만 최적의 효과를 발휘합니다.

결국 선택은 "표현의 자유"와 "컴파일러가 줄 수 있는 최적화" 사이의 균형점입니다. 어느 쪽이 더 중요한지는 프로젝트의 규모, 팀의 구성, 성능 요구사항에 따라 달라집니다.


다음 글에서는 빌드와 런타임 을 다룹니다. 프레임워크가 개발자의 코드를 브라우저가 실행할 수 있는 결과물로 변환하는 과정, 번들링, 코드 스플리팅, 서버 렌더링까지 살펴보겠습니다.