# Svelte의 상태 관리

- Author: @baealex
- Published: 2023-06-20
- Updated: 2023-10-16
- Source: http://blex.me/@baealex/svelte-state
- Tags: 프로그래밍, 자바스크립트, 프론트엔드, 스벨트

---

최근에 작업하는 토이 프로젝트의 프론트엔드를 바닐라로 개발하다가 심각하게 복잡해지기 시작했다. 비교적 단순한 것임에도 불구하고 말이다. UI의 업데이트가 필요한 부분은 개발자가 가장 잘 안다는 생각 하나로 리액트보다 효율적인 UI 관리를 해보겠다는 생각이었다.

괜찮은 구조를 만들 수 있으리라 기대했는데...

<br>

<grid-image col="1">
  @gif[https://static.blex.me/images/content/2023/6/20/202362020_Dqve51UFM3rELo2ck74f.mp4]
  <caption>난리난 코드</caption>
</grid-image>

<br>

UI를 관리하기 위한 로직이 점차 복잡해졌고, 상태로만 관리하자니 그에 따른 렌더링 구조를 만들지 않았기에 효율과는 거리가 멀어 보였다. 결국은 리액트를 써야하나 싶다가 새로운 기술을 사용해 볼 목적으로 Svelte를 사용해 보았다. 처음에는 마냥 쉽고 재밌었는데 복잡한 상태를 관리하는 부분에서 난항을 겪기도 하였다. 그에 대해서 알아낸 것들을 적어두려 한다.

<br>

#### 1. 컴포넌트 상태

위에서 말했다. *'UI의 업데이트가 필요한 부분은 개발자가 가장 잘 안다'* 아니었다. Svelte가 더 잘 알아준다. Svelte는 let으로 선언한 변수에 대해서 변화가 생기면 변경이 필요한 부분에 대해서 UI를 다시 렌더링 해준다. 물론 함수형 프로그래밍이 유행하는 현 시점에서 상태를 직접 변경한다는 것은 큰 이점은 아닐 수 있겠지만 매우 직관적이고 단순한 형태라고 생각되었다.

```ts
<script>
    let count = 0;

    function increment() {
        count += 1;
    }
</script>

<main>
    <h1>Count: {count}</h1>
    <button on:click={increment}>Increment</button>
</main>
```

단순히 `count`라는 변수를 선언하고 변경했을 뿐이지만 Svelte는 멋지게 렌더한다. 또한 해당 변수를 export 하면 부모 컴포넌트에서 props 형태로 전달해 줄 수 있는 상태가 된다.

<br>

#### 2. 리액티브 선언

```ts
<script>
    let count = 0;
	
    $: doubleCount = count * 2
	
    $: {
        console.log("count updated!")
    }

    function increment() {
        count += 1;
    }
</script>

<main>
    <h1>Count: {count}</h1>
    <h1>Doucle Count: {doubleCount}</h1>
    <button on:click={increment}>Increment</button>
</main>
```

직관적이었던 1과는 달리 이 문법은 처음에는 다소 난해해다는 생각이 들었다. `$:` 안에 나열된 상태나 로직은 상태가 업데이트되는 즉시 실행된다. Vue의 `computed`와 유사하게 동작하는 개념이다. 특히 두번째에 나열된 리액티브 선언의 경우에는 아무런 상태와도 연관되어 있지 않기 때문에 초기에만 실행되며 다른 상태가 변경되어도 동작하지 않는다.

<br>

#### 3. 스토어 (writable)

전역 스토어를 만들기 위해 다른 파일(`./count.ts`)에 아래와 같이 선언했다.

```ts
import { writable } from 'svelte/store';

export const count = writable(0);
```

컴포넌트에서 해당 값을 불러온다.

```ts
<script>
    import { count } from "./count";

    $: doubleCount = $count * 2;

    function increment() {
        $count += 1;
    }
</script>

<main>
    <h1>Count: {$count}</h1>
    <h1>Doucle Count: {doubleCount}</h1>
    <button on:click={increment}>Increment</button>
</main>
```

스벨트에서 제공하는 스토어에 `writable`라는 함수를 이용해서 상태를 선언하고 외부에서 이 값을 사용할때는 `$count`와 같이 접근하여 값을 출력할 수 있다. 이는 스벨트에서 내부적으로 관리되는 값이기 때문에 해당 문법을 사용하는 것으로 보인다.

해당 값은 위처럼 직접 변경할 수도 있고 혹은 아래와 같이 update 함수를 사용해도 된다

```ts
function increment() {
    count.update((n) => n + 1);
}
```

다른 컴포넌트에서도 해당 상태에 접근하면 당연히 동일한 값을 가진다.

<br>

#### 4. 스토어 구독 스토어 (derived)

derived는 상당히 다용도로 활용할 수 있는 스토어로 보이는데, 나는 다음과 같은 상황에서 필요했다. 하나의 배열에 동일한 형태이지만 독립적인 상태들이 존재하고 각 상태의 메소드에 의해 수정된 상태를 UI에 반영하고 싶었다. 예를들어 다음과 같은 상황이다.

```ts
<script lang="ts">
    import type { Writable } from "svelte/store";
    import { writable } from "svelte/store";
    import { onMount } from "svelte";
	
    interface Item {
        title: string;
        delete: () => void;
    }
	
    function itemState(item: Item) {
        const store = writable(item);
        const methods = {
            delete() {
                store.update((item) => ({
                    ...item,
                    title: "deleted",
                }));
            }
        };
        return {
            ...store,
            ...methods,
        };
    }
		
    let items: Writable<Item>[] = [];

    onMount(() => {
        getItems().then(({ data }) => {
            items = data.allItems.map(itemState);
        });
    });
</script>
```

여기까진 좋았는데 템플릿에서의 접근할 방도가 없었다.

<br>

**시작되는 삽질**

```ts
<main>
    {#each items as item}
        <p>{item.title}</p>
        <button on:click={() => item.delete()}>삭제</button>
    {/each}
<main>
```

```
Property 'title' does not exist on type 'Writable<Item>'
```

```ts
<main>
    {#each items as item}
        <p>{$item.title}</p>
        <button on:click={() => $item.delete()}>삭제</button>
    {/each}
<main>
```

```
Cannot find name '$item'. Did you mean 'item'?
```

<br>

<grid-image col="1">
  @gif[https://static.blex.me/images/content/2023/6/21/20236210_I1HOX9ldFQFufJDU3dia.mp4]
	<caption>어떻게 하라는 거야!</caption>
</grid-image>

<br>

그러다 derived의 존재를 알게 되었는데, derived는 상태거나 상태를 가진 배열인 경우에 구독을 진행할 수 있다. 구독한 상태의 변화에 대응해서 값을 생성할 수 있는데, 첫번째 인자에는 구독하는 상태, 두번째 인자에는 생성되는 값을 나열한다.

```ts
<script lang="ts">
    ...
    import { writable, derived, get } from "svelte/store";
    ...

    $: resolveItems = derived(items, () =>
        array.map((item) => ({
            ...get(item),
            ...item,
        }))
    );
</script>

<main>
    {#each $resolveItems as item}
        <p>{item.title}</p>
        <button on:click={() => item.delete()}>삭제</button>
    {/each}
</main>
```

처음 사용해 본 스벨트. 첫 인상으로는 쉽고, 재밌고, 번들 사이즈도 작고 아직까진 매우 훌륭한 프레임워크 라고 생각된다. 앞으로도 바닐라로 작업할 일(?)이 있다면 가능한 스벨트를 선택하게 될 것 같다.
