이하 내용은 Svelte@3.31.0을 기준으로 작성했습니다.
인프런 강의 - https://www.inflearn.com/course/스벨트-완벽-가이드?inst=c1552804
유튜브 공개 목록 - https://www.youtube.com/watch?v=QjaHjFlPa-g&list=PL5v0w59YqSue9aPueJ15phdPv6lcvWzVy
이 강의는,
Svelte Core API 완벽 가이드에 포함된 Rollup 기반의 Trello 클론 프로젝트입니다.
프로젝트 내 각 파일에 주석으로 자세한 설명을 명시했으니, 학습에 도움이 되실 거예요!
GitHub Repo - https://github.com/HeropCode/Svelte-Trello-app
DEMO - https://boring-agnesi-165a0d.netlify.app
인프런 강의 - https://www.inflearn.com/course/스벨트-실습-프로젝트?inst=0bd6a806
유튜브 공개 목록 - https://www.youtube.com/watch?v=yMOSlm667To&list=PL5v0w59YqSueBr4Nwu2xHb_a32RRkDqH4
이 강의는,
GitHub Repo - https://github.com/HeropCode/Svelte-Movie-app
DEMO - https://competent-cori-258206.netlify.app
인프런 강의 - https://www.inflearn.com/course/스벨트-입문-가이드?inst=e4eed96c
이 강의는,
Svelte(스벨트)는 Rich Harris가 제작한 새로운 접근 방식을 가지는 프론트엔드 프레임워크입니다.
Svelte는 자신을 ‘프레임워크가 없는 프레임워크’ 혹은 ‘컴파일러’라고 소개합니다.
이는 Virtual(가상) DOM이 없고, Runtime(런타임)에 로드할 프레임워크가 없음을 의미합니다.
기본적으로 빌드 단계에서 구성 요소를 컴파일하는 도구이므로 페이지에 단일 번들(bundle.js)을 로드하여 앱을 렌더링할 수 있습니다.
최근까지 ‘The magical disappearing UI framework’라는 태그라인을 사용했습니다.
‘Cybernetically enhanced web apps’라는 태그라인으로 변경되었습니다.
다른 프레임워크와 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>
위 코드는 React와 Vue에서 다음과 같이 작성할 수 있습니다.
// 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>
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`
count
는 writable()
에서 반환된 쓰기용 객체 데이터이기 때문에,$
접두사($count
)를 사용해 Store를 참조하겠다는 의미로 사용할 수 있습니다.
<script>
import { count } from './store.js';
</script>
<button on:click={() => $count += 1}>
{$count}
</button>
W3C HTML5 Conf 2019에서 변규현 님의 Svelte와 React 퍼포먼스 비교 시연은 생각보다 놀라웠습니다.
메모리 사용량의 비교 결과를 보시면 차이가 확실히 느껴지는데, 컴파일 Output이 워낙 작기도 하고 가상 DOM Diffing이 없어서인지 훨씬 안정적으로 동작하고 있었습니다.
발표 자료는 변규현 님의 블로그(Let’s start SVELTE, goodbye React & Vue)에서 확인하실 수 있습니다.
이 파트에서는 Svelte의 전반적인 내용을 비교적 가볍게 다룹니다.
Svelte의 특징에 대해서 빠르게 이해할 수 있습니다.
영상 다음에 첨부된 REPL은 최종 결과로, 강의 진행과는 일부 코드가 다를 수 있습니다.
Svelte는 런타임 프레임워크가 아니기 때문에, CDN을 제공하지 않습니다!
Svelte REPL(레플)이 준비되어 있습니다.
‘+’ 버튼을 눌러 파일을 추가하고 상대경로(확장자를 작성해야 합니다)로 접근할 수 있습니다.
Degit을 이용해 Rollup.js 기반의 새로운 프로젝트를 생성합니다.
sveltejs/template에서 템플릿 구조를 확인할 수 있습니다.
$ npx degit sveltejs/template PROJECT_NAME
$ cd PROJECT_NAME
$ npm install
$ npm run dev
설치 후 Svelte는 다음과 같은 구조를 가집니다.
/public/build
에는 Svelte가 수행한 컴파일 결과가 들어갑니다./src
는 모든 사용자 정의 Svelte 코드를 저장합니다.rollup.config.js
은 Rollup이라는 Webpack에 대응하는 자바스크립트용 모듈 번들러의 설정 파일입니다. 각 번들러의 차이점을 이해하고 싶다면 Comparing bundlers: Webpack, Rollup & Parcel를 확인해 보세요.sirv public
을 이용해 SPA 서버를 실행합니다.main.js
는 Svelte의 시작점입니다.
기본 구성을 App.svelte
컴포넌트에서 가져오고 다음 2개 속성을 포함하는 생성자로 App
인스턴스를 생성합니다.
target
은 App.svelte
컴포넌트에서 생성된 HTML Output(출력)을 문서에 삽입하도록 지정합니다.import App from './App.svelte';
const app = new App({
target: document.body
});
export default app;
그리고 main.js
를 rollup.config.js
에서 진입점(Entry point)으로 설정합니다.
export default {
input: 'src/main.js',
// ...
};
Svelte에는 Rollup을 위한 플러그인뿐만 아니라 Webpack을 위한 Loader 그리고 Parcel을 위한 플러그인도 준비되어 있습니다.
Snowpack 기반의 Svelte 템플릿을 만들었습니다.
별도의 구성 없이 바로 프로젝트를 시작할 수 있습니다.
템플릿에서 지원하는 내용은 크게 다음과 같습니다.
다음과 같이 설치합니다.
## Install template
$ npx degit ParkYoungWoong/svelte-snowpack-template DIR_NAME
## Change directory
$ cd DIR_NAME
## Install dependencies
$ npm i
## Start dev server
$ npm run dev
타입스크립트를 사용하는 경우 다음과 같이 작성할 수 있습니다.
<script lang="ts">
let count: number = 0
</script>
SCSS를 사용하는 경우 다음과 같이 작성할 수 있습니다.
<style lang="scss">
$color--primary: royalblue;
h1 {
color: $color--primary;
}
</style>
<script>
let name = 'world'
let age = 85
function assign() {
name = 'Heropy'
age = 36
}
</script>
<h1>Hello {name}!</h1>
<h2 class={age < 85 ? 'active': ''}>
{age}
</h2>
<img
src=""
alt={name} />
<input
type="text"
bind:value={name} />
<button on:click={assign}>
Assign
</button>
<style>
h1 {
color: red;
}
.active {
color: blue;
}
</style>
<script>
let name = 'world'
let toggle = false
</script>
<button on:click={() => {toggle = !toggle}}>
Toggle
</button>
{#if toggle}
<h1>Hello {name}!</h1>
{:else}
<div>No name!</div>
{/if}
<script>
let name = 'Fruits'
let fruits = ['Apple', 'Banana', 'Cherry', 'Orange', 'Mango']
function deleteFruit() {
fruits = fruits.slice(1)
}
</script>
<h1>Hello {name}!</h1>
<ul>
{#each fruits as fruit}
<li>{fruit}</li>
{/each}
</ul>
<button on:click={deleteFruit}>
Eat it!
</button>
<script>
let name = 'world'
let isRed = false
function enter() {
name = 'enter'
}
function leave() {
name = 'leave'
}
</script>
<h1>Hello {name}!</h1>
<div
class="box"
style="background-color: {isRed ? 'red' : 'orange'};"
on:click={() => { isRed = !isRed }}
on:mouseenter={enter}
on:mouseleave={leave}>
Box!
</div>
<style>
.box {
width: 300px;
height: 150px;
background-color: orange;
}
</style>
<script>
let text = ''
</script>
<h1>
{text}
</h1>
<input
type="text"
value={text}
on:input={e => {text = e.target.value}} />
<input
type="text"
bind:value={text} />
<button on:click={() => {text = 'Heropy'}}>
Click
</button>
<script>
import Fruits from './Fruits.svelte'
let fruits = ['Apple', 'Banana', 'Cherry', 'Orange', 'Mango']
</script>
<Fruits {fruits} />
<Fruits {fruits} reverse />
<Fruits {fruits} slice="-2" />
<Fruits {fruits} slice="0, 3" />
<script>
// Props
export let fruits
export let reverse
export let slice
let computedFruits = []
let name = ''
if (reverse) {
computedFruits = [...fruits].reverse()
name = 'reverse'
} else if (slice) {
computedFruits = fruits.slice(...slice.split(','))
name = `slice ${slice}`
} else {
computedFruits = fruits
}
</script>
<h2>
Fruits {name}
</h2>
<ul>
{#each computedFruits as fruit}
<li>{fruit}</li>
{/each}
</ul>
<script>
import { storeName } from './store.js'
import Parent from './Parent.svelte'
let name = 'world'
// let $hello = '' // Error!
$storeName = name
// console.log(storeName) // 스토어 객체
// console.log($storeName) // 스토어 값(데이터)
</script>
<h1>Hello {name}!</h1>
<Parent />
<script>
import Child from './Child.svelte'
</script>
<div>
Parent
</div>
<Child />
<script>
import { storeName } from './store.js'
</script>
<div>
Child {$storeName}
</div>
import { writable } from 'svelte/store'
export let storeName = writable('Heropy')
<script>
import { writable } from 'svelte/store'
import Todo from './Todo.svelte'
let title = ''
let todos = writable([])
let id = 0
function createTodo() {
if (!title.trim()) {
title = ''
return
}
$todos.push({
id,
title
})
$todos = $todos
title = ''
id += 1
}
</script>
<input
bind:value={title}
on:keydown={(e) => {e.key === 'Enter' && createTodo()}} />
<button on:click={createTodo}>
Create Todo
</button>
{#each $todos as todo}
<Todo {todos} {todo} />
{/each}
<script>
export let todos // Store!
export let todo
let isEdit = false
let title = ''
function onEdit() {
isEdit = true
title = todo.title
}
function offEdit() {
isEdit = false
}
function updateTodo() {
todo.title = title
$todos = $todos
offEdit()
}
function deleteTodo() {
$todos = $todos.filter(t => t.id !== todo.id)
}
</script>
{#if isEdit}
<div>
<input
type="text"
bind:value={title}
on:keydown={(e) => {e.key === 'Enter' && updateTodo()}} />
<button on:click={updateTodo}>OK</button>
<button on:click={offEdit}>cancel</button>
</div>
{:else}
<div>
<span>{todo.title}</span>
<button on:click={onEdit}>Edit</button>
<button on:click={deleteTodo}>Delete</button>
</div>
{/if}
앞선 ‘Svelte 시작하기’ 파트를 통해 기본적인 내용을 모두 학습하시고 다음으로 넘어가시는 것을 추천합니다.
Svelte에서는 다음과 같은 라이프 사이클을 제공합니다.
라이프 사이클 | 설명 |
---|---|
onMount | 컴포넌트가 연결된 직후 콜백을 실행 |
onDestroy | 컴포넌트가 연결 해제되기 직전 콜백을 실행 |
beforeUpdate | 컴포넌트의 데이터가 업데이트되기 직전 콜백을 실행 |
afterUpdate | 컴포넌트의 데이터가 업데이트된 직후 콜백을 실행 |
tick | 변경된 데이터가 화면에 반영될 때까지 기다림 |
<script>
import { beforeUpdate, afterUpdate, onMount, onDestroy } from 'svelte'
import Something from './Something.svelte'
let toggle = false
beforeUpdate(() => console.log('Before update!'))
afterUpdate(() => console.log('After update!'))
onMount(() => console.log('Mounted!'))
onDestroy(() => console.log('Before Destroy!'))
</script>
<button on:click={() => {toggle = !toggle}}>
Toggle
</button>
{#if toggle}
<Something />
{/if}
<h1>
Something
</h1>
공개 가능한 영상은 여기까지입니다.
더 많은 영상은 인프런 강의 커리큘럼을 확인하세요.
반응성 데이터의 변경이 곧 화면의 갱신을 의미하지는 않습니다.tick
을 사용하면 데이터 변경 후 화면의 갱신까지 기다릴 수 있습니다.
단, 비동기로 처리해야 합니다.
<script>
import { tick } from 'svelte'
let name = 'world'
async function handler() {
name = 'Heropy'
await tick()
const h1 = document.querySelector('h1')
console.log(h1.innerText) // Hello Heropy!
}
</script>
<h1 on:click={handler}>Hello {name}!</h1>
<script>
import { tick } from 'svelte'
let isShow = false
let input
async function showInput() {
isShow = true
await tick() // Wait..
input && input.focus()
}
</script>
<button on:click={showInput}>Show input..</button>
{#if isShow}
<input
bind:this={input}
type="text" />
{/if}
Svelte의 라이프 사이클은 컴포넌트 외부에서도 정의할 수 있기 때문에, 모듈로 만들 수 있습니다.
<script>
import { lifecycle, delayRender } from './lifecycle.js'
import Something from './Something.svelte'
let done = delayRender()
lifecycle()
</script>
{#if $done}
<h1>Hello Lifecycle!</h1>
{/if}
<Something />
<script>
import { delayRender } from './lifecycle.js'
let done = delayRender(1000)
</script>
{#if $done}
<h1>Something...</h1>
{/if}
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte'
import { writable } from 'svelte/store'
export function lifecycle() {
onMount(() => {
console.log('Mounted!')
})
onDestroy(() => {
console.log('Before destroy!')
})
beforeUpdate(() => {
console.log('Before update!')
})
afterUpdate(() => {
console.log('After update!')
})
}
export function delayRender(delay = 3000) { // ms
let render = writable(false)
onMount(() => {
setTimeout(() => {
// $render = true
console.log(render) // set, update, subscribe
render.set(true)
}, delay)
})
return render
}
{ }
(중괄호)를 사용해 데이터를 속성/내용 등에 보간할 수 있습니다.
<script>
let href = 'https://heropy.blog'
let name = 'Heropy'
let value = 'New input value!'
let isUpperCase = false
</script>
<!-- 속성과 내용의 단방향 연결 -->
<a {href}>{name}</a>
<!-- 입력 요소 양방향 연결 -->
<input
{value}
on:input={e => value = e.target.value} />
<!-- bind 지시어로 양방향 연결 -->
<input bind:value />
<!-- 표현식 -->
<div>{isUpperCase ? 'DIV' : 'div'}</div>
기본 보간법은 데이터를 HTML이 아닌 일반 텍스트로 해석합니다.
실제 HTML을 출력하려면 {@html}
를 사용해야 합니다.
단, 이는 XSS 취약점으로 이어질 수 있기 때문에, 신뢰할 수 있는 콘텐츠에서만 사용하세요!
<script>
let h1 = '<h1>Hello Heropy</h1>'
let xss = '<iframe onload="alert(123)"></iframe>'
</script>
{@html h1}
{@html xss}
데이터가 변경되면 이를 감지해 로그를 작성합니다.
개발자 도구가 열려있는 경우엔 프로세스를 일시정시합니다.
HTML 구조에서 데이터 변경 감지를 작성할 때 유용하겠지만,
개인적으로 자주 사용하고 있진 않습니다.
<script>
let name = 'Heropy'
let index = 0
</script>
{@debug index, name}
<h1 on:click={() => {index += 1}}>
Hello {name}!
</h1>
Svelte에서 반응성 갱신하려면 할당 연산자(=
)를 사용해야 합니다!.push
나 .splice
같은 메소드 사용은 반응성을 갱신할 수 없습니다.
다음 예제에서 user.numbers.push(3)
은 반응성이 갱신됩니다.
그러나 assign
함수에서 user.name = 'Neo'
과 user.depth.a = 'c'
를 제거하면 반응성은 갱신되지 않습니다.
이는 user.name = 'Neo'
과 user.depth.a = 'c'
가 동작하면서 각자 user
객체를 재할당하기 때문입니다.
주석 처리된
$$invalidate
함수 실행을 확인하세요!
실제$$invalidate
함수는 컴파일 결과에서 확인할 수 있습니다.
<script>
let name = 'Heropy'
let fruits = ['Apple', 'Banana', 'Cherry']
let user = {
name: 'Heropy',
age: 85,
depth: {
a: 'b'
},
numbers: [1, 2]
}
let numbers = user.numbers
let hello = 'world'
function assign() {
name = 'Neo'
fruits.push('Orange') // ['Apple', 'Banana', 'Cherry', 'Orange']
fruits = fruits
user.name = 'Neo' // $$invalidate(2, user.name = "Neo", user);
user.depth.a = 'c' // $$invalidate(2, user.depth.a = "c", user);
user.numbers.push(3)
numbers = numbers
}
</script>
<button on:click={assign}>Assign!</button>
<h1>name: {name}</h1>
<h2>fruits: {fruits}</h2>
<h2>user name: {user.name} / {user.age}</h2>
<h2>user depth: {user.depth.a}</h2>
<h2>user numbers: {user.numbers}</h2>
<h2>numbers: {numbers}</h2>
<h2>{hello}</h2>
$:
은 Label 식별자(Identifier)가 $
인 순수한 자바스크립트 Label 구문이며,
Svelte는 이 구문에 특별한 의미를 부여하고 반응성을 자동으로 계측합니다.let
선언을 사용하지 않는 것에 주의합시다.
데이터 변경이 아닌 반응성을 계측하는 것이기 때문에, 데이터의 변경이 즉각 반영되지 않습니다!
따라서 다음 예제와 같이 tick
라이프 사이클을 사용해 반응성을 기다릴 수 있습니다.
<script>
import { tick } from 'svelte'
let count = 0
$: double = count * 2
async function assign() {
count += 1
console.time('timer')
await tick() // Wait for reactivity..
console.timeEnd('timer') // 0.1~0.5ms
console.log(double)
}
</script>
<button on:click={assign}>Assign!</button>
<h2>{count}</h2>
<h2>{double}</h2>
다음 예제는 REPL에서 개발자 도구 콘솔을 꼭 확인해 보세요!
<!-- 콘솔을 확인해 보세요! -->
<script>
let count = 0
// 선언
$: double = count * 2
// 블록
$: {
console.log(count)
console.log(double)
}
// 함수 실행
$: count, log()
// 즉시 실행 함수(IIFE)
$: count, (() => {
console.log('iife: Heropy')
})();
// 조건문(If)
$: if (count > 0) {
console.log('if:', double)
}
// 반복문(For)
$: for (let i = 0; i < 3; i += 1) {
count
console.log('for:', i)
}
// 조건문(Switch)
$: switch (count) {
case 1:
console.log('switch: 1')
break
default:
console.log('switch: default')
}
// 유효범위
$: {
function scope1() {
console.log('scope1')
function scope2() {
console.log('scope2')
function scope3() {
console.log('scope3', count)
}
scope3()
}
scope2()
}
scope1()
}
function log() {
console.log('fn: Heropy!')
}
function assign() {
count += 1
}
</script>
<button on:click={assign}>Assign!</button>
클래스와 스타일은 모두 기본 보간을 통해 데이터를 연결할 수 있습니다.
추가로 클래스는 class
지시어(Directive)를 제공하는데,
이를 통해 좀 더 단순한 문법으로 작성할 수 있습니다.
<script>
let active = false
let color = 'tomato'
let white = 'white'
let letterSpacing = 'letter-spacing: 5px;'
</script>
<button on:click={() => {active = !active}}>
Toggle!
</button>
<!-- <div class={active ? 'active' : ''}> -->
<div class:active={active}>
Hello
</div>
<h2 style="
background-color: {color};
color: {white};
{letterSpacing}">
Heropy!
</h2>
<style>
div {
width: 120px;
height: 200px;
background: royalblue;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 20px;
transition: .4s;
}
.active {
width: 250px;
background: tomato;
}
</style>
<script>
let active = true
let valid = false
let camelCase = true
let color = {
white: '#FFF',
red: '#FF0000'
}
let bold = 'font-weight: bold;'
function multiClass() {
return 'active valid camel-case'
}
</script>
<div class={active ? 'active' : ''}>
3항 연산자 보간
</div>
<div class:active={active}>
Class 지시어(Directive) 바인딩
</div>
<div class:active>
Class 지시어 바인딩 단축 형태
</div>
<div
class:active
class:valid
class:camelCase
class:camel-case={camelCase}>
다중 Class 지시어 바인딩
</div>
<div class={multiClass()}>
함수 실행
</div>
<div
class="style-binding"
style="
color: {color.white};
background-color: {color.red};
{bold}">
스타일 바인딩
</div>
Svelte 컴포넌트에서 작성하는 스타일을 더 쉽고 안전하게 관리할 수 있는 편리한 방법을 제공합니다.<style>
에 선언된 CSS는 기본적으로 해당 컴포넌트의 유효범위(Scoped)를 가집니다.
요소의 class
속성에 Svelte-Hash가 추가됩니다.
입문자 시각에선 스타일 유효범위가 더 복잡한 관리 방식으로 보일 수 있지만,
애플리케이션 규모가 늘어나는 경우, 전역 스타일로 인해 심각하고 복잡한 스타일 충돌이 일어날 수 있습니다!
<style>
ul.container li.item {
width: 100px;
}
</style>
만약 유효범위 없이 선언하려면 :global(선택자)
수정자(Modifier)를 사용할 수 있습니다.
<style>
:global(ul.container li.item) {
width: 100px;
}
</style>
컴포넌트에서 정의한 Keyframes 규칙 또한 Svelte-Hash가 적용됩니다.
규칙 이름 앞에 -global-
수식어를 작성해 Keyframes 규칙을 전역화할 수 있습니다.
<div class="box"></div>
<style>
:global(body) {
padding: 50px;
}
.box {
width: 100px;Class & Style Binding: Pattern
height: 100px;
background: tomato;
animation: zoom .4s infinite alternate;
}
/* -global- */
@keyframes -global-zoom {
0% {
transform: scale(1);
}
100% {
transform: scale(1.5);
}
}
</style>
DOM에서 검색하지 않아도 bind:this
를 통해 바로 요소를 참조할 수 있습니다.
화면에 없던 요소를 데이터를 통해 출력하고 참조하기 위해,
데이터가 변경되고 화면이 갱신될 때까지 기다리도록 tick
라이플 사이클을 사용할 수 있습니다.
<script>
import { tick, onMount } from 'svelte'
let isShow = false
let inputEl
async function toggle() {
isShow = !isShow
await tick()
// const inputEl = document.querySelector('input')
console.log(inputEl)
inputEl && inputEl.focus()
}
</script>
<button on:click={toggle}>Edit!</button>
{#if isShow}
<input bind:this={inputEl} />
{/if}
입력 요소는 기본적으로 value
속성을 통해 데이터를 연결(바인딩)하며, 많은 경우 양방향 데이터 연결을 위해 bind
지시어를 사용합니다.(bind:value
)
Svelte에서는 ‘checkbox’ 타입을 위해 bind:checked
를,
‘radio’ 타입 등의 여러 입력 요소를 위해 bind:group
을 제공합니다.
아래 예제를 통해 다양한 사용 패턴을 확인하세요!
<script>
let text = ''
let number = 3
let checked = false
let fruits = ['Apple', 'Banana', 'Cherry']
let selectedFruits = []
let group = 'Banana'
let textarea = ''
let select = 'Banana'
let multipleSelect = ['Banana', 'Cherry']
</script>
<!-- let text = '' -->
<section>
<h2>Text</h2>
<input type="text" bind:value={text} />
</section>
<!-- let number = 3 -->
<section>
<h2>Number/Range</h2>
<div>
<input type="number" bind:value={number} min="0" max="10" />
</div>
<div>
<input type="range" bind:value={number} min="0" max="10" />
</div>
</section>
<!-- let checked = false -->
<section>
<h2>Checkbox</h2>
<input type="checkbox" bind:checked={checked} /> Agree?
<label>
<input type="checkbox" bind:checked={checked} /> Agree?(label wrapping)
</label>
</section>
<!-- let fruits = ['Apple', 'Banana', 'Cherry'] -->
<!-- let selectedFruits = [] -->
<section>
<h2>Checkbox 다중 선택</h2>
<strong>Selected: {selectedFruits}</strong>
{#each fruits as fruit}
<label>
<input type="checkbox" value={fruit} bind:group={selectedFruits} />
{fruit}
</label>
{/each}
</section>
<!-- let group = 'Banana' -->
<section>
<h2>Radio</h2>
<!--
<input type="radio" value="Apple" name="my radio" />
<input type="radio" value="Banana" name="my radio" />
<input type="radio" value="Cherry" name="my radio" />
-->
<strong>Selected: {group}</strong>
<label>
<input type="radio" value="Apple" bind:group={group} /> Apple
</label>
<label>
<input type="radio" value="Banana" bind:group={group} /> Banana
</label>
<label>
<input type="radio" value="Cherry" bind:group={group} /> Cherry
</label>
</section>
<!-- let textarea = '' -->
<section>
<h2>Textarea</h2>
<pre>{textarea}</pre>
<textarea bind:value={textarea} />
</section>
<!-- let select = 'Banana' -->
<section>
<h2>Select 단일 선택</h2>
<strong>Seleced: {select}</strong>
<div>
<select bind:value={select}>
<option disabled value="">Please select one!</option>
<option>Apple</option>
<option>Banana</option>
<option>Cherry</option>
</select>
</div>
</section>
<!-- let multipleSelect = ['Banana', 'Cherry'] -->
<section>
<h2>Select 다중 선택(Multiple)</h2>
<strong>Seleced: {multipleSelect}</strong>
<div>
<select multiple bind:value={multipleSelect}>
<option disabled value="">Please select one!</option>
<option>Apple</option>
<option>Banana</option>
<option>Cherry</option>
</select>
</div>
</section>
Svelte는 Contenteditable(내용 수정이 가능한) 요소에 연결할 수 있는, innerHTML
과 textContent
속성을 제공합니다.
Contenteditable 요소도 하나의 입력 요소이기 때문에 bind
지시어를 통해서 양방향으로 데이터를 연결합니다.
<script>
let innerHTML = ''
let textContent = 'Hello world!'
</script>
<div
contenteditable
bind:innerHTML
bind:textContent>
Hello world!
</div>
<div>{innerHTML}</div>
<div>{textContent}</div>
<div>{@html innerHTML}</div>
<style>
div {
border: 1px solid red;
margin-bottom: 10px;
}
</style>
If 조건에 따라 블록을 렌더링합니다.
조건이 true를 반환할 때만 렌더링 됩니다.
기본 형태는 다음과 같습니다.
<script>
let age = 90
</script>
{#if age > 70}
<div>The old man!</div>
{/if}
시작 블록은 #
을,
중간 블록은 :
을,
종료 블록은 /
를 사용합니다.
<script>
let count = 0
</script>
<button on:click={() => { count += 1}}>증가!</button>
<button on:click={() => { count -= 1}}>감소!</button>
<h2>{count}</h2>
<section>
<h2>if</h2>
{#if count > 3}
<div>count > 3</div>
{/if}
</section>
<section>
<h2>if else</h2>
{#if count > 3}
<div>count > 3</div>
{:else}
<div>count <= 3</div>
{/if}
</section>
<section>
<h2>if else if</h2>
{#if count > 3}
<div>count > 3</div>
{:else if count === 3}
<div>count === 3</div>
{:else}
<div>count < 3</div>
{/if}
</section>
<section>
<h2>다중 블록</h2>
{#if count > 3}
{#if count === 5}
count === 5
{:else}
count > 3
{/if}
{/if}
</section>
<style>
section {
border: 1px solid orange;
margin-bottom: 10px;
padding: 10px;
}
h2 {
margin: 0;
margin-bottom: 10px;
}
</style>
Each 반복 블록은 배열 데이터를 기반으로 렌더링합니다.
기본 형태는 다음과 같습니다.
<script>
let fruits = ['Apple', 'Banana', 'Cherry']
</script>
{#each fruits as fruit}
<div>{fruit}</div>
{/each}
Svelte는 할당을 통해 반응성을 갱신하므로 반복 데이터 자체가 갱신되면 목록 전체가 다시 렌더링 됩니다.
이떄 Svelte가 변경되지 않은 데이터의 항목을 다시 렌더링하지 않도록 식별 가능한 고유 Key를 제공하는 것이 중요합니다.
Key는 고유해야 합니다!
많은 경우 반복 데이터 각 항목의
id
속성을 사용합니다.
사용할 데이터 구조가 Key로 사용할 고유한 값을 가지도록 설계하는 것이 좋습니다.
다음 예제의 출력 이름에 Apple
이 중복되고 있지만,id
속성으로 식별 가능한 고유 Key를 제공했기 때문에 문제없이 동작합니다.
<script>
let fruits = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
{ id: '3', name: 'Cherry' },
{ id: '4', name: 'Apple' }
]
function deleteFirst() {
fruits = fruits.slice(1) // ['Banana', 'Cherry', 'Orange']
}
</script>
<button on:click={deleteFirst}>
Delete first fruit!
</button>
<ul>
{#each fruits as fruit (fruit.id)}
<li>{fruit.name}</li>
{/each}
</ul>
시작 블록은 #
을,
중간 블록은 :
을,
종료 블록은 /
를 사용합니다.
<script>
let fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
{ id: 4, name: 'Apple' },
{ id: 5, name: 'Orange' }
]
let todos = []
let fruits2D = [
[1, 'Apple'],
[2, 'Banana'],
[3, 'Cherry'],
[4, 'Orange']
]
let user = {
name: 'Heropy',
age: 85,
email: 'thesecon@gmail.com'
}
</script>
<section>
<h2>기본</h2>
<!-- {#each 배열 as 속성} {/each} -->
{#each fruits as fruit}
<div>{fruit.name}</div>
{/each}
</section>
<section>
<h2>순서(index)</h2>
<!-- {#each 배열 as 속성, 순서} {/each} -->
{#each fruits as fruit, index}
<div>{index} / {fruit.name}</div>
{/each}
</section>
<section>
<h2>아이템 고유화(key)</h2>
<!-- {#each 배열 as 속성, 순서 (키)} {/each} -->
{#each fruits as fruit, index (fruit.id)}
<div>{index} / {fruit.name}</div>
{/each}
</section>
<section>
<h2>빈 배열 처리(else)</h2>
<!-- {#each} {:else} {/each} -->
{#each todos as todo (todo.id)}
<div>{todo.name}</div>
{:else}
<div>아이템이 없어요!</div>
{/each}
</section>
<section>
<h2>구조 분해(destructuring)</h2>
<!-- {#each 배열 as {id, name}} {/each} -->
{#each fruits as {id, name} (id)}
<div>{name}</div>
{/each}
</section>
<section>
<h2>2차원 배열</h2>
<!-- {#each 배열 as [id, name]} {/each} -->
{#each fruits2D as [id, name] (id)}
<div>{name}</div>
{/each}
</section>
<section>
<h2>나머지 연산자(rest)</h2>
<!-- {#each 배열 as {id, ...rest}} {/each} -->
{#each fruits as {id, ...rest} (id)}
<div>{rest.name}</div>
{/each}
</section>
<section>
<h2>객체 데이터</h2>
{#each Object.entries(user) as [key, value] (key)}
<div>{key}: {value}</div>
{/each}
</section>
<style>
section {
border: 1px solid orange;
margin-bottom: 10px;
padding: 10px;
}
h2 {
margin: 0;
}
</style>
키 블록은 연결된 데이터가 변경되면 블록 안의 내용을 화면에 다시 렌더링합니다.
블록 안에서 Svelte 컴포넌트를 사용하는 경우,
컴포넌트가 다시 초기화되고 연결(Mount)됩니다.
<script>
import Count from './Count.svelte'
let reset = false
</script>
{#key reset}
<Count />
{/key}
<button on:click={() => reset = !reset}>
Reset!
</button>
<script>
let count = 0
setInterval(() => {
count += 1
}, 1000)
</script>
<h1>
{count}
</h1>
Await 비동기 블록은 Promise 객체를 사용해서 비동기 코드를 다음 상태로 분기할 수 있습니다.
다음의 간단한 영화 검색 예제를 테스트하기 위해 OMDb API에서 API키를 무료로 발급받으세요.
<script>
// Svelte REPL에서는 npm install을 사용할 수 없어요...
// 자바스크립트 fetch 함수를 사용해도 되지만, 사용법이 일부 다릅니다.
// VS Code에서는 Axios 모듈을 다음과 같이 설치하세요!!
// npm i -D axios
// import axios from 'axios'
import axios from 'https://unpkg.com/axios/dist/axios.min.js'
// http://www.omdbapi.com/apikey.aspx에서 API키를 무료로 발급 받을 수 있습니다.
// 발급 받은 API키를 입력하고 테스트해 보세요!
// 무료 API는 하루 1000개 데이터 제한이 있어요.
let apikey = 'ENTER_YOUR_API_KEY'
let title = ''
// let promise = new Promise(resolve => resolve([]))
let promise = Promise.resolve([])
function searchMovies() {
return new Promise(async (resolve, reject) => {
try {
const res = await axios(`https://www.omdbapi.com/?apikey=${apikey}&s=${title}`)
console.log(res)
resolve(res.data.Search)
} catch (err) {
console.log(err)
reject(err)
} finally {
console.log('Done!')
}
})
}
</script>
<input bind:value={title} />
<button on:click={() => promise = searchMovies()}>검색!</button>
{#await promise}
<!-- pending(대기) -->
<p style="color: royalblue;">loading...</p>
{:then movies}
<!-- fulfilled(이행) -->
<ul>
{#each movies as movie}
<li>{movie.Title}</li>
{:else}
<li>검색된 결과가 없어요...</li>
{/each}
</ul>
{:catch err}
<!-- rejected(거부) -->
<p style="color: red;">{err.message}</p>
{/await}
on
지시어를 사용해 DOM 이벤트를 작성합니다.
기본 형태는 다음과 같습니다.
<script>
let count = 0
</script>
<button on:click={() => count += 1}>
Click me!
</button>
<h1>{count}</h1>
한 요소에 같은 이벤트를 여러 개 연결할 수 있습니다.
<script>
let count = 0
function increase() {
count += 1
}
function current(e) {
console.log(e.currentTarget)
}
</script>
<button
on:click={increase}
on:click={current}
on:click={() => console.log('click!')}>
Click me!
</button>
<h1>{count}</h1>
Svelte에서는 DOM 이벤트를 위한 여러 수식어를 제공합니다.|
(Vertical bar) 기호를 이용해 작성할 수 있으며, 체이닝이 가능합니다.
<a
href="#"
on:click|preventDefault={() => console.log('link!')}>
Internal link..
</a>
<div on:click|preventDefault|capture|self|once={() => console.log('!')}>
Chaining..
</div>
다음과 같은 수식어를 사용할 수 있습니다.
수식어 | 설명 |
---|---|
preventDefault | 기본 동작 방지 |
stopPropagation | 이벤트 버블링 방지 |
passive | 이벤트 처리를 완료하지 않고도 기본 속도로 화면을 스크롤 |
nonpassive | 명시적인 passive: false (보통 필요하지 않음) |
capture | 캡쳐링에서 핸들러 실행 |
once | 최초 실행 후 핸들러 삭제 |
self | 이벤트의 target 과 currentTarget 이 일치하는 경우 핸들러 실행 |
<script>
function clickHandler(event) {
// console.log(event.target)
console.log(event.currentTarget)
}
function wheelHandler(event) {
console.log(event)
}
</script>
<section>
<!-- 기본 동작 방지 -->
<!-- el.addEventListener('click', e => e.preventDefault()) -->
<h2>preventDefault</h2>
<a
href="https://naver.com"
target="_blank"
on:click|preventDefault={clickHandler}>
Naver
</a>
</section>
<section>
<!-- 최초 실행 후 핸들러 삭제 -->
<h2>Once</h2>
<a
href="https://naver.com"
target="_blank"
on:click|preventDefault|once={clickHandler}>
Naver
</a>
</section>
<section>
<!-- 이벤트 버블링 방지 -->
<!-- el.addEventListener('click', e => e.stopPropagation()) -->
<h2>stopPropagation</h2>
<div
class="parent"
on:click={clickHandler}>
<div
class="child"
on:click|stopPropagation={clickHandler}></div>
</div>
</section>
<section>
<!-- 캡쳐링에서 핸들러 실행 -->
<!-- el.addEventListener('click', e => {}, true) -->
<!-- el.addEventListener('click', e => {}, {capture: true}) -->
<h2>capture</h2>
<div
class="parent"
on:click|capture={clickHandler}>
<div
class="child"
on:click={clickHandler}></div>
</div>
</section>
<section>
<!-- event의 target과 currentTarget이 일치하는 경우 핸들러 실행 -->
<h2>self</h2>
<div
class="parent"
on:click|self={clickHandler}>
<div class="child"></div>
</div>
</section>
<section>
<!-- 이벤트 처리를 완료하지 않고도 기본 속도로 화면을 스크롤 -->
<!-- el.addEventListener('wheel', e => {}, {passive: true}) -->
<h2>passive</h2>
<div
class="parent wheel"
on:wheel|passive={wheelHandler}>
<div class="child"></div>
</div>
</section>
<style>
section {
border: 1px solid orange;
padding: 10px;
margin-bottom: 10px;
}
h2 {
margin: 0;
margin-bottom: 10px;
}
.parent {
width: 160px;
height: 120px;
background: royalblue;
padding: 20px;
}
.child {
width: 100px;
height: 100px;
background: tomato;
}
.wheel.parent {
overflow: auto;
}
.wheel .child {
height: 1000px;
}
</style>
<script>
import { onMount } from 'svelte'
import Heropy from './Heropy.svelte'
let heropy
onMount(() => {
// Error in REPL, Try in VS Code.
// console.log(heropy)
// console.log(heropy.title)
})
</script>
<Heropy />
<Heropy title="Hello Heropy" />
<Heropy
title="Hello Neo"
bind:this={heropy} />
<script>
export let title = 'Default value!!'
let name = 'Heropy'
let age = 85
let email = 'thesecon@gmail.com'
</script>
<h2>{title}</h2>
<div>{name}</div>
<div>{age}</div>
<div>{email}</div>
컴포넌트의 Props를 사용해 부모에서 자식 컴포넌트로 데이터를 전달할 수 있습니다.
기본적으로 단반향 연결입니다.
관련한 몇 가지 사용 패턴을 살펴보세요!
<script>
import User from './User.svelte'
let users = [
{
name: 'Neo',
age: 85,
email: 'neo@abc.com'
},
{
name: 'Lewis',
age: 30,
email: 'lewis@abc.com'
},
{
name: 'Evan',
age: 52
}
]
</script>
<section>
{#each users as user}
<User
name={user.name}
age={user.age}
email={user.email} />
{/each}
</section>
<section>
{#each users as {name, age, email}}
<User {name} {age} {email} />
{/each}
</section>
<section>
{#each users as user}
<User {...user} />
{/each}
</section>
<style>
section {
border: 1px solid orange;
margin-bottom: 10px;
padding: 10px;
}
</style>
<script>
export let name
export let age
export let email = 'None...'
</script>
<ul>
<li>{name}</li>
<li>{age}</li>
<li>{email}</li>
</ul>
자식 컴포넌트가 부모로부터 전달받은 Props를 내부에서 수정(할당)하는 경우, 부모 컴포넌트에선 반응성을 가지지 않습니다.
만약 자식에서 수정한 Props가 부모 컴포넌트에서도 반응성을 유지하려면 bind
지시어를 사용해 양방향으로 연결해야 합니다.
편리한 방법이지만, 데이터의 유효범위 관리를 위해 이 방법을 남발하지 않는 것이 좋습니다.
<script>
import Todo from './Todo.svelte'
let todos = [
{ id: 1, title: 'Breakfast', done: false },
{ id: 2, title: 'Lunch', done: false },
{ id: 3, title: 'Dinner', done: false }
]
</script>
{#each todos as todo, index (todo.id)}
<Todo
bind:todos
{todo}
{index} />
{/each}
<script>
export let todos
export let todo
export let index
function deleteTodo() {
todos.splice(index, 1)
todos = todos
console.log(todos)
}
</script>
<div>
<input type="checkbox" bind:value={todo.done} />
{todo.title}
<button on:click={deleteTodo}>X</button>
</div>
Props가 부모에서 자식으로 데이터를 전달하는 방법이라면,
Event Dispatcher는 자식에서 부모로 데이터(이벤트)를 전달하는 방법입니다.
자식 컴포넌트에서 데이터를 포함하는 이벤트를 발생시키고,
부모 컴포넌트에선 그 이벤트를 수신한 핸들러에서 데이터를 꺼내는 방식입니다.
자식 컴포넌트(Child.svelte
)에서 사용하는 기본 구조는 다음과 같습니다.
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
const 전달할_데이터 = '나는 데이터!'
dispatch('이벤트_이름', 전달할_데이터)
</script>
부모 컴포넌트에서 사용하는 기본 구조는 다음과 같습니다.
<script>
import Child from './Child.svelte'
</script>
<Child on:이벤트_이름={event => {
console.log(event.detail) // '나는 데이터!'
}} />
<script>
import Todo from './Todo.svelte'
let todos = [
{ id: 1, title: 'Breakfast', done: false },
{ id: 2, title: 'Lunch', done: false },
{ id: 3, title: 'Dinner', done: false }
]
function deleteTodo(event) {
// event.detail => `new CustomEvent()`를 통해 이벤트를 초기화 할 때 전달 된 모든 데이터를 반환
const todo = event.detail.todo
const index = todos.findIndex(t => t.id === todo.id)
console.log(todo)
todos.splice(index, 1)
todos = todos
}
</script>
{#each todos as todo (todo.id)}
<Todo
{todo}
on:deleteMe={deleteTodo} />
{/each}
<script>
import { createEventDispatcher } from 'svelte'
export let todo
const dispatch = createEventDispatcher()
function deleteTodo() {
// dispatch('deleteMe', todo)
dispatch('deleteMe', {
todo
})
}
</script>
<div>
<input
type="checkbox"
bind:value={todo.done} />
{todo.title}
<button on:click={deleteTodo}>X</button>
</div>
이벤트 포워딩은 자식에서 부모 컴포넌트로 이벤트를 던져 올리는 방법으로,
이벤트 핸들러를 부모 컴포넌트에서 작성할 수 있습니다.
사용법은 아주 간단합니다.
단순히 이벤트의 핸들러를 명시하지 않으면 됩니다.
<div on:click>
Forwarding!
</div>
다음은 Child 컴포넌트에서 Parent 컴포넌트를 거쳐 App 컴포넌트로 myEvent
라는 이벤트를 전달하는 예제입니다.
Parent 컴포넌트의 click
이벤트도 잘 확인해 보세요!
<script>
import Parent from './Parent.svelte'
function handler(e) {
console.log(e.currentTarget)
}
function myEventHandler(e) {
console.log(e.detail.myName)
}
</script>
<Parent
on:click={handler}
on:myEvent={myEventHandler} />
<script>
import Child from './Child.svelte'
</script>
<h2>Parent!</h2>
<button on:click>
Parent click!
</button>
<Child on:myEvent />
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
</script>
<h2>Child!</h2>
<button on:click={() => {
dispatch('myEvent', {
myName: 'Heropy!!'
})
}}>
Child click!
</button>
컴포넌트에서 setContext
로 데이터를 지정하면,
그 컴포넌트를 포함한 모든 하위 컴포넌트에서 getContext
로 그 정의된 데이터를 가져와 사용할 수 있습니다.
Props와 Store의 중간 개념 정도로 이해하면 쉽습니다.
Context API는 다음과 같이 Getter와 Setter로 구성되어 있습니다.
API | 설명 |
---|---|
getContext | 정의된 데이터를 가져옵니다. |
setContext | 자신을 포함한 하위 컴포넌트에서 사용할 데이터를 정의합니다. |
다음 예제를 통해 Context API가 동작하는 범위를 이해하세요!
<script>
import { getContext } from 'svelte'
import Heropy from './Heropy.svelte'
import Lewis from './Lewis.svelte'
import Evan from './Evan.svelte'
const pocketMoney = getContext('heropy') // undefined
</script>
<h1>App({pocketMoney})</h1>
<div>
<Heropy />
<Lewis />
<Evan />
</div>
<style>
h1 {
font-size: 50px;
}
div {
padding-left: 50px;
}
</style>
<script>
import { getContext, setContext } from 'svelte'
import Anderson from './Anderson.svelte'
setContext('heropy', 10000)
const pocketMoney = getContext('heropy') // 10000
</script>
<h1 style="color: red">
Heropy({pocketMoney})
</h1>
<ul>
<li>
<Anderson />
</li>
</ul>
<script>
import { getContext } from 'svelte'
const pocketMoney = getContext('heropy') // undefined
</script>
<h1>Lewis({pocketMoney})</h1>
<script>
import { getContext } from 'svelte'
const pocketMoney = getContext('heropy') // undefined
</script>
<h1>Evan({pocketMoney})</h1>
<script>
import { getContext } from 'svelte'
import Neo from './Neo.svelte'
import Emily from './Emily.svelte'
const pocketMoney = getContext('heropy') // 10000
</script>
<h2>Anderson({pocketMoney})</h2>
<ul>
<li>
<Neo />
</li>
<li>
<Emily />
</li>
</ul>
<script>
import { getContext } from 'svelte'
const pocketMoney = getContext('heropy') // 10000
</script>
<h3>Neo({pocketMoney})</h3>
<script>
import { getContext } from 'svelte'
const pocketMoney = getContext('heropy') // 10000
</script>
<h3>Emily({pocketMoney})</h3>
다음과 같이 context="module"
속성/값을 가지는 SCRIPT 태그에 정의된 내용은,
컴포넌트를 사용하기 위해 모듈로 처음 가져오는 상황에 전역으로 한 번 실행됩니다.
이 블록 안에서 선언된 값은 해당 컴포넌트 내에서 접근 가능하지만,
반대로 <script context="module">
가 해당 컴포넌트의 다른 값에는 접근할 수 없습니다.
흥미로운 기능이지만 <script context="module">
내에서 선언된 변수는 값을 재할당해도 반응성(DOM 업데이트)이 없다는 점에 주의해야 합니다.
반응성이 없기 때문에 화면 갱신용이 아닌, 단순 전역 데이터처럼 사용하면 됩니다.
<script context="module">
let count = 0
</script>
컴포넌트 외부에서도 변수/함수 등을 참조할 수 있도록 export
를 사용할 수 있습니다.
<script context="module">
export let count = 0
</script>
<script>
import Fruit, { count } from './Fruit.svelte'
let fruits = [
'Apple',
'Banana',
'Cherry',
'Mango',
'Orange'
]
</script>
<button on:click={() => console.log(count)}>
Total count log!
</button>
{#each fruits as fruit}
<Fruit {fruit} />
{/each}
<script context="module">
export let count = 0
console.log('Module context!')
</script>
<script>
export let fruit
console.log('Each component init!')
</script>
<div on:click={() => {
count += 1
// console.log(count)
}}>
{fruit}
</div>
다음은 공식 홈페이지의 예제를 이해하기 쉽도록 간소화한 예제입니다.
Set 생성자에 대한 이해만 있으면 충분한데,new Set()
으로 만들어진 인스턴스는 .add()
, .forEach()
같은 메소드를 사용할 수 있는 일종의 유사 배열입니다..add()
는 .push()
를 생각하면 쉽습니다.
stopAll
함수가 어떤 역할을 하는지 이해하는 것이 포인트입니다.
<script>
import AudioPlayer, { stopAll } from './AudioPlayer.svelte'
let audioTracks = [
'https://sveltejs.github.io/assets/music/strauss.mp3',
'https://sveltejs.github.io/assets/music/holst.mp3',
'https://sveltejs.github.io/assets/music/satie.mp3'
]
</script>
<button on:click={stopAll}>
Stop all!
</button>
{#each audioTracks as src}
<AudioPlayer {src} />
{/each}
<script context="module">
const players = new Set()
export function stopAll() {
players.forEach(p => p.pause())
}
</script>
<script>
import { onMount } from 'svelte'
export let src
let player
onMount(() => {
// Like players.push(player)
players.add(player)
})
</script>
<div>
<audio
bind:this={player}
{src}
controls>
<track kind="captions" />
</audio>
</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>
Svelte는 컴포넌트가 전달받는 모든 Props의 정보를 가진 객체($$props
)를 제공합니다.
따라서 전달받을 Props를 모두 명시하지 않아도 사용할 수 있는 장점이 있습니다.
혹은 명시한 Props를 제외한 나머지만 다룰 수 있는 객체($$restProps
)도 제공합니다.
이름 | 설명 |
---|---|
$$props | 컴포넌트에 전달된 모든 Props 정보를 가진 객체입니다. |
$$restProps | 컴포넌트에 명시된 Props를 제외한, 나머지 Props 정보를 가진 객체입니다. |
다음 예제에서 TextField 컴포넌트에 명시된 Props는 value
와 color
입니다.
value
와 color
를 포함한,
컴포넌트에 연결된 type
, placeholder
같은 모든 Props 정보가 들어있는 객체는 $$props
이고,
value
와 color
를 제외한,
컴포넌트에 연결된 type
, placeholder
같은 Props 정보만 들어있는 객체가 $$restProps
입니다.
<script>
import TextField from './TextField.svelte'
let id = ''
let pw = ''
function submit() {
//
}
</script>
<TextField
bind:value={id}
color="yellowgreen"
type="email"
placeholder="ID!"
maxlength="10"
required />
<TextField
bind:value={pw}
color="tomato"
type="password"
placeholder="Password!"
required />
<button on:click={submit}>
Submit!
</button>
<!-- <div>{id} / {pw}</div> -->
<script>
export let value
export let color
</script>
<div class="my-custom-input">
<input
bind:value
style="color: {color};"
{...$$restProps} />
<!-- {...$$props} /> -->
</div>
<style>
.my-custom-input input {
border-radius: 100px;
padding: 10px 20px;
}
</style>
슬롯(Slot)은 컴포넌트의 내용(Content)입니다.
요소(Element)의 내용(Content)과 개념이 같습니다.
<script>
import Hello from './Hello.svelte'
</script>
<!--요소의 내용-->
<h1>Hello world!</h1>
<!--컴포넌트의 내용-->
<Hello>Hello world!</Hello>
단지 컴포넌트의 내용이 어디에 삽입(출력)될 것인지만 <slot>
로 지정하면 됩니다.
다음은 위 예제에서 사용한 Hello 컴포넌트입니다.
<h2>
<!--내용은 <slot>에 삽됩니다!-->
<slot></slot>
</h2>
<p>I'm 'Hello' Component.</p>
슬롯으로 들어오는 내용이 없는 경우 기본 내용을 지정할 수 있습니다.
이를 ‘Fallback Content’라고 합니다.
Fallback Content는 글자(문장)만 아니라 여러 요소 및 컴포넌트를 포함할 수 있습니다.
<slot>Fallback content, 들어오는 내용이 없으면 이 문장을 출력합니다!</slot>
<script>
import Btn from './Btn.svelte'
</script>
<Btn></Btn>
<Btn>Submit!</Btn>
<Btn block>Submit!</Btn>
<Btn color="royalblue">Submit!</Btn>
<Btn
block
color="red">
Danger!
</Btn>
<script>
export let block
export let color
</script>
<button
class:block
style="
background-color: {color};
color: {color ? 'white' : '' };">
<slot>
Default Button!
</slot>
</button>
<style>
button {
background: lightgray;
padding: 10px 20px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: .2s;
}
button:hover {
text-decoration: underline;
}
button:active {
transform: scale(1.05);
}
button.block {
width: 100%;
display: block;
}
</style>
다음과 같이 내용에 슬롯 이름을 지정해 원하는 슬롯에 삽입할 수 있습니다.
<div slot="슬롯_이름"></div>
슬롯에 지정된 이름으로 해당 내용이 삽입됩니다.
<slot name="슬롯_이름"></slot>
<script>
import Card from './Card.svelte'
</script>
<Card>
<div slot="age">85</div>
<h2 slot="name">Heropy</h2>
<div slot="email">thesecon@gmail.com</div>
</Card>
<Card>
<span slot="email">neo@abc.com</span>
<h3 slot="name">Neo</h3>
</Card>
<style>
h2 {
font-weight: 400;
}
h3 {
color: red;
}
</style>
<div class="card">
<slot name="name"></slot>
<slot name="age">??</slot>
<slot name="email"></slot>
</div>
<style>
.card {
margin: 20px;
padding: 12px;
border: 1px solid gray;
border-radius: 10px;
box-shadow: 4px 4px 0 rgba(0,0,0,.1);
}
</style>
범위를 가지는 슬롯으로 컴포넌트 내부에서 특정한 값을 꺼내서 사용할 수 있습니다.
다음과 같이 슬롯에 속성과 값을 포함합니다.
<script>
let message = 'Hello world!'
</script>
<slot myMsg={message}></slot>
그리고 컴포넌트에서 let
지시어를 통해 그 속성을 정의하고 범위 내에서 변수(데이터)처럼 사용할 수 있습니다.
<script>
import Heropy from '~/components/Heropy.svelte'
</script>
<Heropy let:myMsg>
<h2>{myMsg}</h2>
</Heropy>
<script>
import Wrap from './Wrap.svelte'
let fruits = {
apple: {
value: '',
options: {
readonly: false,
disabled: false,
placeholder: 'placeholder A'
}
},
banana: {
value: 'BANANA',
options: {
disabled: false,
placeholder: 'placeholder A'
}
}
}
function add(name) {
console.log(name)
}
function update(name) {
console.log(name)
}
function remove(name) {
console.log(name)
}
</script>
<Wrap
scopeName="apple"
let:_name>
<label
class="fruits__{_name}"
name="{_name}">
<input
bind:value={fruits[_name].value}
readonly={fruits[_name].options.readonly}
disabled={fruits[_name].options.disabled}
placeholder={fruits[_name].options.placeholder}
on:change={() => add(_name)} />
</label>
</Wrap>
<Wrap
scopeName="banana"
let:_name>
<input
bind:value={fruits[_name].value}
disabled={fruits[_name].options.disabled}
placeholder={fruits[_name].options.placeholder}
on:click={() => update(_name)} />
</Wrap>
<Wrap
scopeName="cherry"
let:_name>
<div
class="hello-{_name}"
name="{_name}"
on:click={() => remove(_name)}>
{_name}
</div>
</Wrap>
<script>
export let scopeName
</script>
<div>
<slot _name={scopeName}></slot>
</div>
슬롯 포워딩은 부모 컴포넌트로부터 전달(Forwarding)받은 내용(Content)을 자식 컴포넌트의 내용으로 슬롯(Slot)을 이용해 전달하는 것을 의미합니다.
앞서 살펴본 단일 슬롯, 이름을 가지는 슬롯, 범위를 가지는 슬롯 모두 자식 컴포넌트로 전달할 수 있습니다.
<script>
import Parent from './Parent.svelte'
</script>
<Parent let:scoped>
<h2>Default slot..</h2>
<h3 slot="named">Named slot..</h3>
<h1 slot="scoped">Scoped slot.. {scoped}</h1>
</Parent>
<script>
import Child from './Child.svelte'
</script>
<Child let:scoped>
<slot></slot>
<slot
name="named"
slot="named"></slot>
<slot
name="scoped"
slot="scoped"
scoped={scoped}></slot>
</Child>
<script>
let scoped = 'Scoped!!'
</script>
<slot
name="scoped"
scoped={scoped}></slot>
<slot name="named"></slot>
<slot></slot>
Svelte는 컴포넌트가 전달받은 내용(Content)의 정보를 가진 객체($$slots
)를 제공합니다.
슬롯으로 받을 내용이 존재하는지를 확인하는 데 유용합니다!
<script>
import UserCard from './UserCard.svelte'
</script>
<UserCard>
<h2 slot="name">HEROPY</h2>
<div slot="age">85</div>
<div slot="email">thesecon@gmail.com</div>
</UserCard>
<UserCard>
<h2 slot="name">Neo</h2>
<div slot="email">neo@zillinks.com</div>
</UserCard>
<UserCard>
<h2 slot="name">Evan</h2>
</UserCard>
<script>
console.log($$slots)
</script>
<div class="user-card">
<slot name="name"></slot>
{#if $$slots.age}
<hr />
<slot name="age"></slot>
{/if}
{#if $$slots.email}
<hr />
<slot name="email"></slot>
{/if}
</div>
<style>
.user-card {
width: 300px;
margin: 20px 10px;
padding: 0 10px 20px;
border: 4px solid lightgray;
border-radius: 10px;
box-shadow: 8px 8px rgba(0,0,0,.03);
position: relative;
}
</style>
위 예제의 각 UserCard.svelte
컴포넌트에서 출력한 콘솔을 확인하면 다음과 같습니다.
이름을 가지는 슬롯으로 받는 내용이 2개 이상이면 default
속성(단일 슬롯)이 자동으로 추가되는 것을 확인할 수 있는데,
이는 줄 바꿈으로 인한 공백 문자(띄어쓰기)가 내용으로 전달되면서 해석되는 것으로 확인했습니다.
따라서 다음과 같이 인라인으로 내용을 전달하면 default
속성(단일 슬롯)이 사라지게 됩니다.
‘Svelte 시작하기 > 스토어’ 파트를 먼저 학습하시면 좋습니다.
Svelte는 자체적으로 스토어(Store)를 지원합니다.
내장된 svelte/store
모듈을 사용하면 됩니다.
import { readable, writable, derived, get } from 'svelte/store';
export const r = readable(1);
export const w = writable(7);
export const d = derived(w, $w => $w + 1);
get(r) // 1
get(w) // 7
get(d) // 8
readable
, writable
, derived
로 정의된 스토어 객체는 기본적으로 subscribe
메소드를 포함하며,writable
로 정의된 객체는 추가로 set
과 update
메소드를 사용할 수 있습니다.
subscribe
메소드를 직접 사용해서 수동 구독을 만들 수 있습니다.
<script>
import { w } from './store.js';
const data = w.subscribe(d => data = d)
console.log(data) // 7
</script>
Svelte 컴포넌트에서는 각 메소드를 사용할 필요 없이 $
접두사로 스토어를 참조할 수 있습니다.
이를 자동 구독(Auto-subscription)이라고 합니다.
자동 구독을 통해 set
과 update
메소드를 대신할 수 있어 편리합니다.
Svelte 컴포넌트에선 자동 구독(
$
접두사) 사용을 권장합니다!
<script>
import { w } from './store.js';
console.log($w) // 7
$w = 1; // w.set(1)
$w += 1; // w.update(v => v + 1) // .update()의 콜백에서 반환하는 값이 지정됩니다.
console.log($w) // 2
</script>
Svelte 컴포넌트가 아니면(.js
, .ts
파일 같은) 자동 구독을 사용할 수 없기 때문에,set
, update
, subscribe
메소드를 직접 사용해야 합니다.
값을 읽거나 쓸 수 있는 스토어를 생성합니다.
writable(값)
writable(값, 콜백)
첫 번째 인수는 스토어의 값(초깃값)입니다.
두 번째 인수는 스토어 구독(자동, 수동)이 발생하면 실행될 콜백입니다.
콜백에서 반환하는 함수는 구독이 모두 취소되면(구독자가 모두 없어지면) 실행됩니다.
import { writable } from 'svelte/store'
export let store = writable('값', () => {
// 구독자가 1명 이상이 되면 실행!
return () => {
// 구독자가 0명이 되면 실행!
}
})
<script>
import WritableMethods from './WritableMethods.svelte'
let toggle = true
</script>
<button on:click={() => toggle = !toggle}>
Toggle
</button>
{#if toggle}
<WritableMethods />
{/if}
<script>
import { onDestroy } from 'svelte'
import { get } from 'svelte/store'
import { name, count } from './store.js'
let number
let userName
// Store 객체
// 사용 가능 메소드: set, update, subscribe
console.log(name, count)
// 구독하지 않고 Store 객체의 값만 얻기
console.log(get(name), get(count))
// count 구독자 추가!
const unsubscribeCount = count.subscribe(c => {
number = c
})
// count 구독자 추가!
const unsubscribeCount2 = count.subscribe(() => {})
// name 구독자 추가!
const unsubscribeName = name.subscribe(n => {
userName = n
})
function increase() {
count.update(c => c + 1)
try {
unsubscribeCount() // Only once!
unsubscribeCount2()
} catch (e) {}
}
function changeName() {
// name.update(() => 'Neo')
name.set('Neo')
}
onDestroy(() => {
unsubscribeCount()
unsubscribeCount2()
unsubscribeName()
})
</script>
<button
on:click={increase}
on:click={changeName}>
Click me!
</button>
<h2>{number}</h2>
<h2>{userName}</h2>
import { writable } from 'svelte/store'
export let name = writable('Heropy', () => {
console.log('name 구독자가 1명 이상일 때!')
return () => {
console.log('name 구독자가 0명일 때...')
}
})
export let count = writable(0, () => {
console.log('count 구독자가 1명 이상일 때!')
return () => {
console.log('count 구독자가 0명일 때...')
}
})
스토어 자동 구독을 통해 훨씬 간단하게 작성할 수 있습니다.
<script>
import { name, count } from './store.js'
</script>
<button on:click={() => {
$count += 1 // Increase
$name = 'Neo' // Change name
}}>
Click me!
</button>
<h2>{$count}</h2>
<h2>{$name}</h2>
값을 읽을 수만 있는 스토어를 생성합니다.
readable(값)
readable(값, 콜백)
첫 번째 인수는 스토어의 값(초깃값)입니다.
두 번째 인수는 스토어 구독(자동, 수동)이 발생하면 실행될 콜백입니다.
콜백에서 반환하는 함수는 구독이 모두 취소되면(구독자가 모두 없어지면) 실행됩니다.
읽을 수만 있는 스토어기 때문에 초깃값을 최초 한 번 수정할 수 있도록 콜백에서 set
함수(매개변수)를 사용할 수 있습니다.
import { readable } from 'svelte/store'
export let store = writable('값', set => {
// 구독자가 1명 이상이 되면 실행!
set('값')
return () => {
// 구독자가 0명이 되면 실행!
}
})
<script>
import Readable from './Readable.svelte'
let toggle = true
</script>
<button on:click={() => toggle = !toggle}>
Toggle
</button>
{#if toggle}
<Readable />
{/if}
<script>
import { user } from './store.js'
// 어떤 메소드를 가지는지 Readable 스토어 출력하기!
console.log(user)
console.log($user)
</script>
<button on:click={() => {$user.name = 'Neo'}}>
Click!
</button>
<h1>{$user.name}</h1>
import { readable } from 'svelte/store'
const userData = {
name: 'Heropy',
age: 85,
email: 'thesecon@gmail.com',
token: 'Ag1oy1hsdSDe'
}
export let user = readable(userData, (set) => {
console.log('user 구독자가 1명 이상일 때!')
delete userData.token
set(userData)
return () => {
console.log('user 구독자가 0명일 때...')
}
})
쓰기 가능(writable
)하거나 읽기 전용(readable
) 스토어를 통해 새롭게 계산한 값을 가지는 스토어를 생성합니다.
유독 사용 패턴이 많지만 앞선 ‘쓰기 가능’, ‘읽기 전용’ 스토어 파트를 이해했다면 특별히 어려운 건 없습니다.
첫 번째 인수는 계산에 사용할 스토어이고,
계산할 스토어가 2개 이상인 경우 첫 번째 인수를 배열로 처리해야 합니다.
두 번째 인수는 스토어 구독(자동, 수동)이 발생하면 실행될 콜백입니다.
콜백에서 반환하는 함수는 구독이 모두 취소되면(구독자가 모두 없어지면) 실행됩니다.
세 번째 인수는 계산이 완료되기 전에(비동기 요청 등) 최초 한 번 출력할 초깃값입니다.
derived(스토어, 콜백)
derived(스토어, 콜백, 초깃값)
derived([스토어1, 스토어2], 콜백)
derived([스토어1, 스토어2], 콜백, 초깃값)
콜백에서 스토어를 사용해 계산할 수 있습니다.
첫 번째 인수는 앞서 명시된 스토어의 값을 매개변수로 받습니다.
앞서 명시된 스토어가 배열로 처리되었다면, 값도 순서대로 배열로 받아야 합니다.
따로 기능이 있는 것은 아니고 통상적으로 매개변수 앞에
$
를 붙여서 ‘스토어의 값’이라는 의미를 부여합니다.
컴포넌트에서 사용하는 ‘스토어 자동 구독(Auto-subscription)’과는 관계가 없습니다.
두 번째 인수는 set
매개변수입니다.
콜백에서 ‘계산된 값’을 반환하면 스토어(derived
)에 반영되는데,set
매겨변수가 명시되어 있으면, 반환되는 값은 ‘구독이 모두 취소되면 실행할 함수’가 됩니다.
‘구독이 모두 취소되면 실행할 함수’가 필요하지 않은 일반적으로는
set
매개변수를 명시하지 마세요.
다음 패턴에선 이해하기 쉽게 일반 함수을 사용했지만, 보통은 화살표 함수 사용을 권장합니다.
function ($스토어) {
// 계산..
return 계산된_값
}
function ([$스토어1, $스토어2]) {
// 계산..
return 계산된_값
}
function ($스토어, set) {
// 계산..
set(계산된_값)
return 구독이_모두_취소되면_실행할_함수
}
<script>
import Derived from './Derived.svelte'
let toggle = true
</script>
<button on:click={() => toggle = !toggle}>
Toggle
</button>
{#if toggle}
<Derived />
{/if}
<script>
import { count, double, total, initialValue } from './store.js'
console.log(initialValue)
console.log($count, $double)
total.subscribe($total => {
console.log($total)
})
</script>
<button on:click={() => $count += 1}>
Click!
</button>
<h1>total: {$total}</h1>
<h2>count: {$count}</h2>
<h2>double: {$double}</h2>
<h2>count+1(after 1s): {$initialValue}</h2>
import { writable, derived } from 'svelte/store'
export let count = writable(1)
export let double = derived(count, $count => $count * 2)
export let total = derived(
[count, double],
([$count, $double], set) => {
console.log('total 구독자가 1명 이상일 때!')
set($count + $double)
return () => {
console.log('total 구독자가 0명일 때...')
}
}
)
export let initialValue = derived(
count,
($count, set) => {
setTimeout(() => set($count + 1), 1000)
},
'최초 계산 중...'
)
구독하지 않고도 스토어의 값을 얻을 수 있습니다.
<script>
import { get } from 'svelte/store'
import { count, double, user } from './store.js'
console.log(get(count))
console.log(get(double))
console.log(get(user))
</script>
import { writable, readable, derived } from 'svelte/store'
export let count = writable(1)
export let double = derived(count, $count => $count * 2)
export let user = readable({
name: 'Heropy',
age: 85,
email: 'thesecon@gmail.com'
})
스토어 객체의 메소드(set
, update
, subscribe
)가 포함된 객체를 ‘커스텀 스토어’라고 합니다.
다른 속성이나 메소드를 사용할 수 있는 장점이 있습니다.
스토어의 수동/자동 구독을 위해서 subscribe
메소드는 필수로 포함해야 합니다!
import { writable } from 'svelte/store'
const store = writable(7)
export let customStore = {
subscribe: store.subscribe,
a: () => {},
b: () => {},
x: 'abc',
y: 123
}
<script>
import { customStore } from './store.js'
// Auto-subscription
console.log($customStore) // 7
</script>
<script>
import { fruits } from './fruits.js'
let value
</script>
<input bind:value />
<button on:click={() => fruits.setItem(value)}>
Add fruit!
</button>
<button on:click={() => console.log(fruits.getList())}>
Log fruit list!
</button>
<ul>
{#each $fruits as {id, name} (id)}
<li>{name}</li>
{/each}
</ul>
import { writable, get } from 'svelte/store'
const _fruits = writable([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
export let fruits = {
..._fruits,
getList: () => get(_fruits).map(f => f.name),
setItem: (name) => _fruits.update(f => {
f.push({
id: f.length + 1,
name
})
console.log(f)
return f
})
}
use
지시어를 사용해 연결된 요소가 생성될 때 호출할 함수를 지정할 수 있습니다.
이 함수를 ‘액션(Action)’이라고 합니다.
요소를 사용하는 플러그인(모듈)을 제작할 때 유용합니다.
<요소 use:함수이름></요소>
<요소 use:함수이름={인수}></요소>
연결된 함수는 다음과 같은 구조를 가집니다.
function 함수이름(요소, 인수) {
// Logic..
return {
update(인수) {}, // '인수'가 변경되면 실행됩니다.
destroy() {} // '요소'가 제거되면 실행됩니다.
}
}
<script>
let toggle = true
let width = 200
function hello(node, options = {}) {
console.log('Init hello function!')
const {
width = '100px',
height = '100px',
color = 'tomato'
} = options
node.style.width = width
node.style.height = height
node.style.backgroundColor = color
return {
update: (opts) => {
console.log('update!', opts)
},
destroy: () => {
console.log('destroy!')
}
}
}
</script>
<button on:click={() => toggle = !toggle}>
Toggle!
</button>
<button on:click={() => width += 20}>
Size up!
</button>
<div use:hello></div>
{#if toggle}
<div use:hello={{
width: `${width}px`,
color: 'royalblue'
}}></div>
{/if}
<script>
import { zoom } from './zoom.js'
</script>
<div use:zoom></div>
<div use:zoom={0.7}></div>
<style>
div {
width: 100px;
height: 100px;
background-color: tomato;
}
</style>
export function zoom(node, scale = 1.5) {
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)
}
}
}
컴포넌트 자신을 재귀적으로 포함합니다.
A 컴포넌트 내부에서 다시 A 컴포넌트를 사용하는 방법입니다.
<svelte:self />
다음의 재귀 함수와 같은 개념입니다.
무한루프에 빠지지 않도록 재귀 호출이 멈출 수 있는 조건이 붙어야 합니다.
function self() {
self()
}
self()
다음 예제의 address
데이터를 활용할 때와 같이,
같은 컴포넌트가 반복적으로 사용될 수 있는 구조에서 유용합니다.
어떤 조건으로 재귀 호출이 멈추는지 확인해 보세요!
<script>
import Address from './Address.svelte'
let address = {
label: '대한민국',
children: [
{
label: '경기도',
children: [
{ label: '수원' },
{ label: '성남' }
]
},
{
label: '강원도',
children: [
{ label: '강릉' },
{ label: '속초' }
]
}
]
}
</script>
<Address {address} />
<script>
export let address
</script>
<ul>
<li>
{address.label}
{#if address.children}
{#each address.children as address}
<svelte:self {address} />
{/each}
{/if}
</li>
</ul>
컴포넌트를 동적으로 렌더링할 때 사용합니다.this
속성에 컴포넌트 객체를 연결해야 합니다.
<script>
import MyComp from './MyComp.svelte'
</script>
<svelte:component this={MyComp} />
<script>
import Heropy from './Heropy.svelte'
import Neo from './Neo.svelte'
import Anderson from './Anderson.svelte'
let components = [
{ name: 'Heropy', comp: Heropy },
{ name: 'Neo', comp: Neo },
{ name: 'Anderson', comp: Anderson }
]
let index = 2
let selected = components[index - 1].comp
</script>
{#each components as {name, comp}, i (name)}
<label>
<input
type="radio"
value={comp}
bind:group={selected}
on:change={() => index = i + 1} />
{name}
</label>
{/each}
<svelte:component
this={selected}
{index} />
<!-- <div>{selected}</div> -->
<script>
export let index
</script>
<h2>{index}. Heropy!</h2>
<script>
export let index
</script>
<h2>{index}. Neo?</h2>
<h2>
Anderson~
</h2>
컴포넌트가 파괴(제거)될 때 같이 제거할 Window 이벤트를 추가하거나,
SSR에서 window 객체의 존재를 확인하지 않고도 이벤트를 추가할 때 사용합니다.bind
지시어를 사용해 아래 명시된 속성들과 연결할 수 있습니다.
<svelte:window
on:이벤트={핸들러}
bind:속성={데이터} />
속성 | 특성 | 설명 |
---|---|---|
innerWidth | 읽기 전용 | 뷰포트의 가로 너비 |
innerHeight | 읽기 전용 | 뷰포트의 가로 너비 |
outerWidth | 읽기 전용 | 브라우저 가로 너비 |
outerHeight | 읽기 전용 | 브라우저 가로 너비 |
online | 읽기 전용 | 네트워크 상태 |
scrollX | 쓰기 가능 | 스크롤 X좌표 |
scrollY | 쓰기 가능 | 스크롤 Y좌표 |
<script>
let key = ''
let innerWidth
let innerHeight
let outerWidth
let outerHeight
let online
let scrollX
let scrollY
// window.addEventListener('keydown', event => {
// key += event.key
// })
</script>
<svelte:window
on:keydown={e => key = e.key}
bind:innerWidth={innerWidth}
bind:innerHeight
bind:outerWidth
bind:outerHeight
bind:online
bind:scrollX
bind:scrollY />
<h1>{key}</h1>
<div>innerWidth: {innerWidth}</div>
<div>innerHeight: {innerHeight}</div>
<div>outerWidth: {outerWidth}</div>
<div>outerHeight: {outerHeight}</div>
<div>online: {online}</div>
<div class="fixed">
<input type="number" bind:value={scrollX} />
<input type="number" bind:value={scrollY} />
</div>
<div class="for-scroll"></div>
<style>
.fixed {
position: fixed;
top: 10px;
right: 10px;
}
.for-scroll {
width: 2000px;
height: 2000px;
}
</style>
document.head
를 통해 정보 요소(META, LINK..)를 삽입하거나,document.body
에 이벤트를 추가할 수 있습니다.
해당 컴포넌트가 파괴(제거)될 때 같이 제거됩니다.
<svelte:head></svelte:head>
<svelte:body />
<script>
import Heropy from './Heropy.svelte'
let toggle = false
</script>
<button on:click={() => toggle = !toggle}>
Toggle!
</button>
{#if toggle}
<Heropy />
{/if}
<svelte:head>
<link rel="stylesheet" href="./main.css" />
<style>
body {
background-color: orange;
}
</style>
</svelte:head>
<svelte:body on:mousemove={e => console.log(e.clientX, e.clientY)} />
<h1>Heropy!</h1>
<svelte:options 속성={값} />
가변성(같은 메모리 주소를 참조)을 가지는 객체 타입(object, array, function..)의 특성으로 인해,
Svelte의 할당은 불필요한 반응성(Reactive, DOM 업데이트)을 가질 수 있습니다.
컴포넌트가 전달받은 Props의 데이터 불변성(Immutable)을 선언합니다.
<svelte:options immutable={true} />
<svelte:options immutable />
해당 Props의 불변성이 확인되면 Svelte는 기존 Props와 새로운 Props를 동등 연산자로 비교하고,
그 결과가 false
가 되면 반응성을 갱신합니다.
기존_Props === 새로운_Props
Fruit 컴포넌트의 옵션을 활성화하고 테스트해보세요!
<script>
import Fruit from './Fruit.svelte'
let fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
{ id: 5, name: 'Mango' },
{ id: 4, name: 'Orange' }
]
</script>
<button on:click={() => {
fruits[0] = { id: 1, name: 'Apple' }
fruits = fruits
}}>
Update!
</button>
{#each fruits as fruit (fruit.id)}
<Fruit {fruit} />
{/each}
<script>
import { afterUpdate } from 'svelte'
export let fruit
// 기존 fruit === 새로운 fruit
let updateCount = 0
afterUpdate(() => {
updateCount += 1
})
</script>
<!-- <svelte:options immutable /> -->
<div>{fruit.name}({updateCount})</div>
외부에서 컴포넌트의 데이터 혹은 함수에 접근을 허용할 때 사용합니다.
단, 허용할 데이터나 함수에 export
를 사용해야 합니다.
<svelte:options accessors={true} />
<svelte:options accessors />
Heropy 컴포넌트의 옵션을 비활성화하고 테스트해보세요!
<script>
import Heropy from './Heropy.svelte'
let heropy
function handler() {
console.log(heropy)
console.log(heropy.name)
console.log(heropy.getAge())
}
</script>
<button on:click={handler}>
Toggle!
</button>
<Heropy bind:this={heropy} />
<svelte:options accessors />
<script>
let age = 85
export let name = 'Heropy'
export function getAge() {
console.log(age)
}
</script>
<h1 on:click={getAge}>{name}!</h1>
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-spa-router는 SPA(Single Page Application)을 위한 라우터 모듈입니다.
Svelte REPL에서도 사용할 수 있습니다.
$ npm i -D svelte-spa-router
routes.js
를 생성해 다음과 같이 Route로 정의할 컴포넌트를 연결합니다.
이 컴포넌트들은 /routes
디렉터리에 생성합니다.
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
컴포넌트도 같이 추가합니다.
<script>
import Router from 'svelte-spa-router'
import routes from './routes'
import Header from './components/Header.svelte'
</script>
<Header />
<Router {routes} />
<script>
import { link } from 'svelte-spa-router'
import active from 'svelte-spa-router/active'
</script>
<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>
<style>
:global(header a.active) {
font-weight: bold;
text-decoration: underline;
}
</style>
svelte-preprocess는 다음과 같은 전/후 처리기들을 지원합니다.
Svelte Preprocess Documentation
svelte-preprocess는 요소의 src
, lang
, type
속성을 기반으로 각각의 전처리기를 자동으로 사용합니다.
<template lang="pug"></template>
<script lang="ts"></script>
<script lang="coffee"></script>
<style type="text/less"></style>
<style lang="scss"></style>
<style src="./my-style.styl"></style>
$ npm i -D rollup-plugin-svelte svelte-preprocess
import svelte from 'rollup-plugin-svelte';
import sveltePreprocess from 'svelte-preprocess';
export default {
plugins: [
svelte({
preprocess: sveltePreprocess({
// 옵션..
})
})
]
};
@snowpack/plugin-svelte를 설치하면 svelte-preprocess이 같이 설치됩니다.
$ npm i -D @snowpack/plugin-svelte
import sveltePreprocess from 'svelte-preprocess';
module.exports = {
plugins: [
['@snowpack/plugin-svelte', {
preprocess: sveltePreprocess({
// 옵션..
})
}]
]
};
<script>
// 상대 경로인 경우..
import MyComponent from '../../../components/MyComponent'
</script>
이하 각 빌드 구성을 마치면,
경로에서 다음과 같이 ~
혹은 @
를 별칭으로 사용할 수 있습니다.
<script>
import MyComponent from '~/components/MyComponent';
</script>
다음과 같이 @rollup/plugin-alias를 설치하고 rollup.config.js
를 설정합니다.
$ npm i -D @rollup/plugin-alias
import path from 'path';
import alias from '@rollup/plugin-alias';
export default {
plugins: [
alias({
entries: [
{ find: '~', replacement: path.resolve(__dirname, 'src/') },
{ find: '@', replacement: path.resolve(__dirname, 'src/') }
]
})
]
};
Snowpack에는 경로 별칭 기능이 내장되어 있습니다.
다음과 같이 구성합니다.
module.exports = {
alias: {
'~': './src',
'@': './src'
}
};
Jest를 사용해 Test 환경을 구성합니다.
Jest 24버전부터 Babel 6버전의 지원이 중단되었습니다. Jest 23버전의 설치와는 방법이 조금 다릅니다.
$ npm i -D jest @babel/core @babel/preset-env babel-core@7.0.0-bridge.0 babel-jest @testing-library/svelte jest-transform-svelte
babel-jest
: Jest를 위한 Babel을 구성.babel-core@7.0.0-bridge.0
: babel-jest
를 @babel/*
패키지에서 정상적으로 동작시키기 위해 사용.jest-transform-svelte
: Jest를 사용해 Svelte 컴포넌트를 정상적으로 동작시키기 위해 사용.Jest의 설정을 위해 jest.config.js
을 생성합니다.
혹은 package.json
에 "jest"
블록을 선언할 수도 있습니다.
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버전을 설치했습니다.
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
설정한 테스트 환경이 정상적으로 동작하는지 확인하기 위해 최소한의 코드만 작성합니다.
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-testing-library의 일부분으로 render
의 반환 값을 확인할 수 있습니다.getQueriesForElement
는 DOM Testing library Queries에서 확인할 수 있습니다.
@web/test-runner는 Snowpack 프로젝트에 권장되는 테스트 러너 입니다.
Jest보다 더 빠르고 실제 브라우저와 더 근접하게 일치하는 테스트 환경을 제공합니다.
Svelte & Snowpack 템플릿로 쉽게 시작하고 테스트하세요!
// NODE_ENV=test - Needed by "@snowpack/web-test-runner-plugin"
process.env.NODE_ENV = 'test';
module.exports = {
plugins: [require('@snowpack/web-test-runner-plugin')()],
};
https://plugins.jetbrains.com/plugin/12375-svelte/
WebStorm을 위한 Svelte 플러그인이 있네요.
WebStorm 버전에 따라 플러그인 버전도 차이가 있을 수 있습니다.
https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode
VS Code를 위한 플러그인도 있으니 확인해 보세요.
‘Svelte for VS Code’ 확장 프로그램은, 기존 ‘Svelte’ 확장 프로그램(by James Birtles)과 통합된 버전입니다.
Svelte <style>
에서 SCSS를 사용할 때,node-sass
를 설치해도 VS Code에서 다음과 같은 에러가 발생할 수 있습니다.
Cannot find any of modules: sass,node-sass ...
확장 프로그램 Svelte for VS Code의 환경설정에서,Svelte > Language-server: Runtime
옵션에 NodeJS 설치 경로를 입력하세요.
NodeJS 설치 경로는 터미널에서 다음과 같이 입력해 확인할 수 있습니다.
# for Mac
$ which node
# for Windows
$ where node
설정 완료 후 VS Code를 재부팅하세요!
For Chrome
https://chrome.google.com/webstore/detail/svelte-devtools/ckolcbmkjpjmangdbmnkpjigpkddpogn
For FireFox
https://addons.mozilla.org/en-US/firefox/addon/svelte-devtools/
Svelte 애플리케이션 디버깅을 위한 브라우저용 확장 프로그램입니다.
크롬과 파이어폭스 브라우저를 지원합니다.