이전 글에서는 Svelte의 기초 문법에 대해서 소개했습니다.

이 글에선 이전에 미처 다루지 못한 문법에 대해서 다루겠습니다.

다룰 내용

  • 동적으로 class 할당하기
  • bind로 입력값 연결하기
  • 자식 컴포넌트에 bind 사용하기
  • bind:this로 참조 얻어오기
  • slot 사용하기
  • 특별한 svelte: 엘리먼트
  • 후기

동적으로 class 할당하기

웹 개발을 하다보면 특정 상태에 따라 스타일을 다르게 지정해야 하는 경우가 빈번합니다.

저는 보통 특정 상태를 스타일링하는 class를 동적으로 넣어서 구현합니다.

다음은 버튼을 클릭해서 toggled 상태를 변경하고, 자바스크립트의 삼항연산자로 상태에 따라 클래스를 할당하는 예시입니다.

<script>
let toggled = false

const toggle = () => toggled = !toggled
</script>

<style>
.toggled {
  background-color: #ffff00;
}
</style>

<button on:click={toggle}>toggle</button>
<p class={toggled ? 'toggled' : ''}>yellow, if toggled</p>

다음처럼 축약해서 쓸 수도 있습니다.

<p class:toggled={toggled === true}>yellow, if toggled</p>

상태 변수가 클래스와 같다면 더 축약 가능합니다.

<p class:toggled>yellow, if toggled</p>

3가지 표현 모두 toggled 값의 상태에 따라 동적으로 클래스가 할당되는걸 확인할 수 있습니다.

bind로 입력값 연결하기

웹 개발을 진행하면 보여주는 것 뿐만 아니라, 로그인, 회원가입 등 사용자들의 입력을 받는 경우가 있습니다.

svelte에선 bind라는 기능을 이용해서 입력값을 변수와 연결할 수 있습니다.

간단하게 텍스트 인풋을 연결하겠습니다.

<script>
let name = ''

</script>

<input type="text" bind:value={name}>
<p>name is {name}</p>

checkbox 타입의 경우 bind:group을 통해 묶을 수 있습니다.

<script>
let skills = []
</script>

<fieldset>
  <legend>다음 중 능숙한 것을 모두 선택하세요.</legend>

  <label>
    <input type="checkbox" bind:group={skills} value="Git">Git
  </label>
  <label>
    <input type="checkbox" bind:group={skills} value="TDD">TDD
  </label>
  <label>
    <input type="checkbox" bind:group={skills} value="DDD">DDD
  </label>
  <label>
    <input type="checkbox" bind:group={skills} value="OOP">OOP
  </label>
  <label>
    <input type="checkbox" bind:group={skills} value="FP">FP
  </label>
</fieldset>

<p>능숙한 것들: {skills}</p>

마찬가지로 radio 타입도 그룹화가 가능합니다.

<script>
let favorite = ''
</script>

<fieldset>
  <legend>가장 관심있는 분야를 선택하세요.</legend>

  <label>
    <input type="radio" bind:group={favorite} value="풀스택">풀스택
  </label>
  <label>
    <input type="radio" bind:group={favorite} value="프론트엔드">프론트엔드
  </label>
  <label>
    <input type="radio" bind:group={favorite} value="백엔드">백엔드
  </label>
</fieldset>

<p>관심분야: {favorite}</p>

input 태그 뿐만 아니라 select, textarea 등의 태그에도 사용 가능합니다.

<script>
let framework = ''
</script>

<select bind:value={framework}>
  <option value="Vue">Vue</option>
  <option value="Angular">Angular</option>
  <option value="React">React</option>
  <option value="Svelte">Svelte</option>
</select>

<p>선택한 프레임워크는 {framework}입니다.</p>

자식 컴포넌트에 bind 사용하기

만약 자식 컴포넌트에서 부모의 값을 변경하려면 어떻게 해야할까요?

기본적으로 데이터는 부모에서 자식 컴포넌트로 단반향 흐름을 가집니다.

하지만 재사용 가능한 input 컴포넌트를 만들거나, 자식에서 특정 상태를 제어하는 경우가 있습니다.

이런 경우에도 bind를 사용해서 구현할 수 있습니다.

bind 기능은 vue의 v-model과 유사하고 양방향 바인딩(two-way-binding)이라고도 부릅니다.

간단하게 로그인 폼 컴포넌트로 예시를 구현해보겠습니다.

LoginInput.svelte를 만들었고 childFormData를 export했습니다.

<script>
export let childFormData = {
  id: '',
  password: ''
}
</script>

<fieldset>
  <legend>로그인</legend>
  <label>
    <input type="text" bind:value={childFormData.id}>아이디
  </label>
  <label>
    <input type="password" bind:value={childFormData.password}>비밀번호
  </label>
</fieldset>

다음은 bind로 위 컴포넌트를 사용하는 방법입니다.

bind:childFormData={formData} 문법으로 사용했고, childFormData가 바뀌면 부모의 formData도 변경됩니다.

childFormDataformData에 할당한다는 개념으로 접근하면 이해가 쉬울거라고 생각합니다.

<script>
import LoginInput from './LoginInput.svelte'

let formData = {
  id: '',
  password: ''
}
</script>

<h1>로그인 페이지</h1>
<LoginInput bind:childFormData={formData}></LoginInput>

아이디 입력값 : {formData.id}
비밀번호 입력값 : {formData.password}

bind:this로 참조 얻어오기

bind:this 기능을 사용하면 엘리먼트나 컴포넌트에 대한 참조를 얻을 수 있습니다.

다음 예시처럼 엘리먼트에 대한 참조를 얻을 수 있고, 이벤트리스너 등을 붙일 수 있습니다.

onMount는 일종의 라이프사이클 훅인데, 실제 컴포넌트가 DOM에 마운트된 이후에 실행됩니다.

<script>
import { onMount } from 'svelte'

let element

onMount(() => {
  console.log(element)
  console.log(element instanceof HTMLElement) // true
  element.addEventListener('click', () => alert('clicked!'))
})
</script>

<div bind:this={element} id="container">container element</div>

bind:this를 엘리먼트가 아닌 컴포넌트에 사용한다면, 해당 컴포넌트 객체의 참조를 얻을 수 있습니다.

vue의 ref와 거의 비슷한 동작을 하는군요.

<script>
import { onMount } from 'svelte'
import MyComponent from './MyComponent.svelte'

let component

onMount(() => {
  console.log(component) // MyComponent 객체
})
</script>

<MyComponent bind:this={component} />

Slot 사용하기

slot 기능을 사용하면 자식 컴포넌트가 부모로부터 콘텐츠를 주입받을 수 있습니다.

여기서 콘텐츠란 다른 컴포넌트나 html 엘리먼트, 텍스트 등을 의미합니다.

vue의 slot, react의 children과 거의 비슷한 개념입니다.

우선 간단하게 Link.svelte 컴포넌트를 만들어보겠습니다.

// Link.svelte
<a href="https://svelte.dev">
  <slot>디폴트 콘텐츠</slot>
</a>

만든 컴포넌트를 사용하겠습니다.

<script>
import Link from './Link.svelte'

</script>

<Link>Link to Svelte!</Link>

최종적으로 다음과 같이 렌더링됩니다.

<a href="https://svelte.dev">Link to Svelte!</a>

만약 다음과 같이 slot 영역에 아무것도 넣지 않는다면,

<Link></Link>

다음과 같이 미리 정의해둔 기본 콘텐츠가 렌더링됩니다.

<a href="https://svelte.dev">디폴트 콘텐츠</a>

텍스트 뿐만 아니라 다른 html 태그도 주입할 수 있습니다.

<Link>Link to Svelte!<i class="link" /></Link>

이름을 가진 slot

다음 예시처럼 이름을 부여해서 두개 이상의 slot도 사용할 수 있습니다.

// Page.svelte
<div>
  <slot name="header">헤더를 주입해주세요!</slot>
  <section>
    섹션 영역
  </section>
  <slot name="footer">푸터를 주입해주세요!</slot>
</div>

이름이 가진 slot을 사용하려면 다음처럼 slot="slotName" 형식으로 사용하면 됩니다.

<script>
import Page from './Page.svelte'

</script>

<Page>
  <header slot="header">주입하는 헤더</header>
  <footer slot="footer">주입하는 푸터</footer>
</Page>

최종적으로 다음과 같이 렌더링됩니다.

<div>
  <header slot="header">주입하는 헤더</header>
  <section>섹션 영역</section>
  <footer slot="footer">주입하는 푸터</footer>
</div>

slot props

slot props 기능을 이용하면 자식 컴포넌트의 데이터를 부모에서 사용할 수 있습니다.

좀 전에 만들어둔 Page.svelte를 약간 변형해보겠습니다.

header slotusername을 마치 props 사용하듯이 전달했습니다.

// Page.svelte
<script>
  const username = 'ashnamuh'
</script>

<div>
  <slot name="header" username={username}>헤더를 주입해주세요!</slot>
  <section>
    제 이름은 {username}입니다.
    저는 개발자입니다:)
  </section>
  <slot name="footer">푸터를 주입해주세요!</slot>
</div>

이제 다음처럼 username을 부모 컴포넌트에서 사용할 수 있습니다.

<Page let:username={pageUsername}>
  <header slot="header">{pageUsername}의 소개 페이지!</header>
  <footer slot="footer">주입하는 푸터</footer>
</Page>

최종은 다음과 같이 헤더에 username이 반영되어 렌더링됩니다.

<div>
  <header slot="header">ashnamuh의 소개 페이지!</header>
  <section>
    제 이름은 ashnamuh입니다.
    저는 개발자입니다:)
  </section>
  <footer slot="footer">주입하는 푸터</footer>
</div>

주의할 점은 <Page let:username={pageUsername}>로 사용해서 마치 footer에서도 pageUsername을 사용할 수 있을 것 같지만, 불가능합니다.

제가 해본 결과 undefined가 출력되었습니다.

slot의 경우 vue에서 영감을 받은 것인지는 모르겠지만, v-slot과 거의 비슷한 기능을 가집니다.

특별한 svelte: 엘리먼트

svelte는 특별한 엘리먼트 몇가지를 제공합니다.

실제 html 엘리먼트가 아닌 가상이지만 편리한 기능을 제공합니다.

이 중 대표적인 것 몇가지만 소개하겠습니다.

svelte:self

self를 통해 컴포넌트 자기 자신을 재귀적으로 사용할 수 있습니다.

props도 전달 가능합니다.

단, 무한재귀가 될 수 있으므로 조건문과 함께 사용해야합니다.

다음 예시는 count 값에 따라 재귀적으로 자기 자신 컴포넌트를 사용합니다.

<script>
export let count = 5
</script>

{#if count > 0}
  <p>카운트가 내려갑니다 {count}</p>
  <svelte:self count="{count - 1}"/>
{:else}
  <p>카운트가 {count}이 되었습니다!</p>
{/if}

최종 렌더링은 다음과 같습니다.

<p>카운트가 내려갑니다 5</p>
<p>카운트가 내려갑니다 4</p>
<p>카운트가 내려갑니다 3</p>
<p>카운트가 내려갑니다 2</p>
<p>카운트가 내려갑니다 1</p>
<p>카운트가 0이 되었습니다!</p>

svelte:component

component는 동적으로 컴포넌트를 렌더링합니다.

취향에 따라 고양이와 개 컴포넌트를 동적으로 렌더링하는 예시를 보여드리겠습니다.

우선 매우 간단하게 Cat.svelteDog.svelte를 각각 만듭니다.

// Cat.svelte
<p>고양이 컴포넌트</p>

// Dog.svelte
<p>개 컴포넌트</p>

그리고 select 태그로 선택한 값에 따라 <svelte:component>this 속성으로 렌더링할 컴포넌트를 전달해줍니다.

select값을 변경하면 렌더링하는 컴포넌트도 변경되는걸 확인 가능합니다.

<script>
import Cat from './Cat.svelte'
import Dog from './Dog.svelte'

const options = [
  { animal: 'cat', component: Cat },
  { animal: 'dog', component: Dog }
]

let selected = options[0]
</script>

<select bind:value={selected}>
  <option value={options[0]}>고양이</option>
  <option value={options[1]}></option>
</select>

<svelte:component this={selected.component}/>

svelte:window

svelte:window는 브라우저의 글로벌 객체 window를 일부 참조합니다.

굳이 window에 addEventListener로 이벤트를 등록하지 않고 <svelte:window>on:으로 사용할 수 있습니다.

<script>
const handleScroll = () => console.log('scrolled!')
</script>

<svelte:window on:scroll={handleScroll} />

또한 window 객체의 속성인 innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY 값을 bind로 얻을 수도 있습니다.

또한, bind:this로 window 객체를 직접 참조할 수 있을까 싶었지만, 사용할 수 없었습니다.

<script>
import { onMount } from 'svelte'

let innerWidth

onMount(() => {
  console.log(innerWidth) // 실제 innerWidth 값이 출력됨
})
</script>

<svelte:window bind:innerWidth={innerWidth} />

svelte:body

window처럼 svelte:body는 body에 대한 참조를 얻습니다.

마찬가지로 이벤트를 등록할 수 있습니다.

<script>
const handleClick = () => console.log('clicked!')
</script>

<svelte:body on:click={handleClick} />

참고로 여기에도 bind:this를 사용해보았지만 undefined가 출력되었습니다.

svelte:head

svelte:head는 head 영역에 컨텐츠를 넣을 수 있게 해줍니다.

메타 태그 등을 조작할 때 유용하겠군요.

예시로 title과 메타태그를 변경해보겠습니다.

<svelte:head>
  <title>ashnamuh 개발 블로그</title>
  <meta name="description" content="ashnamuh의 개발 블로그입니다.">
</svelte:head>

실제로 head가 변경된걸 확인 가능합니다.

<head>
  <title>ashnamuh 개발 블로그</title>
  <meta name="description" content="ashnamuh의 개발 블로그입니다.">
</head>

후기

svelte의 일부 기능은 vue랑 비슷합니다.

하지만 같은 기능이라도 코드의 양은 vue보다 적었습니다.

적은 양의 코드가 유지보수를 좀 더 원활하게 하지 않을까 생각이 듭니다.

다음 svelte 관련 글에서는 라이프사이클, 그리고 상태관리나 라우터에 대해서 다뤄볼 예정입니다.

읽어주셔서 감사합니다(_ _)