최근 수정 시각 : 2024-11-10 23:28:49

Svelte

스벨트에서 넘어옴
웹 프레임워크 기술
{{{#!wiki style="margin:0 -10px"
{{{#!folding [ 펼치기 / 접기 ]
{{{#!wiki style="margin:-5px 0px -10px; word-break:keep-all"
$ 유료 포함 • 취소선 단종 및 중단
<colbgcolor=#f6f6f9,#2f3241> 프론트엔드 <colbgcolor=#fcfcfd,#272935> CSS Bootstrap Tailwind CSS Bulma Foundation Skeleton Pico
JSX React SolidJS Astro Preact Gatsby Remix Inferno Qwik
JS Angular Svelte Backbone.js jQuery Astro htmx Ember.js Lit 11ty Marko VanJS Alpine.js
Vue Vue.js VuePress Gridsome Quasar Astro
Python Reflex
백엔드 Java Spring Struts GWT Grails Jooby Play! Framework Scala
Kotlin Ktor
JS Express NestJS koa Hono fastify
.NET ASP.NET$
PHP Laravel Codeigniter Reasonable phalcon Symfony zend CakePHP FuelPHP Yii Slim PHPixe
Python Django Flask FastAPI
Ruby Ruby on Rails Sinatra
Go Gin echo Fiber
풀스택 JSX Next.js Astro SolidStart Remix Qwik City
JS SvelteKit Fresh Astro Marko
Vue Nuxt.js Astro
Java Vaadin$
Python Streamlit Reflex
Rust Rocket Actix Leptos Axum
하이브리드 .NET Blazor
Dart Flutter
Kotlin Kotlin Multiplatform
}}}}}}}}} ||


<colbgcolor=#ffffff,#1f2023><colcolor=#4A4A55,#ddd> 스벨트
Svelte
파일:Svelte 로고.svg
제작자 리치 해리스(Rich Harris)
분류 웹 프레임워크
출시 2016년 11월 26일
언어 JavaScript
버전 5.1.13
라이선스 MIT 라이선스
파일:홈페이지 아이콘.svg | 파일:GitHub 아이콘.svg 파일:GitHub 아이콘 화이트.svg | | 파일:디스코드 아이콘.svg
1. 개요2. 특징
2.1. 4.02.2. 5.0
3. 룬(Rune)
3.1. 최초 발표(5.0 이전)3.2. 정식 발표(5.0 이후)
4. 예시5. SvelteKit6. Svelte를 사용하는 웹 사이트/프로그램7. 학습
7.1. 유튜브7.2. 웹페이지
7.2.1. 공식 튜토리얼7.2.2. 블로그7.2.3. 기타 웹페이지
7.3. 도서
8. 기타9. 관련 문서

[clearfix]

1. 개요


Svelte는 2016년 출시한 오픈 소스 프론트엔드 웹 프레임워크이다. 기존의 React Vue.js 등의 널리 알려진 웹 프레임워크와 달리, 가상 DOM을 사용하지 않으며 빌드 단계에서 구성 요소를 컴파일하여 성능이 향상되었다.

2. 특징

  • 실제 DOM을 사용, 컴파일러
    가상 DOM을 사용하는 대표적인 웹 프레임워크인 React는 각 구성 요소를 만들면 가상 DOM 객체가 만들어지고, 실제 DOM과 가상 DOM을 비교하여 변경 사항을 실제 DOM으로 업데이트하는 방식으로 작동한다.

    반면, Svelte는 이 과정을 생략하고 바로 실제 DOM을 반영한다. Svelte는 앱을 실행 시점(Run time)에서 해석하지 않고 빌드 시점(Build time)에서 Vanilla JavaScript Bundle로 컴파일하여 속도가 빠르고 따로 라이브러리를 배포할 필요가 없어 간편하다.

    document.querySelector 등의 함수를 사용하여 실제 DOM 접근 및 제어가 더욱 간편한 장점이 있다. 가상 DOM을 사용하는 리액트, 뷰에서는 성능 저하 등의 이유로 실제 DOM을 제어하는 걸 되도록 피하기 위해서 Vue에서는 ref를 사용하고, React에서는 refs, useRef 등의 편법을 사용해야 한다.
  • 비교적 간소화된 소스코드, 상대적으로 작은 용량
    React나 Vue.js와 비교했을 때 훨씬 적은 양의 코드로 동일한 결과물을 만들어낼 수 있으며, 가독성이 좋은 점도 장점이다.
    웹사이트가 거대해질수록 그로 인한 소스코드의 비대화는 수반될 수 밖에 없는데, 상대적으로 간소화된 소스 코드를 가지고 있음으로써 유지보수와 기능 추가 개발에서 효율성이 높아지는 것이다.
  • use: 특성 등을 통한 기존 라이브러리 친화력
    React, Vue의 경우, 기존 JS 라이브러리를 해당 환경에 맞게 컴포넌트를 재구성하여 만들어야 하는 대신, 생태계가 많이 발달하여 어렵지 않게 각 환경의 대체제를 구할 수 있으나, Svelte의 경우, 좁은 생태계에도 불구하고 사용하려고 하는 이유 중 가장 큰 비중을 차지하는 부분이 바로 기존 JS 라이브러리와의 친화력이다. SolidJS도 이 use: 특성을 사용하여 좁은 생태계를 타파하는 방법을 사용하고 있으며, use: 특성을 사용하는 함수에 초기화부터 정리(cleanup)까지 모두 구현할 수 있고, 액션 함수만 다르다면 복수 구현이 가능하여 특정 태그를 기준으로 여러 라이브러리와 생명주기까지 한 번에 해결할 수 있다. 이는 실제 DOM을 주력으로 사용함으로써 얻을 수 있는 이점이다.

2.1. 4.0

한국 시간으로 2023년 6월 23일, 4.0이 출시되었다. 4.0 은 3.x 의 호환성을 유지하면서, 5.0의 디딤돌 역할을 한다고 밝혔다. 3.0 버전이 출시한지 4년 만에 새 메이저 버전으로 많은 우여곡절 끝에, 메인 개발진들의 안착 이후 적극적 활동이 빛을 발하는 셈.
주요 변경 사항은 아래와 같다.
  • 3.x 대비 최대 12% 빠른 성능과 최대 75% 가벼운 메인 패키지를 포함한 최대 50% 가벼운 번들 크기.[1]
  • 최소 요구사항이 node.js 16, webpack 5, vite 4로 상향.
  • 향상된 개발자 경험을 위한 주요 IDE 지원 확대

스벨트 3 프로젝트나 라이브러리 등은 최소 요구사항만 충족하면 대부분 npx svelte-migrate@latest svelte-4 명령어를 통해 바로 4.0으로 간편하게 마이그레이션할 수 있다.
4.0 버전을 발표하면서 5.0에 대한 언급을 추가적으로 한 내용에 의하면, 핵심 컴파일러와 런타임을 재구성하여 더 빠르고 가벼운 기술이 되도록 할 예정이라 밝혔다.

2.2. 5.0

한국시각으로 10월 20일, 갑작스레 5.0 을 발표하였다.
주요 변경 사항은 아래와 같다.
  • 더 좋아진 성능
  • runes 기술을 통한 세분화된 반응성 시스템
  • 스니핏과 이벤트 특성을 통해 더 풍부해진 템플릿 표현력
  • 네이티브 TypeScript 지원
  • 이전 버전 구문과의 호환성

Svelte 4 프로젝트일 경우 npx sv migrate svelte-5 명령어로 자동 마이그레이션이 지원된다. 마이그레이션 가이드
그리고 후술할 룬(rune) 이 정식 차용되어, 명시적인 반응성을 통해 개발자의 혼란을 줄였다는 점이 특징이다.

이외에 또다른 특징이 있는데, .svelte.js.svelte.ts 확장자를 지정하면 스벨트 컴파일러가 이 파일을 인지하여 특히 반응성이 요구되는 로직을 작성할 때 도움이 될 수 있도록 기능이 추가됐다. 특히 후술할 룬 기능을 사용할 때 유용할 것이다.

3. 룬(Rune)

5.0 부터 사용 가능한 신규 문법으로, 접근성 향상과 타입스크립트에 더 유리한 대응이 가능한 신규 문법이다.

4.0 과의 차이점은 변화 추적의 주체와 대상이 코드에서 변수로 옮겨졌다. 기존 Svelte 유저들 사이에서 간결함은 여전하지만, 다소 React스러워졌다는 평가가 많다. 예를 들어 Svelte 4에선 $: expression 형태의 반응형 구문에 코드를 짜 넣으면 알아서 추적해야 할 변화를 찾아 잘 해주던게 Svelte 5에선 React에서 useState 쓰듯 추적해야 할 변수를 일일히 룬으로 선언해줘야 하는 까탈스럽게 구는 언어가 되었다는 불평이 있다. 다만 해당 변화는 신규 유저들과 성능 및 유지보수라는 관점에선 큰 이점이 되었는데, "알아서 잘"은 기술적으로는 뛰어날지 몰라도 최적화 면이나, 심지어 사용자 입장에서도 별로 좋은 옵션은 아니기 때문이다. 변수 타입을 제공하지 않아도 알아서 어떻게든 형변환을 해서든 동작하려고 하는 Javascript에 왜 runtime시점에는 의미도 없는 typing을 제공하는 Typescript가 인기를 끌고, 더 나아가 Javascript에도 typing 도입이 예정되었는지 생각해보면 된다. 정말로 알아서 "모든 걸" 잘 하는 물건은 대개 달성하기 매우 어려운 문제인데, Svelte 4의 경우도 프로그래머 입장에서 직관적으로 봤을 때 기대한 동작을 진짜 알아서 잘 추적하여 구현하지 못 하는 경우가 있었기 때문에 모호함이 꽤 많았다. 이는 아래에 후술.

3.1. 최초 발표(5.0 이전)


5.0 에 신규 문법으로 사용할 예정인 룬(Rune)을 발표하였다.
공식 블로그에 정리된 목록에 의하면, Rune 기능은 접근성 향상을 위해 아래 기존 문법을 대체할 목적에 있다.
  • 최상위 컴포넌트와 하위 컴포넌트의 let 문법 차이
  • export let 문법
  • $: 구문 등의 구형 문법
  • <script> 태그와 <script context="module"> 태그 사용처
  • $$props$$restProps 속성
  • 생명주기 함수 (예를 들면, afterUpdate 함수를 $effect 매크로에 통합)
  • 저장 API와 반응형을 위한 $ 접두어 처리 (더 이상 불필요하지만 제거하진 않을 예정)

Try it 사이트 에서 <svelte:options runes /> 옵션 태그를 추가하는 것으로 시험해 볼 수 있다.

3.2. 정식 발표(5.0 이후)

Svelte 에서 사용하는 $ 기호가 이전에는 반응성 추적에 그쳤는데, 5.0.0 부터는 룬을 상징하는 기호로 변했다.
$ 접두어는 스벨트 컴파일러의 반응형 매크로가 되어, 별도 import 없이 사용 가능하다.
마치 Vue.js<script setup> 구문과 유사하다.

Svelte 4 에서 사용하던 반응형 선언문은,
#!syntax html
<script>
  let list = [];
  let count = 0;
  $: doubled = count * 2;
</script>


<button on:click|preventDefault|stopPropagation={() => {
  count++;
  list = [...list, count];
}}>
	{doubled}
</button>

<ol>
  {#each list as item}
    <li>{item}</li>
  {/each}
</ol>
<p>{count} doubled is {doubled}</p>

이제 Svelte 5 에서 이렇게 사용하면 된다.
#!syntax html
<script>
  let list = $state([]);
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>


<button onclick={(e) => {
  e.preventDefault();
  e.stopPropagation();
  count++;
  list.push(count);
}>
	{doubled}
</button>

<ol>
  {#each list as item}
    <li>{item}</li>
  {/each}
</ol>
<p>{count} doubled is {doubled}</p>


위의 예제로 볼 수 있는 변경사항은
1. $:을 대체하는 기호로 룬($state, $derived, $effect)이 추가 되었다.
2. $state는 내부 프로퍼티의 깊은 변화도 추적 가능하다(단, pure object, array에 한함).
  • 다만 Svelte4도 최신 업데이트에서 어느정도 가능해졌다(object의 경우 pure object에 한해, array는 기존에 선언된 원소에 한해, 혹은 index를 사용한 직접 원소 할당을 통해).
3. event 지시어 on이 사라짐. 모든 event는 props 취급. Component event역시 마찬가지로 prop이 되었기 때문에 disptacher(createEventDispatcher)가 사라졌다. bubbling을 원할 경우 부모에게서 event handler를 prop으로 받아 구현해야 한다.
4. event modifier(preventDefault, stopPropagation, once) 역시 사라졌다. 직접 처리해야 한다. once 구현은 Svelte에서 제공하는 예제를 참고하자.

이 때, count 변수는 예나 지금이나 동일한 반응형 변수가 되기 때문에, 이전 버전과 동일하게 값을 재할당하는 식으로 값 변경과 이에 대한 반응성 두마리 토끼를 챙길 수 있다. 또한 이전에는 반응형 변수를 할당식을 포함하여 $:로 선언하거나, $: [...dependencies], {code} 형태로 dependencies 목록을 만들어 반응성을 부여해야 했던 것을 반응성이 부여되어야 하는 변수 단위로 ($state, $derived)라는 룬으로 특별취급하게 됨으로 프로그래머 입장에선 코드가 좀더 명료해졌고[2], 변수 단위 추적을 빼먹는 실수를 걱정하지 않게 되었으며(룬은 dependency 선언 같은 꼼수 없이 자동 추적된다), 컴파일러 입장에선 어떤 코드가 반응성을 지니는지 좀더 명료해짐으로 컴파일이 빨라지고(예전에는 구문분석으로 일일히 추적했다), 출력된 결과물도 예상치 못한 오류가 발생할 확률이 줄어들고 코드 역시 훨씬 가벼워졌다(Svelte engine이 Runtime 시점에 반응성을 매번 검사해야 했던 예전과 달리, Javascript의 기본 스팩인 Proxy object의 도움을 받는 형태로 구현됨으로 오버헤드가 최소화 되고 기능이 되려 강력해졌다: 룬은 스코프 밖에서, 심지어 Svelte framework 밖에서(!!!) 변화를 주어도 추적된다!).

위의 룬이 얼마나 강력해졌는지를 설명하기 위해 특이한 예제를 하나 더 보자.
#!syntax html
<script>
  let list = $state([]);
  $effect(() => {
    window.__list = list;
  });
</script>

<ul>
  {#each list as item}
    <li>{item}</li>
  {/each}
</ul>
<button onclick={() => (list = [])}>Reset</button>

다음과 같은 컴포넌트 코드를 작성한 다음 해당 컴포넌트를 브라우저에 띄운다. 디버그 콘솔을 열어서 __list.push(10)을 먹이면 반영된다. 즉, Svelte framework 밖에서도 반응성을 유지하는 것이다! 또한 Svelte Component의 Reset 버튼을 누를 경우 $effect를 통해 framework 밖에 새로 할당된 배열을 다시 노출한다. 다만 주의해야 할 점이 있는데, 이전의 window.__list와 Reset 버튼을 누른 이후의 window.__list는 서로 다른 object이다. 또한, 외부에 노출된 변수의 변화가 추적되는 것은 property 단위이다. window.__list = [10] 이런식으로 변수 자체를 재할당 해버리면 추적이 끊긴다. 이는 Svelte가 변화를 추적하는 방식이 Proxy object wrapping을 통해 추적하는 것이기 때문이다. 같은 블록 내에서야 블록 내 모든 반응형 변수를 하나의 Proxy object에 담아 추적하기 때문에 변수 재할당도 추적 되지만, 블록 밖에서 재할당 할 경우 이런 추적이 끊기게 된다(비유하자면 집 안에서야 고용인들의 변화를 추적하는게 가능하지만 외부에서의 변수 재할당은 집 밖에 무전기 들고 나간 파견나간 요원이 파견지에서 쫏겨난거니 파견지의 이후의 일은 본부에서 알 수 없는거다.). 이는 Svelte Component끼리도 마찬가지로, <MyComponent {list} />와 같은 형태로 전달하여 사용할 경우 MyComponent에서 list = [10] 같은 형태로 재할당 하는 경우 추적이 끊긴다(위의 경우는 $bindable()과 bind:list로 해결할 수 있다).

특별취급받던 event가 일반 prop이 됨으로 장점과 단점이 공존하게 되었는데, 장점은 좀더 명확한 처리가 가능해졌다는 것이고, 단점은 기존 browser의 event handling 시스템과 괴리를 가지가 되었다는 점이다. 다만 dom event handling을 사용을 못 한다는 이야기는 아니고, Svelte4에선 CustomEvent를 구현하여 DOM event handling을 사용해 처리하던 작업을 기본적으로 component prop으로 처리하게 유도한 것에 불과하다. 다만 DOM event handling을 아예 사용 못 하게 된건 아닌데, 이제 더이상 언어적으로 제공되는게 아니므로 CustomEvent를 작성해 수동으로 dom reference를 잡아 dispatch하는 등의 귀찮음이 좀 생겼지만, browser의 event handling을 비롯한 DOM 기반 시스템은 여전히 사용 가능하다(사실 이게 Svelte를 비롯한 실제 DOM을 사용하는 Framework의 가장 큰 장점이다).

이러한 변화를 통해, Svelte 에 입문하는 개발자에게 '이것이 반응형 구문이구나' 라는 인지를 확실하게 줄 수 있으며, 어떤 변수가 반응성을 지니는지, 어떤 코드가 어떤 변수의 변화에 반응하는지 쉽게 알 수 있게 되었다. 또한 기존 $ 구문과 Typescript 상의 타입 정의에 대한 모호함을 해소한 효과가 생겼으며, $state, $derived, $effect 등 용도에 따라 명확한 차이가 있는 룬을 제공함으로 코드가 좀더 명료해졌다(이전에는 $: 뒤에 반응형 코드를 적어놓는 방식이라 프로그래머의 코딩스타일에 따라 어느 변수가 반응형인지, 어느 구문이 반응성 코드인지 알기 힘든 중구난방의 엉망진창인 코드가 양산되기 쉬웠다). 요약하자면 Svelte 4에서는 반응형 코드를 통해 반응성을 제공했다면, Svelte 5에선 반응형 변수를 제공하는 방식으로 반응성 프로그래밍 페러다임을 변경한 것이다.

자세한 문서는 여기를 참고하기 바란다.

4. 예시

예를 들어, 두 개의 input 요소에서 숫자를 입력받고 그 숫자들을 더한 값을 출력해주도록 구현을 한다고 가정하자.

React에서는 다음과 같이 구축할 수 있다.
#!syntax javascript
import React, { useState } from 'react';

export default () => {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  return (
    <div>
      <input type="number" value={a} onChange={(e)=>setA(+e.target.value)}/>
      <input type="number" value={b} onChange={(e)=>setB(+e.target.value)}/>

      <p>{a} + {b} = {a + b}</p>
    </div>
  );
};

Vue.js(버전 2 기준)에서는 아래와 같다.
#!syntax xml
<template>
  <div>
    <input type="number" v-model.number="a">
    <input type="number" v-model.number="b">

    <p>{{a}} + {{b}} = {{a + b}}</p>
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        a: 1,
        b: 2
      };
    }
  };
</script>

이를 Svelte 4.0에서 똑같이 구현을 하면 아래와 같다.
#!syntax xml
<script>
  let [a, b] = [1, 2];
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>

React는 442자, Vue.js는 263자의 코드를 작성해야 하지만, Svelte는 145자만 사용하여 동일한 결과물을 만들 수 있다. 이와 같이 Svelte는 훨씬 적은 양의 코드로 간결하게 표현하여 구축할 수 있는 장점 덕분에 작업에 있어 가독성을 향상시키고 시간도 절약할 수 있다.

해당 소스를 살펴보자면,
  1. React에서는 input 태그에서의 양방향 바인딩을 지원하지 않아, input에 onchange 이벤트를 일일이 걸어 변수값을 변경해주도록 만들어줘야 하며, 또한 변수의 초기값 외에 setter 함수로 쓰일 변수와 useState라는 특유의 선언함수를 이용해야 한다(React Hooks). 그나마 과거 class 기반의 방식보다는 간단해진 버전이다. class 기반에서는 react.component 상속 및 생성자에서의 변수 설정이 추가적으로 필요했다. 또한 일반적인 html 렌더링 방식이 아닌 JSX(JavaScript eXtension)라는 특수한 방식으로 결과를 반환해주고 있다. (svelte에서는 @html과 비슷하다.[3])
  2. vue에서는 v-model이라는 양방향 바인딩 지원으로 onchange를 선언할 필요는 없지만, 여전히 template 태그를 별도로 선언해야 하며, 일반적인 자바스크립트 방식의 방식과 다르게 객체 선언 등으로 데이터를 초기화해주고 있다.
    참고로 반응형 문법을 쓰는 방식으로도 구현할 수 있다.[4]
    svelte에서는 $:로 끝나지만, vue.js에서는 'computed, watch'로 별도로 선언해야 하며, react에서는 해당 기능이 존재하지 않으며, 해당 효과를 흉내내기 위하여 일반적으로 useeffect(이전의 componentDidUpdate)등의 생명주기 hook를 이용해야 한다.

Svelte 5는 다음과 같은 형태도 가능하다.
#!syntax xml
<script>
	let v = $state([1, 2]);
</script>

<input type="number" bind:value={v[0]} />
<input type="number" bind:value={v[1]} />
<p>{v[0]} + {v[1]} = {v[0] + v[1]}</p>

Array 내부 원소를 index 기반으로 바인딩하고, 그 변화를 추적 가능한 모습에 주목하자. 이는 React, Vue에선 제공하지 않는 기능이다. 예를 들어 React에서 위의 Svelte와 같이 const [v, setV] = useState([1, 2])과 같은 형태로 state를 선언한 경우 state 변화가 반영되게 하려면 onChange에서 setV([+e.currentTarget.value, v[1]])와 같은 형태로 state 선언 단위로 새로운 변수로 재할당 해야 변화를 인지한다.

5. SvelteKit

Svelte에 기반하여 만들어진 풀스택 웹 프레임워크이다.

6. Svelte를 사용하는 웹 사이트/프로그램

7. 학습

7.1. 유튜브

7.2. 웹페이지

7.2.1. 공식 튜토리얼

7.2.2. 블로그

7.2.3. 기타 웹페이지

7.3. 도서

8. 기타

제작자 리치 해리스(Rich Harris)가 최근 Next.js 프레임워크로 유명한 독일 기업 Vercel에 합류하면서, Svelte 개발에 더 많은 시간을 할애할 수 있다고 밝혔다. # 기여자 위주의 제한적인 개발과 얼어붙은 커뮤니티에 활기를 불어넣어 줄 것으로 기대하고 있다.

9. 관련 문서



[1] Rich Harris가 기존 Typescript로 개발하던 체계를 모두 Javascript로 바꾼 덕분이라고 Hacker News 에 언급했다. 물론 타입스크립트 사용을 고려하여 타입을 적용하도록 대응했기 때문에 타입스크립트 사용에도 문제가 없다. [2] Svelte 4에서는 어느 변수가 반응성을 지니는지 알기 힘들었다. 좀더 정확히는 let으로 선언한 것과 $:로 선언한 변수는 사실 차이가 없었다. 엄밀히 말하자면, $: 뒤에 작성된 코드 자체가 변화의 주체이고 추적의 주체이기 때문에 $:로 변수만 선언해봤자 이는 사실 실재하지 않는 코드가 된다. 예를 들어 $: varname = code expression에서 반응성을 제공하는 것은 varname이라는 변수가 아니라 varname = code expression 전체이다. $: varname 과 같은 형태는 의미를 가질 수 없다는 이야기. 만약 표현식이 특정 변수를 사용하지는 않지만, 그 변수의 변화를 추적하여 실행되는 코드를 만들고 싶을 경우 $: [...dependencies], {code} 와 같은 형태로 꼼수를 부려야 했다(dependencies에 선언된 변수들이 변하면 새로운 배열을 만드는 작업을 해야 하므로, 여하튼 그 dependency가 변하면 뒤의 code를 포함한 해당 반응형 구문 전체가 실행되는 원리이다). Svelte 4의 반응형 구문이 얼마나 모호했는지는 다음 글을 참고해보자(* 주의: 최신 Svelte 4에서 반응성의 상당한 개선이 있었음으로 해당 글과 달리 직관적으로 잘 동작하는 부분이 많다. 가능 불가능의 부분이 아닌 Svelte 4 문법의 모호함에 주목하자). Svelte 5에선 추적의 대상이 룬으로 선언된 변수가 됨으로 위와 같은 모호함이 거의 대부분 자연스럽게 해결된다. [3] https://svelte.dev/tutorial/html-tags [4] https://svelte.dev/tutorial/reactive-declarations