9. 스토어

2020. 10. 7. 17:07재주껏 하는 Front-End/Svelte (준비중)

반응형

이번 글에서는 규모가 큰 애플리케이션을 개발할 때 사용하기 좋은 Store에 대해서 알아보자. Store란 각자 다른 컴포넌트들이 같은 데이터를 접근하기 위해 사용하는 것으로, 애플리케이션의 규모가 커지거나 컴포넌트 간의 상호 작용이 필요할 경우 매우 유용하게 사용할 수 있는 객체를 말한다.

 

 

Store는 여러 개의 컴포넌트들이 참조할 수 있으며, Store의 값이 수정될 때 참조하고 있는 컴포넌트에게 알려주는 구독 기능을 제공한다. 구독을 사용하는 컴포넌트는 개발자가 변경된 값을 화면에 출력하기 위해 DOM을 조작하지 않아도 자동으로 반영된다. 또한 구독 메서드를 사용하면 Svelte 내부 로직을 통해 값이 변경되기 때문에 성능 및 안전성이 높은 장점이 있다.

 

Store의 기본 사용법은 아래와 같다.

 

// Store.js
import { writable } from 'svelte/store';

// 컴포넌트들이 같이 참조하는 Store 변수
export const exampleStore= writable(0);




// Component.js
<script>
  import { exampleStore } from '스토어 경로. js'

  let storeValue = 0;
 
  const unsubscribe = exampleStore.subscribe(value => {
    storeValue  = value
  })
</script>
<div>
  <p> Store value is {storeValue}</p>
</div>

 

Svelte의 Store에서는 Writable, Readable, Derived 구독 메서드를 지원하며, 사용자 지정 스토어도 구성할 수 있다. 각각의 구독 메서드가 어떤 기능을 지원하는지 살펴보자. Svelte 튜토리얼에서 제공하는 예제로 해당 기능들을 살펴보기 위해 아래에 코드를 미리 추가하였다.

 

1) App.svelte

<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';

	let count_value;

	const unsubscribe = count.subscribe(value => {
		count_value = value;
	});
</script>

<h1>The count is {count_value}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>

 

2) Incrementer

<script>
	import { count } from './stores.js';

	function increment() {
		count.update(n => n + 1);
	}
</script>

<button on:click={increment}>
	+
</button>

 

3) Decrementer

<script>
	import { count } from './stores.js';

	function decrement() {
		count.update(n => n - 1);
	}
</script>

<button on:click={decrement}>
	-
</button>

 

4) Resetter

<script>
	import { count } from './stores.js';

	function reset() {
		count.set(0);
	}
</script>

<button on:click={reset}>
	reset
</button>

 

5) Store.js

import { writable } from 'svelte/store';

export const count = writable(0);

 

위의 예제를 실행하면 아래의 사진과 같다.

 

 

1. Writable Store

 

읽기, 쓰기가 가능한 스토어로 Set과 Update 메서드를 지원한다. Set은 값을 초기화하는 메서드이고, Update는 값을 수정하는 메서드이다. Update의 경우 파라미터로 콜백 함수가 전달되는데, 콜백 함수의 파라미터로 스토어의 현재 값이 전달된다. 콜백 함수를 리턴하면 스토어의 값이 수정된다. 사용 방법은 아래와 같다.

 

// Store.js
import { writable } from 'svelte/store'

export const exampleStore = writable(초기값)




// 사용할 컴포넌트.svelte
<script>
  import { exampleStore } from 'Store.js 경로'

  function setStoreFunc () {
    exampleStore.set(초기화 값)
  }

  function updateStoreFunc () {
    exampleStore.update((현재 스토어 값) => {
      return 업데이트할 값
    })
  }
</script>

 

위의 예제에서는 Writable 스토어를 사용하여 값의 초기화와 값을 수정하고 있다. 아래는 update 메서드를 통하여 값을 수정하는 함수이다. + 버튼을 클릭하면 이벤트 핸들러인 아래 함수가 호출되어 스토어의 값에서 + 1을 하여 업데이트한다.

 

function increment() {
  count.update(n => n + 1);
}

 

아래는 set 메서드를 통하여 스토어의 값을 0으로 초기화하는 함수이다. set 메서드는 파라미터를 하나만 받기 때문에 초기화할 값만 전달하면 된다.

 

function reset() {
  count.set(0);
}

 

위와 같이 스토어를 사용하여 여러 컴포넌트들이 같은 데이터를 참조하고 설정하는 것을 확인할 수 있었다. 하지만 위의 코드는 문제를 가지고 있다. 다른 페이지로 이동하거나 애플리케이션을 종료할 때 스토어에 할당된 객체가 소멸되지 않고 메모리에 계속 저장되어 메모리 누수를 발생시킨다.

 

위의 문제를 해결하기 위해 애플리케이션이 파괴될 때 스토어의 구독을 끊어 메모리를 소거시켜야 한다.

 

2. Unsubscribe / Auto Subscribe

 

스토어의 구독을 해지하는 방법은 아래와 같다.

 

<script>
  import { onDestroy } from 'svelte';

  ...

  let storeValue = 0;

  const unsubscribe = exampleStore.subscribe(value => {
    storeValue = value
  })

  onDestroy(() => {
    unsubscribe ()
  })
</script>

 

구독 메서드를 실행하면 리턴 값으로 구독을 해지하는 함수를 리턴한다. 리턴된 함수를 실행하면 자동으로 구독이 해지된다. 일반적으로 스토어의 구독을 해지하는 경우는 애플리케이션이 종료되는 시점인 경우가 많으므로 Svelte 컴포넌트 생명 주기의 onDestroy 내부에서 구독을 해지하는 것이 좋다.

 

위의 내용을 토대로 Counter 애플리케이션을 수정하면 아래와 같다.

 

App.svelte

<script>
    import { onDestroy} from 'svelte'
    import { count } from './stores.js';
    import Incrementer from './Incrementer.svelte';
    import Decrementer from './Decrementer.svelte';
    import Resetter from './Resetter.svelte';

    let count_value;

    const unsubscribe = count.subscribe(value => {
        count_value = value;
    });
    
    onDestroy(unsubscribe);
</script>

<h1>The count is {count_value}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>

 

스토어를 사용해보니 컴포넌트 간 데이터 조작은 편한데 사용법이 너무 번거롭다. 스토어의 값을 참조하기 위해서는 컴포넌트 내부에 subscribe 메서드를 통하여 내부 변수에 구독을 연결해야 되고 메모리 누수를 방지하기 위해서는 구독 해지 메서드를 따로 실행해야 한다. 여간 귀찮은 것이 아니다.

 

Svelte에서는 위의 번거로움을 해소하기 위해 자동 구독 기능을 제공한다. 스토어의 자동 구독 기능을 사용하면 컴포넌트 변수와 매번 연결할 필요도 없고 메모리 해제를 위해 구독 해제 함수를 호출하지 않아도 된다. 자동 구독 기능을 사용하는 방법은 아래와 같다.

 

import { 스토어명 } from '스토어 경로' 

$
스토어명 =...

 

스토어 이름 앞에 $만 붙이면 자동 구독 기능을 사용할 수 있다. 자동 구독 기능은 마크업뿐만 아니라 스크립트에서도 사용이 가능하다. 주의할 점은 최상위에 import 된 스토어 명으로만 자동 구독 기능을 사용할 수 있다는 것이다. 자동 구독을 사용하여 위의 애플리케이션을 개선해보자.

 

App.svelte

<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>

 

위와 같이 매우 깔끔한 코드가 완성되었다. 구독에서 본 것과 같이 Svelte에서는 $로 시작하는 모든 이름을 스토어로 인식하기 때문에 변수명이나 함수명을 지을 때 주의해야 한다.

3. Readable Store

 

Svelte에서 스토어 사용 시 대부분은 읽기 / 쓰기가 가능한 Writable 스토어를 사용하겠지만, 경우에 따라서 값을 바꿀 수 없고 참조만 가능한 스토어가 유용할 때도 있다. 예를 들어, 브라우저의 마우스 위치나 브라우저의 스크린 크기 등 외부에서 변경하는 것이 의미 없는 데이터가 필요한 경우가 있을 것이다. 이 경우 Readable 스토어를 사용하여 읽기 전용 데이터를 표현할 수 있다.

 

Readable 스토어를 사용하는 방법은 아래와 같다.

 

import { readable } from 'svelte/store.js'

export const exampleStore = readable(초기값, function (setFunc) {
  // 스토어 값 설정 시 실행되는 작업

  setFunc(스토어에 설정할 값)

  return function initStoreFunc(){
    // 스토어 초기화 시 실행되는 작업
  }
})

 

Readable 스토어를 실제로 사용해보자. 아래의 코드는 window 객체에서 screen 객체의 정보를 화면에 출력하는 애플리케이션이다.

 

1) App.svelte

<script>
	import { screenObj } from './stores.js';
</script>

<h1>Screen Info</h1>
<p>Width: {$screenObj.width}px</p>
<p>Height: {$screenObj.height}px</p>
<p>Avaliable Width: {$screenObj.availWidth}</p>
<p>Avaliable Height: {$screenObj.availHeight}</p>

 

2) Stores.js

import { readable } from 'svelte/store';

export const screenObj = readable({}, (set) => {
	set(window.screen);
	
	return () => {}
});

 

출력 결과는 아래와 같다.

 

 

Writable 스토어와 선언 및 사용 방법은 크게 차이가 없는 것을 확인할 수 있다. 다만 스토어의 값을 초기화하거나 수정하는 set 또는 update 메서드를 지원하지 않아 컴포넌트에서 값을 바꿀 수 없다는 것이 Writable 스토어와의 차이점이다.

 

4. Derived Store

 

Derived는 이미 존재하는 스토어를 사용하여 새로운 스토어를 만드는 기술이다. 참조하는 원본 스토어의 값을 수정하지 않고 필요한 데이터를 조합하여 컴포넌트에게 제공할 수 있다. 컴포넌트에서는 해당 값을 수정할 수 없기 때문에 Readable 성격을 가지고 있다. Vuex의 Getter와 같은 것이라고 봐도 된다.

 

Derived 스토어의 사용 방법은 아래와 같다.

 

import { readable, derived } from 'svelte/store';

// 참조할 또 다른 스토어
export const anotherStore1=...
export const anotherStore2 =...
export const anotherStore3 =...

// 파생 스토어
export const exampleStore = derived(anotherStore1, ($anotherStore1) => 파생 스토어의 새로운 값, 초기값)
export const exampleStore = derived([$anotherStore1, $anotherStore2...], ([$anotherStore1, $anotherStore2...]) => 파생 스토어의 새로운 값, 초기값)

 

참조하는 스토어가 여러 개라면 derived의 첫 번째 파라미터가 배열로 전달되고 두 번째 콜백 함수의 파라미터도 배열로 전달된다. 세 번째 파라미터는 파생 스토어의 초기값을 뜻한다. 다른 스토어와 차이점이라면 콜백 함수의 리턴이 구독 해지 함수가 아닌 실제 값이라는 것이다.

 

파생 스토어를 실제로 사용해보자. 아래의 코드는 현재 시간을 1초 간격으로 수정하고 화면이 처음 로딩된 시간으로부터 몇 초가 지났는지 출력하는 애플리케이션이다.

 

1) App.svelte

<script>
	import { time, elapsed } from './stores.js';

	const formatter = new Intl.DateTimeFormat('en', {
		hour12: true,
		hour: 'numeric',
		minute: '2-digit',
		second: '2-digit'
	});
</script>

<h1>The time is {formatter.format($time)}</h1>

<p>
	This page has been open for
	{$elapsed} {$elapsed === 1 ? 'second' : 'seconds'}
</p>

 

2) Stores.js

import { readable, derived } from 'svelte/store';

export const time = readable(new Date(), function start(set) {
	const interval = setInterval(() => {
		set(new Date());
	}, 1000);

	return function stop() {
		clearInterval(interval);
	};
});

const start = new Date();

export const elapsed = derived(
	time,
	$time2 => {
		return Math.round(($time2 - start) / 1000);
	}
);

 

출력 결과는 아래와 같다.

 

 

읽기 전용 스토어인 Time을 derived 함수로 파생하여 elapsed 스토어를 생성했다. elapsed 스토어는 참조하고 있는 Time 스토어의 값을 가져와서 현재 시간에서 페이지가 열린 시간을 뺀 값 / 1000을 수행한 결과를 리턴한다. 이런 식으로 파생 스토어는 이미 있는 스토어를 참조하여 클라이언트가 원하는 데이터로 가공한 값을 제공할 때 사용한다.

 

5. Custom Store

 

Svelte는 subscribe 메서드가 포함된 객체를 스토어로 인식한다. 이러한 특징을 이용하면 사용자 정의 스토어를 생성할 수 있다. 위에 있는 카운터 애플리케이션을 아래와 같이 파일 2개로 수정해보자.

 

1) App.svelte

<script>
	import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>

<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

 

2) Stores.js

import { writable } from 'svelte/store';

function createCount() {
	const { subscribe, set, update } = writable(0);

	return {
		subscribe,
		increment: () => {
			return update(n => n + 1)
		},
		decrement: () => {
			return update(n => n - 1)
		},
		reset: () => {
			return set(0)
		}
	};
}

export const count = createCount();

 

실행 결과는 아래와 같다.

 

 

Stores.js 모듈 내부에 createCount 함수를 보면 writable 함수의 subscribe, set, update 메서드를 사용한 새로운 객체를 리턴하는 것을 볼 수 있다. 위와 같이 매우 쉽게 사용자 정의 스토어를 생성할 수 있다.

 

6. Store Binding

 

쓰기가 가능한 스토어의 경우 (Writable 스토어, 사용자 정의 스토어 중 set 메서드가 있는 경우) 컴포넌트에서 양방향 바인딩 기능을 사용할 수 있다. 간단한 예제를 통해 확인해보자.

 

아래의 애플리케이션은 Input에 입력된 글자를 화면에 출력하는 간단한 애플리케이션으로 스토어 바인딩을 사용하였다.

 

1) App.svelte

<script>
	import { name, greeting } from './stores.js';
</script>

<h1>{$greeting}</h1>
<input bind:value={$name}>

<button on:click="{() => $name += '!'}">
	Add exclamation mark!
</button>

 

2) Stores.js

import { writable, derived } from 'svelte/store';

export const name = writable('world');

export const greeting = derived(
	name,
	$name => `Hello ${$name}!`
);

 

일반적인 바인딩 방법과 동일한 것을 확인할 수 있다.

 


 

지금까지 Svelte의 스토어에 대해 알아보았다. 스토어는 애플리케이션의 규모가 커질수록 컴포넌트와 모델 간의 관계가 복잡해지는 문제를 효과적으로 해결할 수 있는 아주 유용한 Svelte의 구성 요소이기 때문에 Svelte를 주 라이브러리로 프로젝트를 진행한다면 반드시 알고 있어야 하는 중요한 도구이다. 스토어의 조작이 다른 라이브러리 대비 어려운 편이 아니기 때문에 여러 번 사용해서 반드시 익히도록 하자.

 

오늘은 여기까지이며 다음에는 컴포넌트의 Slot에 대해 알아보자. 스타일에 관련된 모션, 액션, 변형은 맨 뒤에서 알아보도록 하겠다. 오늘은 여기까지 ~

반응형

'재주껏 하는 Front-End > Svelte (준비중)' 카테고리의 다른 글

11. Special Elements  (0) 2020.11.12
10. Slot  (0) 2020.10.29
8. 컴포넌트 생명 주기  (0) 2020.09.22
7. 바인딩  (0) 2020.09.14
6. 이벤트 처리  (0) 2020.09.08