본문으로 건너뛰기
· engineering · 4분 읽기

React vs Svelte: 반응성, 성능, 생태계 비교

프론트엔드 프레임워크를 고를 때 “뭐가 더 좋아?”라고 묻는 건 의미가 없다. “이 프로젝트에서 뭐가 더 맞아?”가 맞는 질문이다. React은 2013년부터 쌓아온 거대한 생태계가 있고, Svelte는 컴파일러 기반이라 접근 자체가 다르다. 둘 다 써보면서 느낀 점을 정리해봤다.

반응성 모델

상태가 바뀌었을 때 화면을 어떻게 다시 그릴까? React과 Svelte는 정반대의 방식을 택했다.

React: 런타임에서 해결한다

React은 상태가 변하면 컴포넌트 함수를 통째로 다시 돌린다. 이전 화면과 새 화면을 비교해서 바뀐 부분만 반영하는데, 이걸 가상 DOM 방식이라고 부른다.

import { useState, useEffect, useMemo } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const doubled = useMemo(() => count * 2, [count])

  useEffect(() => {
    console.log(`count: ${count}`)
    return () => console.log('cleanup')
  }, [count])

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

상태는 useState, 계산된 값은 useMemo, 부가 동작은 useEffect. 각 훅마다 “이 값이 바뀔 때만 실행해”라는 의존성 배열([count])을 직접 넣어줘야 한다. 이걸 빠뜨리거나 잘못 넣으면 무한 루프가 돌거나 업데이트가 안 된다. React 써본 사람이면 한 번쯤은 겪어봤을 거다.

Svelte 5: 컴파일러가 해결한다

Svelte는 가상 DOM이 없다. 빌드할 때 컴파일러가 “이 변수가 바뀌면 화면 어디를 고쳐야 하는지” 미리 파악해서, 필요한 코드만 만들어둔다.

<script>
  let count = $state(0)
  let doubled = $derived(count * 2)

  $effect(() => {
    console.log(`count: ${count}`)
    return () => console.log('cleanup')
  })
</script>

<div>
  <p>Count: {count}</p>
  <p>Doubled: {doubled}</p>
  <button onclick={() => count++}>+1</button>
</div>

$state로 선언한 변수는 그냥 count++로 바꾸면 된다. $derived는 어떤 값에 의존하는지 알아서 추적한다. $effect도 마찬가지. “이 값이 바뀔 때”를 직접 적어줄 필요가 없어서 편하다.

핵심 차이

React 19Svelte 5
반응성 시점실행 시 (브라우저)빌드 시 (컴파일러)
상태 변경setState(newVal)val = newVal
파생값useMemo(fn, deps)$derived(expr)
부수 동작useEffect(fn, deps)$effect(fn), 자동 추적
가상 DOM사용없음
의존성 관리수동 (배열)자동

React의 훅은 강력하지만, “올바르게 쓰기”가 어렵다. 의존성 배열 관리, 값이 예전 것으로 잡히는 문제, 불필요한 다시 그리기를 막기 위한 최적화 코드. 이런 것들이 React 코드의 상당 부분을 차지한다. 비즈니스 로직이 아니라 프레임워크를 달래는 코드라는 거다.

Svelte는 이 문제를 컴파일러에게 넘긴다. 개발자가 의존 관계를 직접 적을 필요가 없으니 실수할 여지도 줄어든다. 대신 “컴파일러가 내 코드를 어떻게 바꾸는지” 이해해야 하는 부분은 있다.

컴포넌트 작성 체감 차이

같은 걸 만들 때 코드가 얼마나 다른지 보자.

사용자 카드 컴포넌트

React:

import { useState } from 'react'

function UserCard({ name, email, role }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <div className="card">
      <h2>{name}</h2>
      <p>{email}</p>
      {expanded && <p className="role">{role}</p>}
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? '접기' : '펼치기'}
      </button>
    </div>
  )
}

Svelte 5:

<script>
  let { name, email, role } = $props()
  let expanded = $state(false)
</script>

<div class="card">
  <h2>{name}</h2>
  <p>{email}</p>
  {#if expanded}
    <p class="role">{role}</p>
  {/if}
  <button onclick={() => expanded = !expanded}>
    {expanded ? '접기' : '펼치기'}
  </button>
</div>

<style>
  .card { padding: 1rem; border-radius: 8px; }
  .role { color: gray; }
</style>

JSX vs 템플릿. React은 JavaScript 안에 HTML을 쓴다(JSX). className, htmlFor 같은 React 전용 속성명을 써야 하고, 조건부 렌더링은 삼항 연산자나 &&로 처리한다. Svelte는 HTML 안에 JavaScript를 쓴다. class, for를 그대로 쓰고, {#if}/{#each} 같은 블록 문법으로 분기한다.

스타일 격리. Svelte는 <style> 블록에 쓴 CSS가 해당 컴포넌트에만 자동으로 적용된다. 별도 라이브러리나 설정이 필요 없다. React은 CSS가 다른 컴포넌트에 영향을 주지 않도록 하는 방법을 직접 골라야 한다.

Props. React은 함수 매개변수로 받고, Svelte 5는 $props() rune으로 구조분해한다. 큰 차이는 아니다.

보일러플레이트

리스트 렌더링에서 차이가 좀 더 드러난다.

// React
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <UserCard {...user} />
        </li>
      ))}
    </ul>
  )
}
<!-- Svelte 5 -->
<script>
  let { users } = $props()
</script>

<ul>
  {#each users as user (user.id)}
    <li><UserCard {...user} /></li>
  {/each}
</ul>

React은 mapkey가 필수인데, key를 빠뜨리면 경고가 뜨고 성능에도 영향을 준다. Svelte는 {#each} 블록 괄호 안에 키를 넣으면 끝이라 빼먹을 일이 적다.

대체로 같은 기능을 만들면 Svelte가 20~30% 정도 짧다. 다만 “짧다 = 좋다”는 아니고, React의 명시적인 패턴이 팀에서는 오히려 읽기 편할 수 있다.

학습 곡선

React이 더 어려운 이유가 있다.

  • JSX 문법이 HTML과 미묘하게 다르다 (className, self-closing 태그 등)
  • 훅에는 지켜야 할 규칙이 있다. 조건문이나 반복문 안에서 훅을 호출하면 안 된다.
  • 객체나 배열을 직접 수정하면 화면이 안 바뀐다. 항상 새 객체를 만들어서 넣어줘야 한다.
  • 외부 라이브러리 선택 부담이 크다. 라우터, 상태관리, 폼 처리 등 직접 골라야 한다.

Svelte는 일반 HTML/CSS/JS에 가까워서, 웹 기초가 있으면 금방 익숙해진다. $state, $derived 같은 rune 문법만 익히면 대부분의 패턴을 다룰 수 있다. 애니메이션, 트랜지션, 스토어가 기본 포함이라 “뭘 깔아야 하지?” 고민이 적다.

번들 사이즈 & 런타임 성능

접근 방식의 차이

React은 브라우저에서 프레임워크 코드가 함께 돌아간다. 화면 비교 엔진이 상시 동작하면서 UI를 관리한다. 이 코드만 약 42KB. 앱 코드와 별개로 반드시 다운로드해야 한다.

Svelte는 빌드할 때 컴포넌트를 일반 JavaScript로 변환한다. 브라우저에서 별도 프레임워크가 돌아갈 필요가 없다. 전달되는 건 화면을 직접 조작하는 코드뿐이다.

번들 사이즈

구성React 19Svelte 5
코어 런타임~42 KB~2 KB (내장)
라우터+10 KB (React Router)+8 KB (SvelteKit 내장)
상태관리+2 KB (Zustand 등)내장
총합~62 KB~28 KB
실제 앱200~300 KB50~100 KB

Svelte 앱이 받아야 하는 코드량은 React의 1/3~1/5 정도다. 느린 네트워크나 저사양 폰에서 체감이 확실하고, 웹 성능 지표에도 유리하다.

벤치마크 (js-framework-benchmark, 2025~2026)

지표React 19Svelte 5비고
1000행 생성28.4 ops/sec39.5 ops/sec높을수록 빠름
1000행 업데이트25.6 ops/sec35.8 ops/sec
행 스왑8.9 ops/sec11.4 ops/sec
Startup52ms32ms낮을수록 빠름
첫 상호작용 가능 시점350ms200ms
주요 콘텐츠 표시 시점1,200ms850ms

Svelte가 거의 모든 지표에서 앞선다. “이전 화면과 새 화면을 비교”하는 중간 단계가 없기 때문이다. 뭐가 바뀌었는지 이미 알고 있으니, 해당 부분만 바로 고친다.

격차는 좁혀지고 있다

React 쪽도 손 놓고 있진 않다.

  • React Compiler: React 팀이 별도 babel 플러그인으로 제공하는 자동 최적화 도구. 수동으로 성능 코드를 넣지 않아도 컴파일러가 불필요한 다시 그리기를 잡아준다. React 19에 내장된 건 아니고 따로 설정해야 한다.
  • 서버 컴포넌트 (RSC): 서버에서 미리 그려서 보내는 방식. 브라우저가 다운로드할 코드가 30~50% 줄어든다.

근데 솔직히, 대부분의 앱에서 프레임워크 성능 차이를 체감하기는 어렵다. API 호출, 데이터베이스, 이미지 로딩이 느린 경우가 훨씬 많다. 벤치마크에서 30% 빠르다는 건 사실인데, 실제 사용자가 느끼는 차이로 이어지는 경우는 드물다.

진짜 성능이 중요한 곳은 따로 있다. 수천 행짜리 테이블, 실시간 대시보드, 저사양 모바일 타겟. 이런 경우에는 Svelte가 확실히 유리하다.

생태계

생태계도 중요하다. 필요한 라이브러리가 없으면 직접 만들어야 하니까.

규모 차이

ReactSvelte
주간 npm 다운로드89.7M3.64M
GitHub Stars243K86K
npm 패키지 수50,000+수천 개

React 다운로드 수가 Svelte의 약 25배다. 웬만한 문제는 이미 누군가 라이브러리를 만들어놔서, React에서는 검색하면 답이 나온다.

메타 프레임워크: Next.js vs SvelteKit

풀스택으로 확장하면 이렇게 된다.

기능Next.jsSvelteKit
서버 렌더링(SSR)App Router + RSC지원
정적 생성(SSG)지원지원
페이지별 자동 갱신지원부분 지원
파일 기반 라우팅지원지원
API Routes지원지원
이미지 최적화내장직접 설정
배포Vercel 최적화어댑터 기반 (범용)
번들 크기200~300 KB50~100 KB

Next.js는 Vercel 플랫폼과의 통합이 강점이다. 배포, 미리보기, 서버 실행이 매끄럽게 연결된다. 서버 컴포넌트로 브라우저에 보내는 코드도 줄일 수 있다. 단, Vercel이 아닌 환경에서는 일부 기능이 제한된다.

SvelteKit은 어댑터 시스템 덕분에 Cloudflare, Vercel, Node 등 어디에나 배포할 수 있다. 특정 플랫폼에 종속되지 않는다. 번들 크기도 작다. 다만 이미지 최적화나 페이지 자동 갱신 같은 기능은 직접 설정해야 한다.

UI 컴포넌트 라이브러리

React: shadcn/ui, Radix UI, MUI, Ant Design, Chakra UI, Headless UI, Mantine, React Aria. 선택지가 넘친다.

Svelte: shadcn-svelte(shadcn/ui 포팅), Skeleton UI, Flowbite-Svelte. 빠르게 늘고 있긴 한데 React에 비하면 아직 적다. Tailwind 기반의 DaisyUI처럼 프레임워크에 구애받지 않는 라이브러리를 쓰는 것도 방법이다.

상태관리

ReactSvelte
내장useState, useReducer, Context$state, stores
서드파티Zustand, Redux Toolkit, Jotai대부분 내장으로 충분
서버 상태TanStack Query, SWRTanStack Query (Svelte 버전)

Svelte는 상태관리 라이브러리를 따로 고를 필요가 거의 없다. $state와 내장 store로 대부분의 패턴을 커버할 수 있다. React은 Context API만으로는 부족한 경우가 많아서 Zustand 같은 서드파티에 의존하게 된다. React은 “원하는 도구를 고를 자유”가 있고, Svelte는 “고를 필요가 없는 편리함”이 있다.

어떤 상황에서 어느 쪽이 맞는가

상황추천이유
대규모 팀, 엔터프라이즈React검증된 패턴, 풍부한 생태계, 레퍼런스
복잡한 서드파티 통합React대부분의 서비스가 React SDK를 먼저 제공
성능 민감한 마케팅/랜딩Svelte작은 번들, 빠른 반응 속도, 웹 성능 지표 유리
소규모 팀, 1인 개발Svelte보일러플레이트 적고, 빌트인 기능 풍부
다른 사이트에 삽입하는 위젯Svelte런타임 없는 경량 컴포넌트
새 프로젝트, 개발 편의성 우선Svelte간결한 코드, 빠른 프로토타이핑
Next.js RSC가 필요한 경우ReactSvelteKit에 아직 동등한 기능 없음

개인적으로 Svelte를 한 번이라도 써보면 $state$derived의 편리함을 잊기 어렵다. React에서 의존성 배열이랑 씨름하다가 오면 “원래 이렇게 간단한 거였어?” 싶다. 반대로 React의 명시적인 흐름과 거대한 생태계에 익숙한 사람한테는 Svelte가 오히려 불안할 수도 있고. 결국 프로젝트 상황에 맞는 걸 고르면 되는 거다.

참고 자료

관련 글