예제로 배우는 Svelte와 SvelteKit - 고급편
전체 강좌 목록입니다.
** 목차 **
1. 라이프사이클 훅
Svelte는 몇 가지 함수를 이용하여 컴포넌트의 라이프사이클 이벤트 중에 코드를 실행할 수 있게 해 줍니다.
- onMount 라이프사이클 훅은 컴포넌트가 마운트 된 후에 콜백을 실행합니다. 이 시점에서 컴포넌트의 HTML은 DOM에 렌더링 되어 있습니다.
<script>
import { onMount } from 'svelte';
onMount(() => {
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
// ...
});
</script>
<canvas></canvas>
More Lifecycle Functions
-
beforeUpdate
은 Svelte가 DOM을 업데이트하기 전에 콜백을 실행합니다. -
afterUpdate
은 Svelte가 DOM을 업데이트한 후에 콜백을 실행합니다.
beforeUpdate(() => {
// ...
});
afterUpdate(() => {
// ...
});
tick
은 Svelte가 다음번에 DOM을 업데이트한 후에 바로 호출되는 콜백입니다. (보류 중인 업데이트가 없으면 즉시 실행).
await tick();
2. Slots
Svelte 컴포넌트의 내용은 slot을 통해 전달될 수 있습니다.
Svelte는 디폴트 slot, named slot, 그리고 slot을 통해 slotted 콘텐츠로 데이터를 전달하는 slot(범위가 지정된 slot이라고도 함)을 지원합니다.
- slot은 self-closing
<slot>
태그로 선언합니다. slot에는 이름이 지정될 수 있습니다. 기본 slot 내용은 자식으로 전달될 수 있습니다. named slot 내용은 HTML 요소에slot
속성을 설정하여 전달될 수 있습니다.
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
- default slot 내용은 자식으로 전달될 수 있습니다. named slot 내용은 HTML 요소에
slot
속성을 설정하여 전달될 수 있습니다.
<script>
import Layout from './Layout.svelte';
</script>
<Layout>
<h1>Todo List</h1>
<!-- … -->
<p slot="footer">
Subtracting from your list of priorities
is as important as adding to it.
</p>
</Layout>
- 컴포넌트가 named slot 내용을 제공했는지 확인할 수 있습니다.
$$slots
를 사용합니다.
{#if $$slots.footer}
<footer>
<slot name="footer" />
</footer>
{/if}
- slot props을 사용하면 컴포넌트가 slot 콘텐츠로 데이터를 전달할 수 있습니다. 데이터는 컴포넌트처럼 slot에 전달될 수 있습니다.
<script>
let quote = "Subtracting from your list…";
</script>
<blockquote>
<slot {quote} />
</blockquote>
- 데이터는
let:<name>
속성을 사용하여 변수에 바인딩 될 수 있습니다.
<script>
import Quote from "./Quote.svelte";
</script>
<Quote let:quote={quote}>
<p>Quote: {quote}</p>
</Quote>
3. Context
컨텍스트(context)를 사용하면 프롭스(props)를 통해 데이터를 계속해서 전달하지 않고 컴포넌트 간에 데이터를 공유할 수 있습니다.
컨텍스트는 컴포넌트 트리의 여러 수준에서 데이터를 공유하는 데 유용할 수 있습니다.
- Svelte에서는 svelte 패키지에서 context 함수를 import 합니다. 컴포넌트 트리를 따라 데이터를 사용하려면 setContext를 사용하여 키와 값을 설정하십시오.
<script>
import { setContext } from 'svelte';
setContext('theme', 'light');
</script>
<!-- … -->
- 부모 컴포넌트에서 컨텍스트를 읽으려면 getContext를 사용하세요.
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<!-- … -->
4. Actions
액션은 단일 엘리먼트(element)의 라이프사이클에 바인딩되는 함수입니다.
액션은 Svelte를 서드파티 라이브러리와 통합하는 데 특히 유용하게 합니다.
- 액션은 HTML 엘리먼트와 옵셔널한(optional) 값(value)을 받는 함수입니다. 엘리먼트가 마운트되면 액션이 실행됩니다.
- 값의 변경 사항을 리스닝(listening)하려면 update 함수를 return하고, 엘리먼트가 제거될 때 정리(clean up)하기 위해서는 destroy 함수를 리턴하면 됩니다.
- 액션은 use: 지시어를 사용하여 엘리먼트에 바인딩 됩니다.
<script>
export let minDate;
function datepicker(element, { minDate }) {
const pickaday = new Pikaday({
field: element,
minDate,
});
return {
update({ minDate }) {
pickaday.setMinDate(minDate);
},
destroy() {
pickaday.destroy();
},
};
}
</script>
<input use:datepicker={{ minDate, maxDate }} />
5. Special Elements
Svelte는 'special elements'를 제공하는데요.
- 현재 컴포넌트를 재귀적(recursively)으로 렌더링 하려면 svelte:self를 사용하면 됩니다.
{#each page.children as page}
<svelte:self class="child" {page} />
{/each}
- 동적으로 컴포넌트를 렌더링 하려면 svelte:componet를 사용하면 됩니다.
<script>
import Todo from './Todo.svelte';
import Completed from './Completed.svelte';
export let todo;
$: component = todo.completed
? Completed : Todo;
</script>
<svelte:component this={component} {todo} />
- 엘리먼트를 동적으로 렌더링 하려면 svelte:element를 사용하면 됩니다.
<svelte:element this={level === 1 ? 'h1' : 'h2'}>
{title}
</svelte:element>
- window 객체에 이벤트 리스너를 추가하려면 svelte:window를 사용하면 되고, 또 로컬 데이터에 값을 바인딩할 때도 svelte:window를 사용할 수 있습니다.
<script>
let innerWidth, innerHeight,
outerWidth, outerHeight,
scrollX, scrollY,
online;
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window bind:scrollY={innerWidth} />
<svelte:window bind:scrollY={innerHeight} />
<!-- … -->
- 이벤트 리스너와 바인딩하려면 svelte:body와 svelte:document를 사용하면 됩니다.
<svelte:body on:click={handleClick} />
<svelte:document on:click={handleClick} />
- document의 head 태그에 컨텐츠를 추가하고 싶으면 svelte:head를 사용하면 됩니다.
<svelte:head>
<title>Hello, world!</title>
</svelte:head>
- 컴포넌트의 compiler options을 지정하고 싶으면 svelte:options을 사용하면 됩니다.
<svelte:options immutable />
- named slot을 HTML 엘리먼트 안에서 사용하고 싶지 않으려면 svelte:fragment를 사용하면 됩니다.
<svelte:fragment slot="footer" />
Subtracting from your list of priorities
is as important as adding to it.
</svelte:fragment>
6. TypeScript
svelte에서는 <script>
태그 안에는 기본적으로 플레인 자바스크립트인데요, svelte는 typescript도 완벽하게 지원합니다.
- Typescript를 작동시키려면
<script>
태그에 lang='ts'라고 명기하면 됩니다. - 또 타입스크립트와 같이 사용하면 props와 data 또한 평범한 변수면서 타입을 가질 수 있습니다.
- 이벤트 또한 평범한 HTML 이벤트입니다.
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let task: string;
export let completed: boolean = false;
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
dispatch('checked', {
checked: target.checked,
});
}
</script>
<p>
<input
type="checkbox"
checked={completed}
on:input={handleInput}
>
{task}
</p>
7. Writable Stores
스토어(Store)는 로컬 컴포넌트 상태를 넘어 Svelte에서 상태를 관리하는 방법입니다.
쓰기 가능한(writable) 스토어는 Svelte 컴포넌트에서 쓰고 읽을 수 있는 스토어입니다.
- Svelte에서는 쓰기 가능한(writable) 스토어를 생성하기 위해 writable 함수를 import 시킵니다. 스토어를 초기화할 때 기본값을 설정할 수 있습니다.
import { writable } from 'svelte/store';
export const todos = writable([]);
-
쓰기 가능한(writable) 스토어의 값을 설정할 때 set을 사용할 수 있습니다. 또는 update와 함께 콜백함수를 사용하여 쓰기 가능한 스토어의 값을 업데이트할 수 있습니다.
-
업데이트 콜백에서 스토어 상태를 변경하기 위해 객체를 직접 수정할 수 있습니다. 또는 새로운 객체를 반환하여 스토어 상태를 업데이트할 수 있습니다.
-
스토어의 값에 접근할 때는 스토어 이름 앞에 $를 붙일 수 있습니다.
<script>
import Todo from './Todo.svelte';
import AddTodo from './AddTodo.svelte';
import { todos } from './stores.js';
todos.set([
{
task: 'Walk dog',
completed: false,
},
]);
function addTodo() {
todos.update((todos) => {
todos.push({
task: 'Read newspaper',
completed: false,
});
});
}
function checkTodo(index, completed) {
todos.update((todos) => {
return todos.map((todo, i) => {
return index === i
? { ...todo, completed }
: todo;
});
});
}
</script>
{#each $todos as todo, index}
<Todo
{...todo}
on:checked={(event) => {
checkTodo(index, event.detail.checked);
}}
/>
{/each}
<AddTodo on:add={addTodo} />
8. Custom Stores
객체가 스토어로 사용될 수 있는 유일한 조건은 subscribe 함수를 노출하는 것입니다.
Svelte의 내장된 스토어 도우미를 사용하여 사용자 정의 스토어를 만들 수 있습니다.
사용자 정의 스토어는 데이터를 캡슐화하고 상태를 관리하기 위한 특정 함수를 노출하는 데 유용합니다.
-
writable Svelte 스토어를 시작점으로 사용할 수 있습니다.
-
사용자 정의 스토어에서 노출해야 하는 함수들을 구현합니다.
-
그런 다음 해당 함수들과 subscribe를 내보냅니다.
-
Svelte 스토어는 반드시 subscribe 함수를 내보내야 합니다.
import { writable } from 'svelte/store';
const { update, set, subscribe } = writable([]);
function addTodo() {
update((todos) => {
todos.push({
task: 'Read newspaper',
completed: false,
});
});
}
function checkTodo(index, completed) {
update((todos) => {
return todos.map((todo, i) => {
return index === i
? { ...todo, completed }
: todo;
});
});
export {
addTodo,
checkTodo,
subscribe,
};
- 스토어는 템플릿에서 다른 스토어와 마찬가지로 사용할 수 있으며,
$
는 subscribe를 호출합니다.
<script>
import Todo from './Todo.svelte';
import AddTodo from './AddTodo.svelte';
import { todos } from './stores.js';
const { addTodo, checkTodo } = todos;
addTodo('Walk dog');
</script>
{#each $todos as todo, index}
<Todo
{...todo}
on:checked={(event) => {
checkTodo(index, event.detail.checked);
}}
/>
{/each}
<AddTodo on:add={addTodo} />
9. Derived Stores
기타 스토어에서 값을 도출하기 위해 파생된 스토어를 만들 수 있습니다.
파생된 스토어는 상태의 일부에 대한 구독 또는 상태의 변형된 표현을 여러 위치에서 구독하고자 할 때 유용합니다.
-
Svelte는 파생된 스토어를 생성하기 위한
derived
함수를 import합니다. -
derived
함수의 첫 번째 인수는 기존 스토어이고, 두 번째 인수는 현재 스토어 값에서 파생된 데이터를 계산하는 함수입니다.
import { writable, derived } from 'svelte/store';
export const todos = writable([]);
export const completedTodos = derived(
todos,
($todos) => {
return $todos.filter((todo) => todo.completed),
},
);
10. Readable Stores
읽기 전용 스토어는 구독은 할 수 있지만 컴포넌트에서 업데이트할 수 없는 스토어입니다.
읽기 전용 스토어는 외부 데이터에 구독(subscribing)하는 데 유용합니다.
-
svelte는 읽기 가능한 스토어를 만들기 위한 readable 함수를 제공합니다.
-
readable은 두 개의 인수를 받습니다. 첫 번째는 스토어의 기본값이고, 두 번째는 값을 최신 상태로 유지하기 위한 start 함수입니다.
-
컴포넌트가 스토어에 구독(subscribe)할 때 start 함수가 실행됩니다. 아래 코드에서는 미디어 쿼리 변경을 감시할 것입니다.
-
start 함수는 더 이상 스토어에 구독하는 컴포넌트가 없을 때 실행될 stop 함수를 반환해야 합니다.
import { readable } from 'svelte/store';
const mediaQueryList = window
.matchMedia('(prefers-color-scheme: dark)');
export const theme = readable(
mediaQueryList.matches ? 'dark' : 'light';,
function(set) {
function listener(query) {
set(query.matches ? 'dark' : 'light');
}
mediaQueryList.addEventListener(
'change',
listener,
);
return () => {
mediaQueryList.removeEventListener(
'change',
listener,
);
};
}
);
$
을 사용하여 현재 스토어 값에 액세스 할 수 있습니다.
<script>
import { theme } from './stores.js';
</script>
<section class:dark={$theme === 'dark'}>
<!-- … -->
</section>
11. SvelteKit - Filesystem과 Routing
SvelteKit은 Svelte로 FullStack 웹 응용 프로그램을 만들게 해주는 메타 프레임워크입니다.
SvelteKit은 라우팅 및 응용 프로그램을 생성하는 데 필요한 빌드 도구와 함께 제공됩니다.
- 생성된 코드를 캐시하고 일시적으로 저장하는 SvelteKit을 위한 숨겨진 폴더입니다.
.svelte-kit
- 응용 프로그램은 src 에 위치합니다.
src/
- lib 폴더는 특정 경로에 묶이지 않은 응용 프로그램 코드를 저장하는 데 사용됩니다.
src/lib/
- routes 폴더는 웹 앱의 라우팅을 담당합니다.
src/routes/
- 각 페이지에는 +page.svelte 구성 요소가 있습니다. 참고로 routes 폴더에 있는 +page.svelte 파일이 전체 홈페이지의 index.html 파일이 됩니다.
src/routes/+page.svelte
- SvelteKit은 모든 하위 항목에 레이아웃을 적용하는 레이아웃 UI와 및 스크립트(js)도 지원합니다.
src/routes/+layout.js
src/routes/+layout.svelte
- 추가 페이지를 위해서는 routes 폴더 밑에 하위 폴더를 만들면 됩니다. 아래 코드는
/about
페이지를 구성한 겁니다. 페이지 구성 요소는 페이지 스크립트 및 데이터를 가져오고 준비하기 위한 서버 스크립트와 함께 올 수 있습니다.
src/routes/about/
src/routes/about/+page.js
src/routes/about/+page.server.js
src/routes/about/+page.svelte
- lists 폴더를 만든 겁니다. lists 폴더 밑에 대괄호를 이용한 이름의 폴더가 있습니다. 대괄호를 사용하여 라우팅 매개 변수를 지정할 수 있습니다. 라우팅 매개변수를 위한 폴더에도 똑같은 +page.svelte 파일이 들어갑니다.
src/routes/lists/
src/routes/lists/+page.svelte
src/routes/lists/[id]/
src/routes/lists/[id]/+page.svelte
- 응용 프로그램의 주요 CSS 파일 및 HTML 스켈레톤입니다.
src/app.css
src/app.html
.gitignore
package.json
README.md
- svelte.config.js에는 SvelteKit에 전용 설정 파일입니다.
svelte.config.js
- SvelteKit은 Vite로 구축되었으며 이는 추가적인 Vite 플러그인을 등록할 수 있음을 의미합니다.
vite.config.js
12. SvelteKit - Pages와 Links
페이지 구성 요소는 파일 시스템에 의해 결정된 특정 URL에서 렌더링 되는 일반적인 Svelte 구성 요소입니다.
SvelteKit에서 페이지는 기본적으로 서버 측에서 렌더링 됩니다.
- 다른 페이지로 연결하려면 href 속성이 있는 일반적인 a 태그를 사용하십시오.
<-- src/routes/+page.svelte -->
<h1>Hello, word!</h1>
<a href="/about">About</a>
<-- src/routes/about/+page.svelte -->
<h1>About</h1>
-
SvelteKit은 사용자가 링크 위로 마우스를 가져갈 때 이동하려는 페이지를 미리 로드하여 응용 프로그램을 더욱 빠르게 만들 수 있습니다.
-
비활성화하려면 링크에 data-sveltekit-preload-data 속성을 설정하십시오. (모든 링크에서 비활성화하려면 body 태그에 설정하십시오.)
<h1>Hello, word!</h1>
<a
href="/about"
data-sveltekit-preload-data="off"
>
About
</a>
13. SvelteKit - Page Scripts
페이지 스크립트는 페이지 컴포넌트가 렌더링 되기 전에 데이터를 가져오거나 준비할 수 있게 합니다.
SvelteKit 앱이 처음 로드될 때 페이지 스크립트는 초기 렌더링을 위해 서버에서 실행됩니다.
이후의 방문에서는 페이지 스크립트가 클라이언트에서만 실행됩니다.
+page.js
스크립트에서load
함수를 내보내면 페이지 컴포넌트에 대한 데이터를 준비하거나 가져오는 데 사용할 수 있습니다.load
함수는 비동기 함수일 수 있습니다.
import { fetchLists } from './api.js';
export async function load() {
return {
lists: await fetchLists(),
};
}
- 페이지 스크립트에서 가져온 데이터는 페이지 컴포넌트의
data
props에 전달됩니다.
<script>
export let data;
</script>
<h1>Lists</h1>
<ul>
{#each data.lists as list}
<li>
<a href={`/lists/${list.id}`}>
{list.name}
</a>
</li>
{/each}
</ul>
load
함수는 route 매개변수 및 기타 데이터에 액세스 하기 위해context
인자를 받습니다. 페이지 스크립트는 오류를 throw 하고 HTTP 응답 코드를 수정할 수 있습니다.
import pages from "./pages.json";
import { error } from '@sveltejs/kit';
export function load({ params }) {
const page = pages
.find((page) => page.slug === params.page);
if (! page) {
throw error(404, 'Page not found');
}
return { page };
}
load
함수는 JSDoc 또는 TypeScript를 사용하여 타입을 지정할 수 있습니다.
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
// …
}
import type { PageLoad } from "./$types";
export function load({ params }: PageLoad) {
// …
}
data
props 또한 JSDoc 또는 Typescript로 타입을 지정할 수 있습니다.
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
14. SvelteKit - Server Scripts
서버 스크립트는 페이지 컴포넌트가 렌더링 되기 전에 데이터를 가져오거나 준비하는 데 사용됩니다.
당연히 서버 스크립트는 서버에서만 실행됩니다.
+page.server.js
스크립트에서load
함수를 내보내어 페이지 컴포넌트를 준비하거나 데이터를 가져올 수 있습니다.load
함수는 비동기 함수일 수 있습니다.
import { getListsFromDatabase } from './db.js';
/** @type {import('./$types').PageServerLoad} */
export async function load() {
return {
lists: await getListsFromDatabase(),
};
}
- 페이지 스크립트에서의 데이터는 페이지 컴포넌트의
data
속성으로 전달됩니다.
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>
<h1>Lists</h1>
<ul>
{#each data.lists as list}
<li>
<a href={`/lists/${list.id}`}>
{list.name}
</a>
</li>
{/each}
</ul>
15. SvelteKit - Layouts
레이아웃 컴포넌트는 페이지 주변에 렌더링 되며 페이지 내용을 슬롯에 넣습니다.
레이아웃은 서브트리의 모든 페이지에 적용됩니다.
- 레이아웃은 해당 페이지와 모든 하위 항목에 대해 사용됩니다. 아래 레이아웃은 src/routes/+page.svelte, src/routes/about/+page.svelte 등에 적용됩니다. 그리고 페이지는 당연히 slot이 위치한 곳에 렌더링 됩니다.
// src/routes/+layout.svelte
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/lists">Lists</a>
</nav>
<slot></slot>
- 페이지 구성 요소 및 페이지 스크립트와 마찬가지로 레이아웃도 레이아웃 구성 요소에 데이터를 전달하기 위한 레이아웃 스크립트로 동반될 수 있습니다.
// src/routes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
// …
}
src/routes/+layout.svelte
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>
- 레이아웃 데이터는 페이지 데이터와 병합되며 페이지 구성 요소에서도 사용할 수 있습니다.
// src/routes/+page.svelte
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>
16. SvelteKit - Form Actions
폼 액션은 SvelteKit에서 폼 제출용 서버 엔드 포인트를 제공합니다.
액션은 폼 제출 시 호출되므로 JavaScript가 비활성화된 경우에도 액션은 여전히 실행됩니다.
- 액션은 +page.server.js 파일에서 내보내집니다. 액션은 요청을 통해 전달된 폼 데이터에 액세스 할 수 있습니다.
// src/routes/newsletter/+page.server.js
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const data = { email: formData.get('email') };
await fetch('https://mailcoach.app/api/…', {
method: 'POST',
body: JSON.stringify(data),
});
},
};
- Svelte 컴포넌트에서 폼을 통해 액션을 호출할 수 있습니다. 페이지의 기본 액션을 호출할 때는 action 속성이 필요하지 않습니다.
// src/routes/newsletter/+page.svelte
<form method="POST">
<label>Email</label>
<input type="email">
</form>