처음 만나는 Svelte


원문: https://daveceddia.com/svelte-intro/

image.png

몇 달 전, Svelte 3가 릴리즈되었다.

Svelte를 사용해 몇 가지를 시험해 봤고, 튜토리얼을 참고해 몇 가지 작은 앱들을 제작했다. 솔직히 말하면, Svelte를 사용하는것이 React 보다 훨씬 빠르고 쉽게 느껴졌다. 나는 처음 React에서 받았던 놀라움을 Svelte에게서 똑같이 느꼈다.

나는 이 글에서 Svelte에 대한 간략한 소개와, 몇 가지 예제를 통해 이것이 어떻게 동작하는지, 어떻게 시작하는지를 이야기하려 한다.

Svelte는 무엇인가?

현재 세 번째 개정판인 Svelte는(일부는 SvelteJS라 부르지만, 공식적으로는 "Svelte"라고 한다.) React, Vue.js, Angular와 같은 종류의 프론트엔드 프레임워크다. 웹페이지에서 픽셀을 그리는데 도움을 준다는 점에서 비슷하지만, 다른 면에서는 매우 다르다.

Svelte는 빠르다.

처음 알게 된 것 중 하나는 Svelte의 속도가 매우 빠르다는 것이었다.

Svelte는 DOM의 변화가 있을 때 그 부분만 업데이트하므로 실행 속도가 매우 빠르다. React, Vue.js 같이 가상 DOM을 사용하는 프레임워크와 대조적으로 Svelte는 가상 DOM을 사용하지 않는다.

가상 DOM 프레임워크는 실제 DOM에 변경을 커밋하기 전 보이지 않는 트리에서 컴포넌트를 그리기 위한 시간을 소비하는 반면, Svelte는 이런 중간 단계를 뛰어넘고 바로 변경한다. DOM 업데이트가 느릴수는 있지만, Svelte는 어떤 요소에 변화가 일어났는지 정확하게 알고 있기 때문에 빠르게 처리할 수 있다.

또한, Svelte는 개발 속도를 매우 빠르게 할 수 있다. 일반적으로 같은 내용의 컴포넌트를 만들 때 Svelte 컴포넌트는 React보다 적은 코드로 생성할 수 있다. 잠시 후 좀 더 많은 예제를 살펴 보기로 하고, 우선 "Hello world"로 비교해보자. 아래는 Svelte 코드이다.

<script>
  let name = "World";
</script>

<h1>Hello {name}!</h1>

이게 전부다. 이건 Hello 컴포넌트다. name 변수는 일반적인 script 태그 내에 작성한다. 그러면 변수는 아래 HTML 내부에서 사용할 수 있게 된다. 전체적으로 HTML 파일과 매우 비슷한 모습이다.

비교를 위해 ReactHello 컴포넌트를 만들어보자.

import React from "react";

const Hello = () => {
  let name = "World";
  return <h1>Hello {name}!</h1>;
};

export default Hello;

React로 작성된 코드 또한 짧지만, 이 코드를 이해하기 위해 몇 가지 특별한 구문들을 알아야 한다.

Svelte는 작다.

Svelte의 컴파일된 번들 크기는 다른 인기 있는 프레임워크들과 비교해보면 매우 작은 크기를 갖는다.

image
👆 <그림1> 실제 Svelte 앱이다. (역주: 현재는 이미지이지만, 원문에서는 글 내에 실제 앱이 있는 것을 볼 수 있다.)

"Hello World!" 앱의 bundle.js 파일의 크기는 2.3KB이다. 이 번들 파일은 하나의 JS 파일로 Svelte를 모두 포함하고 있다!

이 결과는 React 호환 라이브러리인 작고 멋진 Preact보다 작은 결과이다. Preact는 라이브러리 자체를 위해 용량이 3kb로 시작하게 된다. 그리고 위 예제를 Create React App을 통해 빌드 했을 때 124KB의 자바스크립트 파일이 결과로 나오게 된다.

그래, 그래. gzip을 한 결과가 아니다. gzip을 적용해보면..

$ gzip -c hello-svelte/public/bundle.js | wc -c
    1190
$ gzip -c hello-react/build/static/js/*.js | wc -c
    38496

각 파일은 1.16KB vs 37.6KB 로 작동하게 된다. unzip이 된 이후에도, 브라우저는 여전히 2.3KB vs 124KB를 파싱해야 한다. 작은 번들은 모바일 환경에서 큰 이점이다.

다른 좋은 점은, node_modules 의 폴더 개수는 Hello World Svelte 앱을 띄울 때 단지 29MB, 242개의 패키지뿐이었다. Create React App 프로젝트는 204MB, 1017개의 패키지가 포함되어 있었다.

그래 어쨌든 Dave. 그런 숫자들은 의미가 없어. 그건 작위적인 예시야.

맞다. 물론, 현재 제공되고 있는 큰 규모의 앱들은 1KB든 38KB든 앱을 작동시키는 프레임워크의 크기를 왜소하게 만들 것이다. 하지만 그것이 기본이며, 개인적으로 작고 빠른 환경에서 시작하는 것이 더 좋다고 생각한다.

그리고 심지어 규모가 더 큰 앱의 경우일지라도, Svelte가 더 큰 장점을 누릴 수 있을 것으로 생각한다. 그 이유를 살펴보자.

Svelte는 컴파일된다.

번들 크기가 이렇게 작을 수 있는 이유는 Svelte는 프레임워크이자, 컴파일러이기 때문이다.

아마 당신은 React Project를 컴파일하기 위해 yarn build를 실행하는데 익숙할 것이다. Webpack과 Babel을 호출하여 프로젝트 파일을 번들링 한 뒤, 최소화(minify) 하고, reactreact-dom 라이브러리를 번들에 추가한 뒤, 그 파일을 최소화(minify)하고, 하나의 출력 파일을(혹은 몇 개의 chunk로 분리된 파일을) 생성한다.

반면 Svelte는, 자체적으로 컴포넌트를 컴파일할 수 있다. 결과는 (앱) + (Svelte 런타임 환경)이 아닌, (Svelte가 독자적으로 실행하는 방법을 알려준 앱) 이다. Svelte는 Rollup(혹은 Webpack)의 트리 쉐이킹 이점을 가져와 이용하여 내 코드에서 사용하는 프레임워크의 부분만을 포함해 자체적으로 만든다.

컴파일 된 앱은 여전히 작성한 컴포넌트를 구동시키기 위해 조금의 Svelte 코드를 갖게 된다. 마법처럼 그 코드는 전부 사라지지 않는다. 하지만 이런 부분은 다른 프레임워크들이 작동하는 방법과 반대이다. 대부분의 프레임워크는 실제로 앱을 실행하고, 나타내기 위해 존재해야한다.

Svelte로 쇼핑 리스트 만들기

좋다. Svelte가 얼마나 빠르고, 작고, 멋진지 충분히 이야기했다. 그럼 이제 쇼핑 리스트를 만들어 보면서 코드가 어떻게 작성되는지 알아보자.

image
👆 <그림2> (역주: 번역 글 내에서는 이미지이지만 원문에서 실제 작동하는 앱을 확인할 수 있다.)

리스트에 물건을 추가하고, 실수로 추가한 물건을 지우고, 구매한 것을 체크하면서 확인할 수 있다.

여기, 하드코딩된 구매할 물건 목록이 있다.

<script>
  let items = [
    { id: 1, name: "Milk", done: false },
    { id: 2, name: "Bread", done: true },
    { id: 3, name: "Eggs", done: false }
  ];
</script>

<div>
  <h1>Things to Buy</h1>

  <ul>
    {#each items as item}
    <li>{item.name}</li>
    {/each}
  </ul>
</div>

상단에는 <script> 태그가 있고, 하단에는 HTML 마크업이 있다. 모든 Svelte 컴포넌트는 <script>, <style>, 그리고 마크업 코드를 가질 수 있다.

<script> 코드 안에는 일반적인 자바스크립트 코드가 있다. 이 부분에서 items라 불리는 배열 변수를 선언하면, 하단 마크업 부분에서 해당 변수를 사용할 수 있게 된다.

마크업 코드에선, 이 부분을 제외하고는 일반적인 HTML처럼 느껴졌을 것이다.

{#each items as item}
<li>{item.name}</li>
{/each}

이 코드는 list를 렌더링 하기 위한 Svelte의 템플릿 구문이다. #each를 사용해 items 배열의 요소(item)를 <li> 태그에 itemname과 함께 렌더링한다.

React를 알고 있다면, {item.name} 형태는 친숙하게 보일 것이다. 템플릿 내에서 자바스크립트 표현식을 사용하는 방식이며, React와 동일하게 작동한다. Svelte는 표현식을 평가한 뒤 <li>에 값을 넣을 것이다.

리스트에서 아이템 지우기

몇 가지 기능들을 추가해보자. 다음은 리스트에서 아이템을 제거하는 코드이다.

<script>
  let items = [
    { id: 1, name: "Milk", done: false },
    { id: 2, name: "Bread", done: true },
    { id: 3, name: "Eggs", done: false }
  ];

  const remove = item => {
    items = items.filter(i => i !== item);
  };
</script>

<style>
  li button {
    border: none;
    background: transparent;
    padding: 0;
    margin: 0;
  }

  .done span {
    opacity: 0.4;
  }
</style>

<div>
  <h1>Things to Buy</h1>

  <ul>
    {#each items as item}
      <li>
        <span>{item.name}</span>
        <button on:click={() => remove(item)}>❌</button>
      </li>
    {/each}
  </ul>
</div>

여기 몇 가지 코드를 추가했다.

먼저, script 태그 안에 remove 함수를 추가했다. item을 가져와 배열을 필터링하고, 결정적으로 items 변수에 재할당한다.

Svelte는 반응형이다.

변수에 재할당을 할 때, Svelte는 이 변수가 사용되고 있는 템플릿 부분을 리렌더링 할 것이다.

위 예에서 items 재할당은 Svelte가 리스트를 리렌더링 하는 원인이 된다. 만약 리스트에 item을 푸시한다면(items.push(newThing)) 리렌더링을 발생시킬 수 없었을 것이다. items = something 같은 형태가 Svelte의 재계산을 발생시킬 것이다. (또한, items[0] = thing 혹은 items.foo = 7 같이 프로퍼티에 할당하는 것 또한 발생할 것이다.)

image3.png

Svelte는 컴파일러이다. 이런 사실은 컴파일 할 때, 템플릿과 script 사이의 관계를 살필 수 있게 되고 "items와 관련된 모든 것을 리렌더링 하라" 같은 적은 양의 코드를 삽입할 수 있게 한다. 실제 컴파일된 버전의 remove 함수를 살펴보자.

const remove = item => {
  $$invalidate("items", (items = items.filter(i => i !== item)));
};

원래 작성했던 코드와의 유사한 점들과, Svelte가 업데이트를 하도록 하는$$invalidate로 어떻게 래핑되었는지를 확인할 수 있다. 컴파일 된 코드의 가독성이 매우 좋다.

'on:'으로 시작하는 이벤트 핸들러

그럼, 클릭 이벤트가 존재하는 버튼 또한 추가해보자.

<button on:click={() => remove(item)}>
  ❌
</button>

함수를 위 처럼 전달하는 방식이 React와 유사하다 느낄 수 있지만, 이벤트 핸들러 구분은 약간 다르다.

모든 Svelte의 이벤트 핸들러는 on:click, on:mousemove, on:dblclick 처럼 on:으로 시작한다. 또한, Svelte는 표준 DOM 이벤트 이름들을 모두 소문자로 하여 작성한다.

Svelte는 CSS 또한 컴파일한다.

작성된 <style> 태그를 살펴보자. <style> 태그 내부에는 표준 CSS를 작성할 수 있다.

하지만 반전이 존재한다. Svelte는 특정 컴포넌트 범위의 클래스명을 전부 고유한 이름으로 컴파일할 것이다. 즉, lidiv, li button 같은 단순 선택자를 사용하여 앱 전체에 영향을 준다는 걱정 없이 사용할 수 있다는 것을 의미한다.

  • 리스트가 존재한다.
  • Grocery 리스트 앱과 같은 페이지의 상단부에 작성된다.
  • 스타일은 충돌 나지 않는다.

CSS 이야기가 나온 김에, 몇가지를 고칠 필요가 있다.

동적으로 클래스 변경하기

위 앱에서 버그를 알아차렸는지 모르겠지만, 아이템이 "done"으로 표시되어 있지만, 목록에는 그렇게 나타나지 않았다. 그럼 "done"이 된 아이템에 클래스를 적용해보자.

여기, React와 비슷한 한가지 방법이 있다.

{#each items as item}
  <li class={item.done ? 'done' : ''}>
    <span>{item.name}</span>
    <button on:click={() => remove(item)}>❌</button>
  </li>
{/each}

Svelte는 CSS Class를 나타낼때(React의 className과 다르게) class를 사용한다. 여기서 우리는 class를 계산하기 위해 중괄호를 작성해 자바스크립트 표현식을 작성했다.

같은 동작을 하는 좀 더 나은 방식을 확인해보자.

{#each items as item}
  <li class:done={item.done}>
    <span>{item.name}</span>
    <button on:click={() => remove(item)}></button>
  </li>
{/each}

class:done={item.done} 구문은 done 클래스를 item.done이 truthy 일 때 적용하라는 의미이다.

Svelte는 사용하지 않는 CSS를 탐지한다.

Svelte가 CSS를 컴파일함으로써 얻을 수 있는 좋은 점은 일부 사용하지 않는 CSS 선택자를 찾아준다는 것이다. VSCode에서는, 선택자에 노란 줄로 해당 줄이 나타난다.

사실, 이 블로그 글을 작성하면서, 이 기능은 버그를 잡는 데 도움이 되었다. 나는 아이템이 "done"이 되었을 때 {item.name}를 흐리게 처리하고 싶었고, 이 텍스트를 span 태그로 감싸려 했다. 하지만 태그를 추가하는 것을 잊었고, 존재하지 않는 span을 타겟으로 .done span 선택자를 사용했다. 다음 스크린샷이 내가 에디터에서 본 내용이다.

a.png

Problem 탭에 다음과 같은 경고가 나타났다.

b.png

이런 문제를 발견하는 일은 컴파일러에게 맡기는 것이 좋다. 사용하지 않는 CSS 탐지는 컴파일러들이 항상 해결해 줄 수 있는 문제처럼 보였다.

완료 상태(Done) 표시하기

item에 완료 상태를 나타내는 "done"을 켜거나 끄는 기능을 추가해보자. 가장 먼저 체크박스를 추가할 것이다.

한 가지 방법은, React와 비슷하게 change 핸들러를 사용해서 값을 동기화하는 것이다.

<input
  type="checkbox"
  on:change={e => (item.done = e.target.checked)}
  checked={item.done}
/>

Svelte 방식으로는 bind를 사용하는 방식이 있다.

<input type="checkbox" bind:checked="{item.done}" />

체크박스를 선택하고 해제할 때, bind:checked는 체크박스의 item.done 값과 동기화가 유지되도록 한다. 이것은 양방향 바인딩이며 Angular나 Vue를 사용한다면 익숙할 것이다.

form, input, 그리고 preventDefault

여전히 추가되지 않은 한가지 큰 기능은 항목을 리스트에 추가하는 것이다.

우리는 input과, input을 감싼 form(Enter키로 항목을 추가 할 수 있게), 그리고 항목을 리스트에 추가하기 위한 제출(submit) 핸들러 필요하다. 코드를 작성해보자.

<script>
  // ... 기존 코드 ...
  let name = "";

  const addItem = () => {
    items = [...items, { id: Math.random(), name, done: false }];
    name = "";
  };
</script>

<form on:submit|preventDefault="{addItem}">
  <label for="name">Add an item</label>
  <input id="name" type="text" bind:value="{name}" />
</form>

addItem 함수를 이용하여 새로운 항목을 리스트에 추가할 수 있다. items.push() 대신 items를 재할당하여 이름을 재설정한다. 이 변경을 통해 Svelte는 관련된 부분의 UI를 다시 렌더링한다.

우리는 아직 on:submitbind:value를 다루지는 않았지만, 이전에 본 것과 비슷한 패턴을 따른다. on:submit은 폼에서 제출 했을 때 addItem함수를 호출하고 bind:value={name}name과 input의 값을 동기화 된 상태로 유지한다.

여기서 흥미로운 구문은 on:submit|preventDefault 이다. 이벤트 수식어(Event Modifier) 를 호출하며 addItem 함수 내에서 event.preventDefault()를 호출하지 않아도 호출 된 것 처럼 작동하게 해주는 축약형 표현이다.

<script>
  const addItem = event => {
    event.preventDefault();
    // ... 이전 코드 ...
  };
</script>

<form on:submit="{addItem}">
  <!-- 이전 코드 -->
</form>

이로써 앱을 완성했다.

image
👆 <그림3> (역주: 원문 에서 완성된 앱을 체험할 수 있다.)

더 자세히 알아보려면

Svelte에는 이 포스트에 작성하지 못했지만, 훨씬 더 멋진 것들이 있다.

  • 추가적인 컴포넌트를 만든다거나
  • 컴포넌트로 Props를 넘겨주거나
  • slots (React의 children처럼 동작하는)
  • 반응형을 이용해서 "firstName이나 lastName이 변경되었을 때 name을 재계산하기"나 "변화가 있을 때 firstName을 console에 출력하기" 같은 작업 해보기
  • {#await somePromise} 템플릿 사용하기
  • 빌트인 애니메이션과 트랜지션 이용하기
  • onMountonDestroy같은 lifecycle 메서드 이용하기
  • 컴포넌트 간 데이터를 넘겨주는 Context API
  • 전역 데이터를 관리해주는 반응형 "stores"

공식 Svelte 튜토리얼은 위에서 언급한 모든 정보, 그 이상을 다루며 각 개념을 "레슨" 형태로 훌륭하게 전달한다. 꼭 확인해 보라.

Svelte 사이트에는 브라우저에서 여러 가지를 해볼 수 있는 REPL이 존재한다. 여기 우리가 만든 리스트 예제 앱이다. 직접 해보고 싶다면 svelte.dev/repl 에서 시작해 볼 수 있다.

하나 더! Svelte의 제작자인 Rich Harris는 Rethinking Reactivity라는 제목으로 Svelte를 만든 동기와 멋진 데모와 함께 강연했다. 아직 보지 못했다면 꼭 확인해보라.

  • Rethinking reactivity - Rich Harris
한정2019.10.02
Back to list