스벨트 & 스벨트 킷

Sat Dec 18 2021 · 21 min read
  • Front-end
  • Svelte
  • SvelteKit
cover image

스벨트

기본 구조

스벨트 파일는 .svelte 확장자를 가집니다. 스벨트 파일에는 scirpt, style, 마크업 이렇게 세 가지 블록이 존재합니다. 이 세 가지 섹션은 필수가 아닌 선택요소입니다.

<script>
// 로직이 위치하는 부분
</script>
<!-- 0개 이상의 마크업 태그가 위치하는 부분 -->
<style>
/* 스타일이 위치하는 부분 */
</style>

<script>

<script> 블록은 자바스크립트(혹은 타입스크립트)를 작성하는 부분입니다. 페이지의 로직이 위치하며 일반적으로 최상위에 위치합니다. 스크립트 블록에는 네 가지 규칙이 있습니다.

  1. 컴포넌트 prop을 생성하는 export
  2. ‘리액티브’한 할당
  3. 리액티브를 위한 $: 구문
  4. svelte/store를 편하게 사용하기 위한 $ 접두사

<script> 태그에 context="module" 속성값이 추가되면 처음 한 번만 실행됩니다.

<style>

style 블록은 스타일을 작성하는 부분입니다. 스타일을 전역으로 적용하고 싶다면 :global(...) 수정자를 사용합니다. 만약 키프레임을 전역으로 적용하고 싶다면 이름 앞에 -global-을 붙여줍니다. <style> 블록은 문서에 한 개의 블록만 존재해야 합니다. 마크업 태그 안에는 존재할 수 있지만, 스벨트에서 지원하는 전역 스타일 같은 기능이 지원되지 않고 일반 HTML 문서와 같이 처리됩니다.

<style>
/* 버튼 스타일 예시 */
button {
background-color: gray;
}
/* 버튼 전역 스타일 예시 */
:global(button) {
color: white;
}
/* 키프레임 전역 예시 */
@keyframes -global-my-animation-name {...}
</style>

변수

기본적인 선언 및 사용

<script>
let count = 0;
</script>
<div>
<p>count : {count}</p>
</div>

변수의 선언은 2번째 줄과 같이, <script> 태그 안에서 일반 자바스크립트 변수 선언하듯이 선언합니다. 사용하는 방법은 10번째 줄관 같이 {변수명}과 같이 사용합니다.

export를 사용한 변수

<script>
export let foo;
export const bar = 'readonly';
// Values that are passed in as props
// are immediately available
console.log({ foo });
</script>

만약 변수 선언할 때 export를 붙였다면, 해당 변수는 이 컴포넌트를 사용한 곳에서 읽을 수 있게 됩니다. 자바스크립트 모듈에서 export를 붙인 것과 동일합니다.

만약 변수 선언 시 let이 아닌 const를 사용하였다면, 이 변수는 읽기 전용이 됩니다.

반응형 변수

<script>
let count = 0;
function addOne() {
count = count + 1;
}
</script>
<div>
<p>count : {count}</p>
<button on:click={addOne}>add one</button>
</div>

스벨트는 리액트와 다르게 useState와 같은 메서드를 이용하여 선언을 할 필요가 없습니다. 그냥 일반 변수처럼 선언한 후, 핸들링하는 함수를 만들고 사용하기만 하면 됩니다. 4-6번째 줄과 같이 <script> 태그 안에 1을 더해주는 함수 addOne()을 만들어 주고 11번째 줄의 <button> 태그에 onclick 이벤트로 달아주면 됩니다.

반응형 변수 테스트
반응형 변수 테스트

$: 구문을 이용하여 반응형 업데이트

<script>
let a = 0;
let b = 0;
function add(a, b) {
return a + b;
}
$: sum = add(a, b);
</script>
<div>
<input type="number" bind:value={a} />
+
<input type="number" bind:value={b} />
=
<span>{sum}</span>
</div>

위 예시는 ab를 더하는 코드입니다. input으로 a값과 b값을 받아서 값을 저장한 후 더해서 sum으로 보여줍니다. bind:value는 일단 모르셔도 됩니다. input의 value를 뒤의 변수와 연결시켜주는 역할을 한다는 정도만 이해하시면 됩니다.여기서 $: 다음에 나오는 변수들은 스벨트에서 자동으로 관찰(Observe)하고 있다, 변경이 감지되면 즉각적으로 뒤에 있는 코드 add(a, b)를 실행하게 됩니다.

$: 구문을 이용한 반응형 변수 테스트
$: 구문을 이용한 반응형 변수 테스트

function add(b) {
return a + b;
}
$: sum = add(b);

만약 코드 중 일부가 위와 같이 바뀌면 어떻게 될까요? $: 뒤에 변수가 b만 존재하므로 a의 값이 변경될 때는 sum이 업데이트되지 않습니다. 오로지 b의 값이 변경될 때만 sum값이 업데이트되게 됩니다.

그럼 $:를 안 쓰면 어떻게 될까요?

<script>
let a = 0;
let b = 0;
</script>
<div>
<input type="number" bind:value={a} />
+
<input type="number" bind:value={b} />
=
<span>{a + b}</span>
</div>

$: 구문을 사용했을 때와 크게 다르지 않습니다. $: 구문을 사용하면 별도의 변수로 선언할 수 있고, 원하는 변수가 변경됐을 때만 업데이트 하게 만들 수 있다는 장점이 있을 것으로 보입니다.

$ 접두사를 이용한 svelte/store 사용

svelte/store는 다른 컴포넌트에서 해당 변수를 반응형을 유지하면서 쉽게 접근할 수 있게 만들어주는 svlete 모듈입니다.

count.js
import {writable} from 'svelte/store';
export const count = writable(0);
index.svelte
<script>
import {count} from './count.js'
function addOne() {
$count = $count + 1;
}
</script>
<div>
<a href="/test">move</a>
<br />
<button on:click={addOne}>count is {$count}</button>
</div>
test.svelte
<script>
import {count} from './count.js'
</script>
<div>
<a href="/">back</a>
<br />
<span>count is {$count}</span>
</div>

위의 예제 코드에서는 count.js에서 변수를 선언한 후, index.sveltetest.svelte에서 사용하는 예시입니다. store된 변수에 접근할 때는 예시처럼 변수명 앞에 $ 접두사를 붙여야 합니다.

$ 접두사를 이용한 store 테스트
$ 접두사를 이용한 store 테스트

제어문

스벨트 템플릿 문법에서 흐름을 제어하는 방법은 if, each, await, key 이렇게 4가지가 있습니다. 이 템플릿들의 시작은 {## ...}이며, 끝은 {/ ...}로 표시합니다.

{#if ...}

{## if 표현식}
<!-- ... -->
{:else if 표현식}
<!-- ... -->
{:else}
<!-- ... -->
{/ if}

사용 방법은 일반 if문과 동일합니다. 중괄호 안의 #이나 :, /을 주의해서 사용해주시면 됩니`다.

{#each ...}

{#each expression as name, index (key)}
<!-- ... -->
{/each}

자바스크립트의 Array.prototype.map()과 사용법이 유사합니다. 첫 번째 인자로 배열 내부의 변수의 이름을, 두 번째 인자로 해당 인자의 index값을 받습니다. (key)는 Svelte가 요소의 변경을 감지하고 수정할 수 있도록 도와주는 키값입니다. React에서는 이러한 반복문 안에서 컴포넌트들에게 key값을 넘겨주는 것이 필수이지만, Svelte에서는 에러가 나지 않습니다.

{#each items as {id, ...rest}}
<li>
<span>{id}</span>
<MyComponent values={rest}/>
</li>
{/each}

또한 이렇게 내부에서 구조 분해 할당이나 나머지 연산자를 사용할 수도 있습니다.

{#each todos as todo}
<p>{todo.text}</p>
{:else}
<p>No tasks today!</p>
{/each}

값이 없을 경우 {:else} 블록을 이용하여 해당 내용을 렌더링 할 수도 있습니다.

<script>
let c = [];
function addValue() {
const copiedC = c.slice();
copiedC.push(Math.ceil(Math.random()*10));
c = copiedC;
}
function cleanAll() {
c = [];
}
</script>
<div>
<button on:click={addValue}>add value</button>
<button on:click={cleanAll}>clear</button>
<ul>
{#each c as item, index (index)}
<li>{index} : {item}</li>
{:else}
<li>list is empty.</li>
{/each}
</ul>
</div>

간단한 반복문의 예제 코드입니다.

반복문 테스트
반복문 테스트

{#await ...}

<!-- 전체 제어문 -->
{#await expression}
<!-- 보류 -->
{:then name}
<!-- 성공 -->
{:catch name}
<!-- 실패 -->
{/await}
<!-- 보류 상태와 성공 상태를 한 문장으로 처리한 제어문 -->
{#await expression then name}
<!-- 성공 시에만 -->
{/await}
<!-- 보류 상태와 실패 상태를 한 문장으로 처리한 제어문 -->
{#await expression catch name}
<!-- 실패 시에만 -->
{/await}

Svelte는 비동기 문법에 대한 제어문도 지원합니다. React에서는 동일한 기능을 구현하려면 useState로 새로운 상태 객체를 만들어야 했지만, Svelte에서는 문법적으로 지원합니다. Promisepending(보류), fulfilled(성공), rejected(실패) 이렇게 3가지 상태를 가질 수 있습니다. 보류와 성공 상태를 하나로 묶거나, 보류와 실패 상태를 하나로 묶어 처리할 수도 있습니다. 이 제어문은 SSR 모드에서도 사용할 수 있는데, 이때 보류 상태만 서버에서 렌더링됩니다.

<script>
let resolve_promise = resolvePromise();
let reject_promise = rejectPromise();
async function resolvePromise() {
return Promise.resolve('Hello svelte!');
}
async function rejectPromise() {
return Promise.reject({message: "내부 에러"});
}
</script>
<div>
{#await resolve_promise}
<p>대기 중...</p>
{:then value}
<p>값: {value}</p>
{:catch error}
<p>처리 실패: {error.message}</p>
{/await}
<hr>
{#await reject_promise catch error}
<p>처리 실패: {error.message}</p>
{/await}
</div>

간단한 예시코드입니다. resolve만 하는 Promise 함수(resolvePromise)와 reject만 하는 Promise 함수(rejectPromise)를 렌더링하는 코드입니다.

비동기 구문 테스트
비동기 구문 테스트

{#key ...}

{#key expression}
<!-- ... -->
{/key}

키 블록은 표현식의 값이 변경될 때 내용을 지우고 재생성합니다. 값이 변경될 때 요소의 애니메이션을 재생하고 싶을 때 유용합니다.

기타 블록들

텍스트 표현식

{표현식}

텍스트 표현식에는 자바스크립트 표현식도 포함될 수 있습니다. 다만 정규식(RegExp)을 사용할 때는 괄호로 묶어서 사용해야 합니다.

<div>{(/^[A-Za-z ]+$/).test(value) ? x : y}</div>

주석

<!-- 이 블록이 주석입니다. -->

Svetle의 주석은 HTML 주석과 동일합니다. 무시하고 싶은 오류나 경고를 비활성화 할 때 사용하는 svelte-ignore을 추가해 주고 싶을 때도 이 주석 블록을 사용합니다.

{@html ...}

{@html expression}

{@html ...} 표현식은 React의 dangerouslySetInnerHTML와 유사합니다. Svelte는 텍스트 표현식에서 <>와 같은 문자를 제거하고 표시하지만 이 표현식을 사용했을 경우에는 그렇지 않습니다. 따라서 XSS 공격에 매우 취약하므로 사용에 주의해야 합니다. 또한 이 내부에 오는 코드들은 Svelte가 컴파일하지 않습니다.

{@debug ...}

{@debug}
{@debug var1, var2, ..., varN}

console.log(...)를 사용하는 대신 이 표현식을 사용합니다. 만약 devtools가 열려있다면 코드 실행을 중단하는 중단점 역할을 합니다. 이 블럭은 쉼표로 구별된 변수명만 허용되며, 임의의 표현식을 사용할 수 없습니다.

<!-- 컴파일 됨 -->
{@debug user}
{@debug user1, user2, user3}
<!-- 컴파일 되지 않음 -->
{@debug user.firstname}
{@debug myArray[0]}
{@debug !isReady}
{@debug typeof user === 'object'}

컴포넌트

Svelte의 컴포넌트는 약간 이질적일 수 있습니다.

List.svelte
<script>
export let textContent = "";
</script>
<li>{textContent}</li>
index.svelte
<script>
import List from './List.svelte';
let list = ['a', 'b', 'c', 'd', 'e'];
</script>
<ul>
{#each list as textContent}
<List {textContent} />
{/each}
</ul>

Svelte에서는 export를 선언한 변수가 속성 혹은 prop으로 사용됩니다. 변수에 기본값 설정도 가능합니다.

Info.svelte
<script>
export let name;
export let version;
export let speed;
export let website;
</script>
<p>
<code>{name}</code> 패키지의 속도는 {speed} 빠릅니다.
<a href="https://www.npmjs.com/package/{name}">npm</a>에서 {version} 버전을 받을 수 있고
<a href={website}>여기서 더 자세한 내용을 확인</a>할 수 있습니다.
</p>
index.svelte
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: '엄청나게',
website: 'https://svelte.dev'
};
</script>
<Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>

이렇게 여러개의 속성값을 넘길 때는 하나하나 작성해도 되지만

<Info {...pkg}/>

이렇게 구조분해할당을 사용하여 값을 넘겨줘도 됩니다.

요소의 이벤트 핸들링

on:이벤트명

on:eventname={handler}
on:eventname|modifiers={handler}

on: 지시문을 사용하여 태그의 이벤트를 제어할 수 있습니다.

<script>
let count = 0;
function handleClick(event) {
count += 1;
}
</script>
<div>
<!-- 변수 선언 후 사용 -->
<button on:click={handleClick}>
count: {count}
</button>
<!-- 인라인으로 사용 -->
<button on:click={() => count += 1}>
count: {count}
</button>
</div>

함수를 만들어 함수명을 명시하거나, 성능 저하 없이 화살표 함수를 이용하여 인라인으로 사용할 수도 있습니다.

또한 다음과 같은 수정자(modifier)를 사용할 수도 있습니다.

  • preventDefault - 핸들러 실행 전에 event.preventDefault() 호출.
  • stopPropagation - event.stopPropagation() 호출하여 이벤트 전파 방지.
  • passive - 터치/휠 이벤트에 대한 스크롤 성능 향상.
  • nonpassive - pssive: false를 명시적으로 설정.
  • capture - 캡처링 단계에서 핸들러 실행.
  • once - 핸들러가 처음 실행된 후 제거.
  • self - event.target일 경우에만 핸들러 실행
  • trusted -event.isTrustedtrue일 경우에만 실행.
<form on:submit|preventDefault={handleSubmit}>
<!-- the `submit` event's default is prevented, so the page won't reload -->
</form>

수정자는 위와 같이 |를 붙인 후 사용하며, 여러개의 수정자를 사용할 수 있습니다. 여러개의 수정자를 사용할 경우도 on:click|once|capture={...}처럼 각 수정자 사이를 |로 연결하여 사용합니다.

컴포넌트의 이벤트 핸들링

컴포넌트에서 이벤트를 전달하는 방법에는 두 가지가 있습니다. 첫 번째는 이벤트 핸들러를 직접 전달하여 사용하는 방법이고, 두 번째는 createEventDispatcher를 사용하는 방법입니다.

직접 전달

Button.svelte
<script>
import { createEventDispatcher } from 'svelte';
export let click;
const dispatch = createEventDispatcher();
</script>
<button on:click={() => click('test')}> click! </button>
index.svelte
<script>
import Button from './Button.svelte';
function callbackFunction(detail) {
alert(`Notify fired! Detail: ${detail}`);
}
</script>
<ul>
<Button click={callbackFunction} />
</ul>

전달받은 함수를 컴포넌트에서 직접 호출하는 방법입니다.

createEventDispatcher

Button.svelte
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('notify', 'detail value')}>
click!
</button>
index.svelte
<script>
import Button from './Button.svelte';
function callbackFunction(event) {
alert(`Notify fired! Detail: ${event.detail}`);
}
</script>
<ul>
<Button on:notify={callbackFunction} />
</ul>

createEventDispatcher 함수를 이용하여 함수를 받아오는 방식입니다. dispatch(type, detail)로 함수를 호출하며 on:이벤트명에서 이벤트명이 type에 해당되고, detailevent.detail의 값으로 들어가게 됩니다.

스벨트 킷

Endpoints

스벨트 킷의 서버 사이드 기능 중 하나인 엔드포인트에 대해서 설명합니다.

[slug].json.ts
import db from '$lib/database';
import type {RequestHandler} from '@sveltejs/kit';
import type {Locals} from '$lib/types';
export const get: RequestHandler<Locals> = ({params}) => {
// the `slug` parameter is available because this file
// is called [slug].json.ts
const {slug} = params;
const article = await db.get(slug);
if (article) {
return {
body: {
article,
},
};
}
};

스벨트 킷은 .js.ts 파일을 엔드포인트로 인식합니다. /todos/index.json.ts와 같은 엔드포인트는 index가 생략되어 /todos.json와 같이 접근하며, 동적 경로는 [id].ts와 같은 형식으로 사용할 수 있습니다.

함수명을 get, post, patch와 같이 HTTP 메서드명과 동일하게 작성해줍니다. delete의 경우 자바스크립트에서 이미 사용중인 예약어이기 때문에 del로 축약하여 사용합니다.

Request

[slug].json.ts
import db from '$lib/database';
import type {RequestHandler} from '@sveltejs/kit';
import type {Locals} from '$lib/types';
export const get: RequestHandler<Locals> = ({params}) => {
// the `slug` parameter is available because this file
// is called [slug].json.ts
const {slug} = params;
const article = await db.get(slug);
if (article) {
return {
body: {
article,
},
};
}
};

request에는 params 말고도 body, locals, method, host, path, query, headers, rawBody 객체가 존재합니다.

  • params: Record<string, string> - [slug].json.ts에서 slug와 같이 변수로 설정한 페이지 동적 경로.
  • body: ParameterizedBody<Body>- 요청 바디
  • locals: Locals - ./src/hooks.ts에서 저장한 데이터
  • method : string - HTTP 메서드 종류 (GET, POST, PATCH 등)
  • host: string - 요청 호스트 도메인
  • path: string - 요청 경로
  • query: URLSearchParams - URL 쿼리
  • headers: RequestHeaders - HTTP 헤더 데이터
  • rawBody: RawBody - Uint8Array 타입의 가공되지 않은 요청 바디

body의 파싱은 아래 내용을 따릅니다.

  • content-type이 text/plain일 경우 string 형식으로 변환됩니다.
  • content-type이 application/json일 경우 JSONValue 형식(object, Array, primitive)으로 변환됩니다.
  • content-type이 application/x-www-form-urlencoded 또는 multipart/form-data일 경우 읽기 전용인 FormData 객체로 변환됩니다.
  • 다른 모든 경우 Uint8Array로 변환됩니다.

Response

[slug].json.ts
import db from '$lib/database';
import type {RequestHandler} from '@sveltejs/kit';
import type {Locals} from '$lib/types';
export const get: RequestHandler<Locals> = ({params}) => {
// the `slug` parameter is available because this file
// is called [slug].json.ts
const {slug} = params;
const article = await db.get(slug);
if (article) {
return {
body: {
article,
},
};
}
};

엔드포인트의 반환 객체는 status, header, body 이렇게 세 가지 속성으로 구성됩니다. Body에 객체가 들어갈 경우 headers의 Content-Type에 ‘application/json’을 명시해주지 않아도 자동으로 Json 객체로 변환됩니다.

  • status?: number - HTTP 상태 코드를 명시 (200, 301, 400, 404 등)
  • headers?: ResponseHeaders - HTTP 헤더 데이터 (Content-Type 등)을 명시
  • body?: Body - 응답 바디 데이터

References