HEROPY
Tech
SvelteJS(스벨트) - 새로운 개념의 프론트엔드 프레임워크(updated)
{{ scrollPercentage }}%

SvelteJS(스벨트) - 새로운 개념의 프론트엔드 프레임워크(updated)

sveltejssapper

시작하기에 앞서 개인적인 생각을 정리하면,
프론트엔드 프레임워크 입문자 기준에서는 성숙도나 문서 디테일(한글) 등을 상대적으로 고려했을 때 러닝 커브는 Vue와 비슷하지 않을까 싶고, 기존에 프레임워크를 실무에서 사용하고 있는 수준이라면 확실히 Vue보다 쉽게 학습할 수 있을 겁니다.

후발 주자답게 공식 문서와 여러 튜토리얼 그리고 예제도 자세하게 준비되어 있습니다.

Sapper가 좀 더 쓸만해 지길 기대하고 있는데,
주로 Nuxt를 사용하는 입장에서 비교가 되다 보니 아직은 아쉬움이 있습니다.

Svelte란

이하 내용은 [email protected]을 기준으로 작성했습니다.

Svelte(스벨트)Rich Harris가 제작한 새로운 접근 방식을 가지는 프론트엔드 프레임워크입니다.
Svelte는 자신을 ‘프레임워크가 없는 프레임워크’ 혹은 ‘컴파일러’라고 소개합니다.
이는 Virtual(가상) DOM이 없고, Runtime(런타임)에 로드할 프레임워크가 없음을 의미합니다.
기본적으로 빌드 단계에서 구성 요소를 컴파일하는 도구이므로 페이지에 단일 번들(bundle.js)을 로드하여 앱을 렌더링할 수 있습니다.

최근까지 ‘The magical disappearing UI framework’라는 태그라인을 사용했습니다.

‘Cybernetically enhanced web apps’라는 태그라인으로 변경되었습니다.

Svelte의 특징

다른 프레임워크와 Svelte의 주요 차이점을 알아봅시다.

간결한 코드

Svelte는 높은 가독성을 유지하며 더 적은 코드를 작성할 수 있습니다.
다음의 Svelte 코드를 살펴보세요.

<!-- Svelte -->

<script>
  let a = 1;
  let b = 2;
</script>

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

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

위 코드는 ReactVue에서 다음과 같이 작성할 수 있습니다.

// React

import React, { useState } from 'react';

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

  function handleChangeA(event) {
    setA(+event.target.value);
  }

  function handleChangeB(event) {
    setB(+event.target.value);
  }

  return (
    <div>
      <input type="number" value={a} onChange={handleChangeA}/>
      <input type="number" value={b} onChange={handleChangeB}/>

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

<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>

No virtual DOM

Svelte는 Virtual(가상) DOM을 사용하지 않습니다.
Virtual DOM은 충분히 빠르고 유용하지만 이는 기능이 아닌 수단일 뿐이며, 이를 사용하지 않고도 유사한 프로그래밍 모델을 얻을 수 있다고 Svelte는 설명합니다.
새로운 Virtual DOM을 이전 Snapshot과 비교하거나(Diffing), 상태 변화에 따른 새로운 가상 요소 생성 등에 많은 오버헤드가 있을 수 있으며 최종적으론 실제 DOM을 업데이트해야 하므로 그 과정을 생략하는 것이 더욱 빠를 수 있다고 합니다.

이에 대한 더 자세한 내용은 Virtual DOM is pure overhead에서 더 자세하게 확인할 수 있습니다.

기존 UI 프레임워크와 달리, Svelte는 런타임에 작업을 기다리지 않고 빌드 시간에 앱에서 변경 사항이 어떻게 발생하는지 알고 있는 컴파일러입니다.

반응성

반응성은 변경된 값이 DOM에 자동으로 반영됨을 의미합니다.
Svelte는 별도의 Setter 없이 값의 할당(assignments)만으로 업데이트를 트리거(Trigger)할 수 있습니다.
실제로 상당히 편리합니다!

<script>
  let count= 0;
</script>

<button on:click={() => count += 1}>
  {count}
</button>

컴파일 결과가 할당을 계측하고 DOM을 갱신합니다.

// JS output

$$invalidate('count', count += 1);

이는 다음과 같이 Store 사용에도 굉장한 이점을 부여합니다.

// store.js

import { writable } from 'svelte/store';

export const count = writable(0); // similar to `count = 0`

countwritable()에서 반환된 쓰기용 객체 데이터이기 때문에,
$ 접두사($count)를 사용해 Store를 참조하겠다는 의미로 사용할 수 있습니다.

<script>
  import { count } from './store.js';
</script>

<button on:click={() => $count += 1}>
  {$count}
</button>

Svelte Twitter with Evan you

Svelte2는 Vue와 상당히 비슷했었죠!

퍼포먼스

W3C HTML5 Conf 2019에서 변규현 님의 Svelte와 React 퍼포먼스 비교 시연은 생각보다 놀라웠습니다.
메모리 사용량의 비교 결과를 보시면 차이가 확실히 느껴지는데, 컴파일 Output이 워낙 작기도 하고 가상 DOM Diffing이 없어서인지 훨씬 안정적으로 동작하고 있었습니다.

발표 자료는 변규현 님의 블로그(Let’s start SVELTE, goodbye React & Vue)에서 확인하실 수 있습니다.

W3C 2019 Conf Svlete

W3C HTML5 Conf 2019, 변규현 님의 Svelte 세션

Svelte 설치

Degit을 이용해 새로운 프로젝트를 생성합니다.
sveltejs/template에서 템플릿 구조를 확인할 수 있습니다.

$ npx degit sveltejs/template my-svelte-project
$ cd my-svelte-project
$ npm i
$ npm run dev

설치 후 Svelte는 다음과 같은 구조를 가집니다.

Svelte template directory structure

  • /public에는 Svelte가 수행한 컴파일 결과가 들어갑니다.
  • /src는 모든 사용자 정의 Svelte 코드를 저장합니다.
  • rollup.config.jsRollup이라는 Webpack에 대응하는 자바스크립트용 모듈 번들러의 설정 파일입니다. 각 번들러의 차이점을 이해하고 싶다면 Comparing bundlers: Webpack, Rollup & Parcel를 확인해 보세요.

package.json 내용은 다음과 같습니다.

package.json in Svelte

  • npm-run-allrun-p를 이용해 스크립트를 병렬로 실행합니다.
  • sirv-clisirv public --single을 이용해 SPA 서버를 실행합니다.

Svelte REPL

Svelte REPL(레플)이 준비되어 있습니다.
‘+’ 버튼을 눌러 파일을 추가하고 상대경로(확장자를 작성해야 합니다)로 접근할 수 있습니다.

<script>
 import MyComponent from './MyComponent.svelte';
 import { store } from './store.js';
 // ...
</script>

Svelte REPL

Svelte REPL

main.js

main.js는 Svelte의 시작점입니다.
기본 구성을 App.svelte컴포넌트에서 가져오고 다음 2개 속성을 포함하는 생성자로 App 인스턴스를 생성합니다.

  • targetApp.svelte컴포넌트에서 생성된 HTML Output(출력)을 문서에 삽입하도록 지정합니다.
  • props는 컴포넌트에 전달해야 하는 속성들의 객체 데이터를 할당합니다.
// src/main.js
import App from './App.svelte';

const app = new App({
  target: document.body,
  props: {
    name: 'world'
  }
});

export default app;

그리고 main.jsrollup.config.js에서 진입점(Entry point)으로 설정합니다.

// rollup.config.js
// ...
export default {
  input: 'src/main.js',
  // ...
};

Svelte에는 Rollup을 위한 플러그인뿐만 아니라 Webpack을 위한 Loader 그리고 Parcel을 위한 플러그인도 준비되어 있습니다.

Svelte API

Svelte 홈페이지에 API Doc, 튜토리얼, 예제 등이 워낙 잘 구성되어 있어서 가이드 없이 충분히 쉽게 학습할 수 있습니다.
이하 제가 학습하면서 부분 정리한 내용을 참고하세요.

컴포넌트

.svelte파일(컴포넌트)은 다음과 같은 형식을 가질 수 있습니다.

  • <script>, <style>, 마크업(Markup, HTML 작성)을 선택적으로 작성
  • 마크업 요소는 없거나, 여러 개일 수 있음
  • 순서는 중요하지 않음
<!-- App.svelte -->

<script></script>
<style><style>
<!-- 마크업 -->

<script context=”module”>

<script context="module"> 내에서 선언된 내용은 컴포넌트의 인스턴스가 아닌 모듈이 평가될 때 한 번 실행됩니다.
이 블록 안에서 선언된 값은 해당 컴포넌트 내에서 접근 가능하지만, 반대로 <script context="module">가 해당 컴포넌트의 다른 값은 접근할 수 없습니다.

흥미로운 기능이지만 <script context="module"> 내에서 선언된 변수는 값을 재할당해도 반응성(DOM 업데이트)을 가지지 않는 점에 주의해야 합니다.

<!-- App.svelte -->

<script>
  import RandomBox, {random} from './RandomBox.svelte';
</script>

<button on:click={random}>
  Random!
</button>
<RandomBox />
<RandomBox />
<RandomBox />
<!-- RandomBox.svelte -->

<script context="module">
  let inputEls = new Set();

  export function random() {
    inputEls.forEach(el => {
      el.value = Math.random();
    });
  }
</script>

<script>
  import { onMount } from 'svelte';

  let input;
  let value = Math.random();

  onMount(() => {
    inputEls.add(input);
    return () => inputEls.delete(input); // Unmounted
  });
</script>

<div>
  <input bind:this={input} {value}>
</div>

다음은 <script context="module">를 사용하는 Sapper 동적 라우팅 설정 코드의 예시입니다.

<!-- src/routes/blog/[slug].svelte -->

<script context="module">
  // the (optional) preload function takes a
  // `{ path, params, query }` object and turns it into
  // the data we need to render the page
  export async function preload(page, session) {
    // the `slug` parameter is available because this file
    // is called [slug].svelte
    const { slug } = page.params;

    // `this.fetch` is a wrapper around `fetch` that allows
    // you to make credentialled requests on both
    // server and client
    const res = await this.fetch(`blog/${slug}.json`);
    const article = await res.json();

    return { article };
  }
</script>

<script>
  export let article;
</script>

<style>

<style>에 선언된 CSS는 기본적으로 해당 컴포넌트의 유효범위(Scoped)를 가지게 됩니다.
요소의 class속성에 Svelte-Hash가 추가됩니다.

Svelte style hash

Class 속성에 추가된 Svelte-Hash

Svelte style hash

Svelte-Hash가 적용된 선택자

유효범위 없이 선언하려면 :global(선택자) 수정자(Modifier)를 사용할 수 있습니다.

<style>
  ul.container li.item {
    width: 100px;
  }
</style>

Svelte scoped

일반 선택자 사용
<style>
  :global(ul.container li.item) {
    width: 100px;
  }
</style>

Svelte no scoped

:global() 사용

문법

Reactive Statements(&:)

$:은 Label 식별자(Identifier)가 $인 순수한 자바스크립트 Label 구문이며, Svelte는 이 구문에 특별한 의미를 부여하고 반응성을 자동으로 계측합니다.
let 선언을 사용하지 않습니다.

계산된(computed) 변수 혹은 반응성 감시자(Watch) 정도로 생각하면 쉽습니다.

<script>
  let count = 1;
  $: double = count * 2;
</script>

<h1>{double}</h1>
<button on:click={() => count += 1}>
  Increase!
</button>

Svelte label example

Svelte Reactive Statements 예제 결과

최상위 레벨에 유효 변수가 포함되어 있으면 값이 변경될 때마다 구문을 실행하기 때문에 다음과 같은 작성이 가능합니다.

<script>
  let a = 1;
  let b = 2;

  $: c = a + b;
  $: watch(a); // 자동 구문 실행을 위해 `a`를 인수로 사용.
  $: {
    console.log(a);
    console.log(b);
    watch();
  }

  function watch() {
    console.log('Watch!');
  }
</script>

{#await}

Await 블록은 Promise 객체를 사용해서 비동기 코드를 다음 상태로 분기할 수 있습니다.

  • 대기(pending): ‘이행’하거나 ‘거부’되지 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

다음은 가져온 Todo list를 Each 블록으로 반복 출력하는 예제입니다.
가져오는 동안 ‘로딩 요소’를, 실패 시 ‘에러 요소’를 출력할 때 유용하군요!

<script>
  function fetchTodo() {
    return fetch('https://jsonplaceholder.typicode.com/todos/')
      .then(response => response.json());
  }
</script>

{#await fetchTodo()}
  <!-- pending -->
  Loading...
{:then todos}
  <!-- fulfilled -->
  <ul>
    {#each todos as todo}
      <li>{todo.title}</li>
    {/each}
  </ul>
{:catch error}
  <!-- rejected -->
  Error!
{/await}

‘로딩 요소’ 혹은 ‘에러 요소’ 출력이 필요하지 않다면 다음처럼 좀 더 심플하게 작성할 수 있습니다.

<script>
  const fetchedTodo = fetch('https://jsonplaceholder.typicode.com/todos/')
      .then(response => response.json());
</script>

{#await fetchedTodo then todos}
  <ul>
    {#each todos as todo}
      <li>{todo.title}</li>
    {/each}
  </ul>
{/await}

디렉티브

use

요소가 생성될 때 호출할 함수를 지정합니다.
요소를 베이스로 사용하는 플러그인을 제작할 때 유용하겠네요.

action 함수가 반환하는 destroy 메소드를 통해 use를 사용한 요소(node)가 DOM에서 제거되었을 때를 정의할 수 있습니다.

<script>
  function action(node) {
    // Logic..
    return {
      destroy() {}  // `node`가 DOM에서 제거된(unmounted) 이후 호출
    }
  }
</script>

<div use:action></div>

추가로, 인수도 사용할 수 있습니다.
그리고 인수의 값이 변경되었을 때 호출할 있는 update 메소드를 정의할 수 있습니다.

<script>
  let count = 0;
  function action(node, param) {
    // Logic..
    return {
      update(param) {},  // `param`(`count`)의 값이 변경될 때마다 호출
      destroy() {}
    }
  }
</script>

<div use:action={count}></div>

이해가 쉽도록 간단한 예제를 준비했습니다.
확인해 보세요!

<!-- App.svelte -->

<script>
  import { zoom } from './zoom.js';
</script>

<div use:zoom></div>
<div use:zoom={2}></div>

<style>
  div {
    width: 100px;
    height: 100px;
    background: tomato;
  }
</style>
// zoom.js

export function zoom (node, scale = 1.3) {
  node.style.transition = '1s';

  function zoomIn() {
    node.style.transform = `scale(${scale})`;
  }

  function zoomOut() {
    node.style.transform = 'scale(1)';
  }

  node.addEventListener('mouseenter', zoomIn);
  node.addEventListener('mouseleave', zoomOut);

  return {
    destroy() {
      node.removeEventListener('mouseenter', zoomIn);
      node.removeEventListener('mouseleave', zoomOut);
    }
  };
}

특별한 요소

<svelte:options>

<svelte:options>라는 특별한 요소를 통해 컴포넌트의 컴파일러 옵션을 지정할 수 있습니다.

const svelte = require('svelte/compiler');

const result = svelte.compile(source, {
    // options
});

<svelte:options immutable />

같은 메모리 주소를 참조할 수 있는 객체 타입(object, array 등)의 특성(Mutable) 때문에, Svelte의 할당은 불필요한 반응성(Reactive, DOM 업데이트)을 가질 수 있습니다.
그래서 이 문제를 해결하기 위해 컴포넌트에 데이터 불변성(Immutability) 옵션(immutable)을 다음과 같이 지정할 수 있습니다.

<svelte:options immutable={true} />
<!-- or -->
<svelte:options immutable />

다음 예제는 버튼(Add Todo!)을 누르면 todo 데이터를 가져와 todos에 추가합니다.
가져온 Todo 데이터를 Todo.svlete 컴포넌트로 렌더링하고 DOM이 업데이트되면 called XX times라는 메시지를 통해 해당 컴포넌트가 몇 번 업데이트 되었는지를 보여줍니다.
여기서 Todo.svelte 컴포넌트 상단에 <svelte:options immutable /> 여부에 따라 결과가 달라집니다.

Svelte에서 push() 같은 배열 메소드는 반응성을 가지지 않습니다.

<!-- App.svelte -->

<script>
  import Todo from './Todo.svelte';

  let todos = [];
  let index = 1;

  // Todo 아이템 가져오기.
  function fetchTodo(i = 1) {
    return fetch(`https://jsonplaceholder.typicode.com/todos/${i}`)
      .then(response => response.json());
  }

  // Todo 아이템을 목록(todos)에 추가하기.
  function addTodo() {
    fetchTodo(index)
      .then(todo => {
        todos = [...todos, todo];
        index += 1;
      });
  }

  addTodo(); // 첫 번째 아이템 호출
</script>

<button on:click={addTodo}>
  Add Todo!
</button>

{#each todos as todo, i (todo.id)}
  <Todo {todo} />
{/each}
<!-- Todo.svelte -->

<!-- 컴포넌트 상단에 불변성 옵션을 추가!! -->
<svelte:options immutable />

<script>
  import { afterUpdate } from 'svelte';

  export let todo;

  let updatedCount = 0;

  $: isPlural = updatedCount > 1 ? 'times' : 'time';

  // 업데이트 된 직후 콜백이 실행
  afterUpdate(() => {
    updatedCount += 1;
  });
</script>

<div>
  {todo.id}. {todo.title} (called <strong>{updatedCount}</strong> {isPlural})
</div>

svelte:options immutable false

<svelte:options immutable={false} /> 예제 결과
아이템이 추가될 때마다 Todos 전체가 업데이트되는 문제를 가지고 있습니다.

svelte:options immutable true

<svelte:options immutable={true} /> 예제 결과
아이템이 추가돼도 각 Todo가 업데이트되지 않습니다.

라이프사이클

tick

tick은 언제든지 호출할 수 있습니다.
컴포넌트(다른 컴포넌트를 포함)의 상태 변경이 DOM에 적용되면 바로 Promise(resolve) 객체를 반환합니다.

Promise로 랩핑된 nextTick?!

다음 예제에서 await tick()이 없을 때와 비교해 보세요!

<!-- App.svelte -->

<script>
  import { tick } from 'svelte';

  let w = 100;
  let boxWidth = 100;

  function getRandomSize() {
    return parseInt(Math.random() * (500 - 100) + 100);
  }
  async function shuffleSize() {
    w = getRandomSize();
    await tick();
    boxWidth = document.querySelector('.box').getBoundingClientRect().width;
  }
</script>

<h2>Random W: {w}</h2>
<h2>Box Size: {boxWidth}</h2>
<div on:click={shuffleSize}
     style="width: {w}px;"
     class="box"></div>

<style>
  .box {
    height: 100px;
    background: tomato;
    border-radius: 10px;
    cursor: pointer;
  }
</style>

스토어

Svelte는 자체적으로 스토어(Store)를 지원합니다.
svelte/store 모듈을 사용하면 됩니다.

// store.js

import { readable, writable, derived } from 'svelte/store';

export const r = readable(0);
export const w = writable(0);
export const d = derived(w, $w => $w + 1);

readable, writable, derived로 정의된 상태(스토어 객체)는 기본적으로 subscribe 메소드를 포함하며, writable로 정의된 상태는 추가로 setupdate 메소드를 사용할 수 있습니다.
이는 Svelte에서 각 메소드를 사용하지 않고 $ 접두사로 스토어를 참조할 수 있도록 해줍니다.

<script>
  import { w } from './store.js';

  $w = 1;  // w.set(1)
  $w += 1;  // w.update(v => v + 1)
</script>

Svelte는 $ 접두사 사용을 권장합니다!

Svelte store object

derived

derived는 스토어의 정의된 상태(스토어 객체)를 가져와, 변경을 감지하고 콜백을 실행합니다.

계산된(Computed) 상태(state)를 만든다고 생각하면 쉽습니다.

간단한 예제를 살펴봅시다.
count를 콜백에서 $count로 참조하고, 콜백의 반환 값이 double의 값이 됩니다.

// store/count.js

import { writable, derived } from 'svelte/store';

export const count = writable(2); // count is 2
export const double = derived(count, $count => $count * 2); // double is 4

derived로 생성된 값은 ‘읽기전용’이며, Svelte 내에서 다음과 같이 $로 스토어를 참조해 호출합니다.

subscribe 메소드를 가지는 스토어 객체만 $를 사용해 참조할 수 있습니다.
따라서 $를 접두사로 사용하는 일반 변수 및 가져오기 이름을 사용할 수 없습니다.
E.g. let $hello = 'world'; // Not allowed!

<!-- App.svelte -->

<script>
  import { count, double } from './store/count.js';
</script>

<h2>{$double}</h2>
<button on:click={() => $count += 1}>
  Plus 1
</button>

다른 스토어의 값도 호출할 수 있습니다.

확실히 편리하군요!
단, 참조 관계가 복잡해지지 않도록 조심하는 것이 좋겠네요.

// store/number.js

import { derived } from 'svelte/store';
import { count } from './count.js';

export const triple = derived(count, $count => $count * 3); // triple is 6

derived의 콜백에 다음과 같이 2번째 인수(set)를 지정할 수 있습니다.
2번째 인수가 지정되면 더 이상 콜백의 반환(return)으로 double에 값을 지정할 수 없지만, 대신 비동기 설정에 유용합니다.

// store/count.js

import { writable, derived } from 'svelte/store';

export const count = writable(2);
export const double = derived(count, ($count, set) => {
  fetch(`https://jsonplaceholder.typicode.com/todos/${$count}`)
    .then(response => response.json())
    .then(json => set(json.title));
});

2개 이상의 상태(스토어 객체)를 사용하기 위해 다음과 같이 인수로 배열을 사용할 수 있습니다.

// store/multiply.js

import { writable, derived } from 'svelte/store';

export const n1 = writable(1);
export const n2 = writable(7);
export const multiply = derived([n1, n2], ([$n1, $n2]) => $n1 * $n2);

모션

tweened

tweened는 지정된 값을 정해진 시간 동안 업데이트하는 재미있는 기능으로 스토어 객체를 반환하는 것에 주의해야 합니다.
다음과 같이 작성할 수 있습니다.

import { tweened } from 'svelte/motion';

const store = tweened(1, { // 첫 번째 인수로 기본값 설정
  delay: 0, // 값을 업데이트 하기 전에 대기 시간
  duration: 400, // 값을 업데이트 하는 시간
  easing: t => t, // Same as `linear`, 타이밍 함수
  interpolate: (a, b) => t => value // 보간 함수
});

tweened가 스토어 객체를 반환하기 때문에 number을 업데이트하기 위해 $($number += 1)를 사용합니다.
number.update(n => n + 1)과 같이 update 메소드를 사용할 수도 있습니다.

<script>
  import { tweened } from 'svelte/motion';
  import { linear } from 'svelte/easing';

  const number = tweened(1, {
    duration: 1000,
    // easing: linear  // Default value!
  });

  $: fixedNumber = $number.toFixed(2);
</script>

<h1>
  {fixedNumber}
</h1>
<button on:click={() => $number += 1}>
  Increase!
</button>

Svelte tweened store example

Tweened Store 예제 결과

라이브러리와 플러그인 for Svelte

Preprocess(SCSS, Autoprefixer…)

‘svelte-preprocess’는 다음과 같은 (전)처리기들을 지원합니다.

  • PostCSS
  • CoffeeScript
  • TypeScript
  • Pug
  • Sass(SCSS)
  • Less
  • Stylus

저는 ‘SCSS’‘Autoprefixer(PostCSS)’를 설정하려고 합니다.
다음의 모듈을 설치합니다.

$ npm i -D svelte-preprocess postcss postcss-load-config autoprefixer node-sass
// rollup.config.js

import svelte from 'rollup-plugin-svelte';
import { scss, postcss } from 'svelte-preprocess';

plugins: [
  svelte({
    // ...
    preprocess: [
      scss(),
      postcss({
        plugins: [
          require('autoprefixer')
        ]
      })
    ]
  }),
  // ...
]
{
  "_comment": "package.json",
  "browserslist": [
    "defaults"
  ]
}

.svelte 파일 내에서 다음과 같이 사용할 수 있습니다.

<style type="text/scss">
  // ...
</style>
<!-- OR -->
<style lang="scss">
  // ...
</style>

<!-- IMPORT -->
<style src="./my-style.scss"></style>

Router

SPA를 위한 라우터 모듈로 Svelte-spa-router를 선택했습니다.
SSR이 필요하다면 Sapper가 좋을 선택이 될 것입니다!

$ npm i -D svelte-spa-router

routes.js를 생성해 다음과 같이 Route로 정의할 컴포넌트를 연결합니다.
이 컴포넌트들은 /routes 디렉터리에 생성합니다.

// src/routes.js

import Home from './routes/Home.svelte';
import About from './routes/About.svelte';
import Blog from './routes/Blog.svelte';

const routes = {
  '/': Home,
  '/about': About,
  '/blog': Blog
};

export default routes;

위 설정 파일을 App.svelte에서 가져와 다음과 같이 연결합니다.
각 경로로 이동할 수 있게 Header.svelte 컴포넌트도 같이 추가합니다.

Svelte SPA Router default directory structure

<!-- src/App.svelte -->

<script>
  import Router from 'svelte-spa-router';
  import routes from './routes';
  import Header from './components/Header.svelte';
</script>

<Header />
<Router {routes} />
<!-- src/components/Header.svelte -->

<script>
  import { link } from 'svelte-spa-router';
  import active from 'svelte-spa-router/active';
</script>

<style>
  :global(header a.active) {
    font-weight: bold;
    text-decoration: underline;
  }
</style>

<header>
  <a href="/" use:link use:active>Home</a>
  <a href="/about" use:link use:active>About</a>
  <a href="/blog" use:link use:active>Blog</a>
</header>

Svelte SPA Router Example

Import path alias

<script>
  import MyComponent from '../../../components/MyComponent';
</script>

Svelte template에는 별도의 경로 별칭(alias)이 설정되어 있지 않아 다음과 같이 rollup-plugin-alias를 설치하고 rollup.config.js를 설정합니다.

$ npm i -D rollup-plugin-alias
// rollup.config.js

import path from 'path';
import alias from 'rollup-plugin-alias';

export default {
  plugins: [
    alias({
      resolve: ['', '.svelte', '.js'],
      entries: [
        { find: '~', replacement: path.resolve(__dirname, 'src/') }
      ]
    })
  ]
};

이제 경로에서 ~를 사용할 수 있습니다.
혹은 @ 등 원하는 별칭을 사용하시면 됩니다.

<script>
  import MyComponent from '~/components/MyComponent';
</script>

Test Library with Jest

Jest를 사용해 Test 환경을 구성합니다.
테스트 관련은 아직 많이 부족하다고 느껴지네요.
빠른 업데이트를 기대합니다.

Jest 24버전부터 Babel 6버전의 지원이 중단되었습니다. Jest 23버전의 설치와는 방법이 조금 다릅니다.

$ npm i -D jest @babel/core @babel/preset-env [email protected] babel-jest @testing-library/svelte jest-transform-svelte
  • babel-jest: Jest를 위한 Babel을 구성.
  • [email protected]: babel-jest@babel/*패키지에서 정상적으로 동작시키기 위해 사용.
  • jest-transform-svelte: Jest를 사용해 Svelte 컴포넌트를 정상적으로 동작시키기 위해 사용.

Jest의 설정을 위해 jest.config.js을 생성합니다.
혹은 package.json"jest"블록을 선언할 수도 있습니다.

// jest.config.js

const sveltePreprocess = require('svelte-preprocess'); // If you use Svelte preprocess like SCSS or Autoprefixer

module.exports = {
  transform: {
    '^.+\\.js$': 'babel-jest',
    '^.+\\.svelte$': [
      'jest-transform-svelte',
      {
        preprocess: sveltePreprocess(),
        debug: false,
        noStyles: true,
        compilerOptions: {}
      }
    ]
  },
  moduleFileExtensions: ['js', 'svelte'],
  moduleNameMapper: {
    '~(.*)$': '<rootDir>/src/$1', // If you use Import path alias `~`
  },
  coverageReporters: ['html'],
  bail: false,
  verbose: false
};

우리는 Babel 7버전을 설치했습니다.

// babel.config.js

module.exports = {
  'env': {
    'test': {
      'presets': [['@babel/preset-env', { 'targets': { 'node': 'current' } }]]
    }
  }
};

간단한 설정이 끝났습니다.
빠른 테스트 실행을 위해 Watch 모드로 스크립트를 등록합니다.

{
  "_comment": "package.json",
  "scripts": {
    "test": "jest --watchAll"
  }
}

이제 다음과 같이 실행할 수 있습니다.

$ npm test
$ npm t # Alias

정상적으로 동작하는지 테스트하기 위해,
__tests__ 디텍터리를 생성하고 테스트를 진행할 파일과 동일한 이름으로 App.test.js을 생성합니다.

├─ /src
│   ├─ /__tests__
│   │   └─ App.test.js
│   └─ App.svelte

설정한 테스트 환경이 정상적으로 동작하는지 확인하기 위해 최소한의 코드만 작성합니다.

// __tests__/App.test.js

import App from '../App.svelte';
import { render, cleanup } from '@testing-library/svelte';

beforeEach(cleanup); // Required!

describe('App', () => {
  test('정상적으로 동작해야 합니다', () => {
    const { container } = render(App);

    expect(container.nodeName).toBe('BODY');
  });
});

Svelte test with Jest

다음은 svelte-testing-library의 일부분으로 render의 반환 값을 확인할 수 있습니다.
getQueriesForElementDOM Testing library Queries에서 확인할 수 있습니다.

WebStorm plugin

https://plugins.jetbrains.com/plugin/12375-svelte/

WebStorm을 위한 Svelte 플러그인이 있네요.
WebStorm 버전에 따라 플러그인 버전도 차이가 있을 수 있습니다.

Svelte for Webstorm

VS Code plugin

https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode

VS Code를 위한 플러그인도 있으니 확인해 보세요.

Svelte for VS Code

Svelte Devtools

For Chrome
https://chrome.google.com/webstore/detail/svelte-devtools/ckolcbmkjpjmangdbmnkpjigpkddpogn

For FireFox
https://addons.mozilla.org/en-US/firefox/addon/svelte-devtools/

Svelte 애플리케이션 디버깅을 위한 브라우저용 확장 프로그램입니다.
크롬과 파이어폭스 브라우저를 지원합니다.

Svelte Devtools

구성이 단순하네요

UI Components

Svelte Material UI
https://sveltematerialui.com/

Svelte Flat UI Components
https://svelteui.js.org

사용자가 많이 늘어나야 업데이트가 잘 되겠지만,
개인적으로 양식 요소(설정할 스타일이 비교적 많다보니..) 외 다른 UI 컴포넌트는 잘 사용하지 않는 편이라, 그런 의미로는 이미 충분히 쓸만합니다.

참고 자료(References)

https://svelte.dev/
https://www.infoq.com/news/2019/05/svelte-3-interview-rich-harris/
https://velog.io/@ashnamuh/hello-svelte
https://github.com/rollup/rollup-plugin-alias/issues/49

공지 이미지
{{ title }}
{{ eventPlace }}
{{ eventDate }} ({{ eventDay }}) {{ eventTime }}
오늘 하루 그만 보기