Svelte 속성과 로직

Svelte 속성과 블록 구문 이해하기

Svelte 속성

속성 내보내기

값은 주어진 컴포넌트 내에서만 액세스할 수 있는데, 실제 응용 프로그램에서는 한 컴포넌트의 데이터를 하위 컴포넌트로 전달해야 하는 경우가 많아.

한 컴포넌트에서 다른 컴포넌트로 데이터를 전달하기 위해서는 일반적으로 ‘props’로 축약되는 속성을 선언해야 하는데, Svelte에서는 export 키워드로 이를 수행할 수 있어.

속성 선언하기
export let answer;

다음의 코드는 상위 컴포넌트인 App.svelte에서 컴포넌트에 데이터를 전달하고, 하위 컴포넌트인 Nested.svelte에서 export로 데이터를 전달받아 출력하는 예제야.

상위 컴포넌트 export
App.svelte
<script>
    import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
하위 컴포넌트에서 데이터 받기
Nested.svelte
<script>
    export let answer;
</script>

<p>The answer is {answer}</p>

속성 기본값 설정

하위 컴포넌트인 Nested.svelte에서는 속성의 데이터의 기본값을 쉽게 설정 할 수 있어.

속성 기본값 설정하기
export let answer = 'a mystery';

위 코드와 같이 export 할 데이터 속성에 기본 값을 넣어주기만 하면 answer 속성은 컴포넌트를 추가할 때 데이터를 전달하지 않아도 기본 값이 속성에 전달되기 때문에 해당 기본 값으로 컴포넌트가 표시 돼.

기본 컴포넌트 추가하기
App.svelte
<script>
    import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
<Nested/>
export 속성에 기본 값 설정하기
Nested.svelte
<script>
    export let answer = 'a mystery';
</script>

<p>The answer is {answer}</p>

객체 속성 전달

속성 개체가 존재하는 경우에는 각각의 속성을 하나씩 지정하는 대신 {…pkg}와 같이 해당 객체를 직접 전달할 수 있어. 다음 코드와 같이 pkg라는 객체를 만들어 객체에 필요한 속성들을 추가하여 객체를 전달하면, 해당 객체를 받는 컴포넌트에서는 export로 데이터를 받을 수 있어.

객체 속성 전달하기
App.svelte
<script>
    import Info from './Info.svelte';

    const pkg = {
        name: 'svelte',
        version: 3,
        speed: 'blazing',
        website: 'https://svelte.dev'
    };
</script>

<Info {...pkg}/>
객체 속성 export 하기
Info.svelte
<script>
    export let name;
    export let version;
    export let speed;
    export let website;
</script>

<p>
    The <code>{name}</code> package is {speed} fast.
    Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
    and <a href={website}>learn more here</a>
</p>
컴포넌트의 모든 속성 참조하기

반대로 export로 선언되지 않은 것을 포함하여 컴포넌트에 전달된 모든 props를 참조해야 하는 경우에는 $$props에 직접 액세스하여 해당 데이터를 참조할 수 있어. 하지만 $$props로 참조하는 경우에는 Svelte로 최적화하기가 어렵기 때문에 권장되는 방법은 아니야.

블록 조건 구문

HTML은 조건문이나 루프같은 논리를 표현하는 방법이 없지만, Svelte는 블록 래핑으로 조건이나 반복을 수행하여 마크업을 렌더링할 수 있어.

if 블록 구문

다음의 코드는 if 블록으로 래핑하여 조건에 따라 컴포넌트의 일부만 렌더링하는 코드야.

if 문 렌더링하기
App.svelte
<script>
    let user = { loggedIn: false };

    function toggle() {
        user.loggedIn = !user.loggedIn;
    }
</script>

{#if user.loggedIn}
    <button on:click={toggle}>
        Log out
    </button>
{/if}

{#if !user.loggedIn}
    <button on:click={toggle}>
        Log in
    </button>
{/if}

if-else 블록 구문

앞에서 살펴 본 if 문을 사용한 코드의 경우, if user.logged Inif !user.logged In의 두 조건은 상호 배타적이기 때문에 else 블록을 사용하여 코드를 더 단순하게 표현할 수 있어.

if-else 문 렌더링하기
App.svelte
<script>
    let user = { loggedIn: false };

    function toggle() {
        user.loggedIn = !user.loggedIn;
    }
</script>

{#if user.loggedIn}
    <button on:click={toggle}>
        Log out
    </button>
{:else}
    <button on:click={toggle}>
        Log in
    </button>
{/if}

위 코드를 살펴보면 블록 구문은 항상 #으로 시작하고, / 문자로 닫는다는 것을 알 수 있는데, :else와 같이 구문의 중간에 들어가는 : 문자는 블록의 연속 태그임을 나타내는 규칙이야.

if-else-if 블록 구문

if 문을 사용하는 경우 if-else if-else와 같이 여러 조건을 연결하여 사용하는 경우가 많은데, Svelte의 블록 구문 역시 이런 문법을 지원하고 있어.

if-else-if 문 렌더링하기
App.svelte
<script>
    let x = 7;
</script>

{#if x > 10}
    <p>{x} is greater than 10</p>
{:else if 5 > x}
    <p>{x} is less than 5</p>
{:else}
    <p>{x} is between 5 and 10</p>
{/if}

블록 루프 구문

만약 HTML의 컴포넌트를 데이터 목록 만큼 반복해야 하는 경우에는 다음과 같이 each 블록을 사용할 수 있어.

each 문 렌더링
<ul>
    {#each cats as cat}
        <li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
            {cat.name}
        </a></li>
    {/each}
</ul>

표현식은 임의의 배열 또는 배열과 유사한 객체가 될 수 있는데, each [...iterable]을 사용하여 일반 HTML 컴포넌트를 반복할 수 있어.

만약 인덱스가 필요하면, 다음과 같이 두 번째 인수로 인덱스를 가져올 수 있어.

인덱스 가져오기
{#each cats as cat, i}
    <li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
        {i + 1}: {cat.name}
    </a></li>
{/each}

필요한 경우에는 each cats as { id, name }와 같이 구조화 문법을 사용할 수 있는데, 구조화를 사용하는 경우에는 cat.idcat.name 대신 idname으로 데이터를 가져올 수 있어.

each 문 렌더링 전체 예제
App.svelte
<script>
    let cats = [
        { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
        { id: 'z_AbfPXTKms', name: 'Maru' },
        { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
    ];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
    {#each cats as { id, name }, i}
        <li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
            {i + 1}: {name}
        </a></li>
    {/each}
</ul>

고유 식별자 지정하기

기본적으로 each 블록의 값을 수정하면 블록의 끝에 항목이 추가되거나 제거되고, 변경된 값이 업데이트 되어야 하는데, 다음의 코드를 실행해보면 Remove first thing 버튼을 클릭해도 첫 번째 요소가 제거되지 않고, 마지막 DOM 노드가 제거 된다는 것을 알 수 있어.

마지막 요소가 삭제되는 코드
App.svelte
<script>
    import Thing from './Thing.svelte';

    let things = [
        { id: 1, name: 'apple' },
        { id: 2, name: 'banana' },
        { id: 3, name: 'carrot' },
        { id: 4, name: 'doughnut' },
        { id: 5, name: 'egg' },
    ];

    function handleClick() {
        things = things.slice(1);
    }
</script>

<button on:click={handleClick}>
    Remove first thing
</button>

{#each things as thing}
    <Thing name={thing.name}/>
{/each}
마지막 요소가 삭제되는 코드
Thing.svelte
<script>
    const emojis = {
        apple: "????",
        banana: "????",
        carrot: "????",
        doughnut: "????",
        egg: "????"
    }

    // prop 값이 변경될 때마다 이름 업데이트
    export let name;

    // "emoji" 변수는 컴포넌트 초기화 시 고정됨
    const emoji = emojis[name];
</script>

<p>
    <span>The emoji for { name } is { emoji }</span>
</p>

<style>
    p {
        margin: 0.8em 0;
    }
    span {
        display: inline-block;
        padding: 0.2em 1em 0.3em;
        text-align: center;
        border-radius: 0.2em;
        background-color: #FFDFD3;
    }
</style>

위 코드는 <Thing>의 첫 번째 DOM 노드를 제거한 후, 나머지 DOM 노드에서 name을 업데이트하지만 이모티콘은 제대로 업데이트가 되지 않는데, 만약 <Thing>의 첫 번째 DOM 노드만 제거하고 나머지는 영향을 받지 않도록 하고 싶다면, 다음과 같이 each 블록에 고유한 식별자를 지정해 주어야 돼.

each에 고유 식별자 지정하기
{#each things as thing (thing.id)}
    <Thing name={thing.name}/>
{/each}

위 코드에서 (thing.id)는 컴포넌트가 업데이트될 때 변경할 DOM 노드를 Svelte에 알려주는 의 역할을 하는데, Svelte는 내부적으로 Map을 사용하기 때문에 모든 객체를 키로 사용할 수 있어.

즉, (thing.id) 대신 (thing)을 사용해도 되지만, 굳이 문자열이나 숫자를 사용하는 것을 권장하는 것은, 예를 들어 API 서버에서 최신 데이터로 업데이트할 때 참조 동등성 없이 ID가 지속된다는 의미가 되어, 더 안전한 동작을 수행할 수 있기 때문이라고 해.

고유 식별자를 사용한 each 문
App.svelte
<script>
    import Thing from './Thing.svelte';

    let things = [
        { id: 1, name: 'apple' },
        { id: 2, name: 'banana' },
        { id: 3, name: 'carrot' },
        { id: 4, name: 'doughnut' },
        { id: 5, name: 'egg' },
    ];

    function handleClick() {
        things = things.slice(1);
    }
</script>

<button on:click={handleClick}>
    Remove first thing
</button>

{#each things as thing (thing.id) }
    <Thing name={thing.name}/>
{/each}
고유 식별자를 사용한 each 문
Thing.svelte
<script>
    const emojis = {
        apple: "????",
        banana: "????",
        carrot: "????",
        doughnut: "????",
        egg: "????"
    }

    // the name is updated whenever the prop value changes...
    export let name;

    // ...but the "emoji" variable is fixed upon initialisation of the component
    const emoji = emojis[name];
</script>

<p>
    <span>The emoji for { name } is { emoji }</span>
</p>

<style>
    p {
        margin: 0.8em 0;
    }
    span {
        display: inline-block;
        padding: 0.2em 1em 0.3em;
        text-align: center;
        border-radius: 0.2em;
        background-color: #FFDFD3;
    }
</style>

비동기 처리하기

대부분의 웹 애플리케이션은 어느 시점에서 비동기 데이터를 처리해야 하는데, Svelte에서는 마크업에서 직접 Promise의 값을 기다릴 수 있는 awit 문을 사용할 수 있어.

await 문으로 promise 구현하기
{#await promise}
    <p>...waiting</p>
{:then number}
    <p>The number is {number}</p>
{:catch error}
    <p style="color: red">{error.message}</p>
{/await}

await는 가장 최근의 promise만 고려되기 때문에 다른 조건에 대해 고민할 필요가 없는데, 만약 promise를 취소 할 수 없다는 것을 알고 있는 경우에는 catch 블록을 생략할 수 있고, 만약 Promise가 수행될 때까지 아무것도 표시하고 싶지 않다면 첫 번째 블록을 생략할 수도 있어.

then-catch 블록 생략하기
{#await promise then value}
    <p>the value is {value}</p>
{/await}

다음은 promise를 수행하는 전체 샘플 코드야.

await로 비동기 처리 구현하기
App.svelte
<script>
    async function getRandomNumber() {
        const res = await fetch(`/tutorial/random-number`);
        const text = await res.text();

        if (res.ok) {
            return text;
        } else {
            throw new Error(text);
        }
    }

    let promise = getRandomNumber();

    function handleClick() {
        promise = getRandomNumber();
    }
</script>

<button on:click={handleClick}>
    generate random number
</button>

{#await promise}
    <p>...waiting</p>
{:then number}
    <p>The number is {number}</p>
{:catch error}
    <p style="color: red">{error.message}</p>
{/await}

답글 남기기