아일랜드 간 상태 공유
아일랜드 아키텍처 / 부분 수화를 사용하여 Astro 웹 사이트를 구축할 때 다음 문제에 직면했을 수 있습니다: 컴포넌트 간 상태를 공유하고 싶습니다.
React 또는 Vue와 같은 UI 프레임워크는 다른 컴포넌트가 사용하도록 “context” providers를 권장할 수 있습니다. 하지만 Astro 또는 Markdown에서 컴포넌트를 부분적으로 수화하는 경우 이러한 컨텍스트 래퍼를 사용할 수 없습니다.
Astro는 공유 클라이언트 측 스토리지를 위한 다른 솔루션인 Nano Stores를 권장합니다.
왜 Nano Stores인가요?
섹션 제목: 왜 Nano Stores인가요?Nano Stores 라이브러리를 사용하면 모든 컴포넌트가 상호 작용할 수 있는 저장소를 작성할 수 있습니다. 다음과 같은 이유로 Nano Store를 추천합니다.
- 가볍습니다. Nano Stores는 종속성이 전혀 없는 최소한의 필수 JS (1KB 미만)를 제공합니다.
- 프레임워크에 구애받지 않습니다. 이는 프레임워크 간 상태 공유가 원활하다는 것을 의미합니다! Astro는 유연성을 기반으로 구축되었으므로 여러분의 선호도와 상관없이 유사한 개발자 경험을 제공하는 솔루션을 좋아합니다.
그래도 탐색할 수 있는 대안이 많이 있습니다. 여기에는 다음이 포함됩니다.
- Svelte의 내장 stores
- 컴포넌트 컨텍스트 외부 Solid signals
- Vue의 reactivity API
- 컴포넌트 간 사용자 정의 브라우저 이벤트 전송
🙋 .astro 파일이나 기타 서버 측 컴포넌트에서 Nano Stores를 사용할 수 있나요?
Nano Stores는 서버 측 컴포넌트에서 가져오고, 쓰고, 읽을 수 있지만 권장하지는 않습니다! 이는 몇 가지 제한 사항으로 인해 발생합니다.
.astro파일이나 수화되지 않은 컴포넌트에서 저장소에 쓰는 것은 클라이언트 측 컴포넌트에서 받은 값에 영향을 주지 않습니다.- Nano Stores를 클라이언트 측 컴포넌트에 “prop”으로 전달할 수 없습니다.
- Astro 컴포넌트는 다시 렌더링되지 않으므로
.astro파일의 변경 사항 저장을 구독할 수 없습니다.
이러한 제한 사항을 이해하고, 여전히 사용 사례를 찾고 있다면 Nano Stores를 사용해 볼 수 있습니다! Nano Stores는 특히 클라이언트의 변경 사항에 반응하도록 구축되었다는 점을 기억하세요.
🙋 Svelte stores은 Nano Stores과 어떻게 비교되나요?
Nano Stores와 Svelte stores은 매우 유사합니다! 실제로 nanostore를 사용하면 Svelte stores에서 사용할 수 있는 구독에 대해 동일한 $를 사용할 수 있습니다.
타사 라이브러리를 피하고 싶다면, Svelte stores는 그 자체로 훌륭한 아일랜드 간 통신 도구입니다. 그럼에도 불구하고, a) “objects” 및 비동기 상태에 대한 추가 기능이 마음에 들거나 b) Svelte와 Preact 또는 Vue와 같은 다른 UI 프레임워크 간 통신하려는 경우 Nano Store를 선호할 수 있습니다.
🙋 Solid signals는 Nano Stores와 어떻게 비교됩니까?
한동안 Solid를 사용해 본 적이 있다면 컴포넌트 외부에서 signals 또는 stores를 이동해 보셨을 것입니다. 이는 Solid 아일랜드 간 상태를 공유하는 좋은 방법입니다! 공유 파일에서 signals를 내보내보세요.
import { createSignal } from 'solid-js';
export const sharedCount = createSignal(0);…그리고 sharedCount를 가져오는 모든 컴포넌트는 동일한 상태를 공유합니다. 이것이 잘 작동하더라도 a) “objects” 및 비동기 상태에 대한 추가 기능이 마음에 들거나 b) Solid와 Preact 또는 Vue와 같은 다른 UI 프레임워크 간 통신하려는 경우 Nano Stores를 선호할 수 있습니다.
Nano Stores 설치
섹션 제목: Nano Stores 설치시작하려면 즐겨 사용하는 UI 프레임워크용 도우미 패키지와 함께 Nano Stores를 설치하세요.
npm install nanostores @nanostores/preactnpm install nanostores @nanostores/reactnpm install nanostores @nanostores/solidnpm install nanostores여기에는 도우미 패키지가 없습니다! Nano Stores는 표준 Svelte stores처럼 사용할 수 있습니다.
npm install nanostores @nanostores/vuenpm install nanostores @nanostores/lit여기에서 Nano Stores 사용 안내서로 이동하거나 아래 예시를 따라갈 수 있습니다!
사용 예 - 전자상거래 장바구니 플라이아웃
섹션 제목: 사용 예 - 전자상거래 장바구니 플라이아웃세 가지 대화형 요소로 간단한 전자상거래 인터페이스를 구축한다고 가정해 보겠습니다.
- “add to cart” 제출 양식
- 추가된 항목을 표시하는 장바구니 플라이아웃
- 장바구니 플라이아웃 토글
완성된 예시를 컴퓨터에서 또는 StackBlitz를 통해 온라인으로 사용해 보세요!
기본 Astro 파일은 다음과 같습니다:
---import CartFlyoutToggle from '../components/CartFlyoutToggle';import CartFlyout from '../components/CartFlyout';import AddToCartForm from '../components/AddToCartForm';---
<!DOCTYPE html><html lang="en"><head>...</head><body> <header> <nav> <a href="/">Astro storefront</a> <CartFlyoutToggle client:load /> </nav> </header> <main> <AddToCartForm client:load> <!-- ... --> </AddToCartForm> </main> <CartFlyout client:load /></body></html>“atoms” 사용
섹션 제목: “atoms” 사용CartFlyoutToggle을 클릭할 때마다 CartFlyout을 열어 시작해 보겠습니다.
먼저 저장소를 포함할 새 JS 또는 TS 파일을 만듭니다. 이를 위해 “atom”을 사용합니다.
import { atom } from 'nanostores';
export const isCartOpen = atom(false);이제 이 저장소를 읽거나 써야 하는 모든 파일로 가져올 수 있습니다. CartFlyoutToggle을 연결하는 것부터 시작하겠습니다.
import { useStore } from '@nanostores/preact';import { isCartOpen } from '../cartStore';
export default function CartButton() { // `useStore` 후크를 사용하여 저장소 값을 읽습니다. const $isCartOpen = useStore(isCartOpen); // `.set`을 사용하여 가져온 저장소에 쓰기 return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button> )}import { useStore } from '@nanostores/react';import { isCartOpen } from '../cartStore';
export default function CartButton() { // `useStore` 후크를 사용하여 저장소 값을 읽습니다. const $isCartOpen = useStore(isCartOpen); // `.set`을 사용하여 가져온 저장소에 쓰기 return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button> )}import { useStore } from '@nanostores/solid';import { isCartOpen } from '../cartStore';
export default function CartButton() { // `useStore` 후크를 사용하여 저장소 값을 읽습니다. const $isCartOpen = useStore(isCartOpen); // `.set`을 사용하여 가져온 저장소에 쓰기 return ( <button onClick={() => isCartOpen.set(!$isCartOpen())}>Cart</button> )}<script> import { isCartOpen } from '../cartStore';</script>
<!--저장소의 값을 읽으려면 "$"를 사용하세요.--><button on:click={() => isCartOpen.set(!$isCartOpen)}>Cart</button><template> <!--`.set`을 사용하여 가져온 저장소에 쓰기--> <button @click="isCartOpen.set(!$isCartOpen)">Cart</button></template>
<script setup> import { isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
// `useStore` 후크를 사용하여 저장소 값을 읽습니다. const $isCartOpen = useStore(isCartOpen);</script>import { LitElement, html } from 'lit';import { isCartOpen } from '../cartStore';
export class CartFlyoutToggle extends LitElement { handleClick() { isCartOpen.set(!isCartOpen.get()); }
render() { return html` <button @click="${this.handleClick}">Cart</button> `; }}
customElements.define('cart-flyout-toggle', CartFlyoutToggle);그런 다음 CartFlyout 컴포넌트에서 isCartOpen을 읽을 수 있습니다.
import { useStore } from '@nanostores/preact';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;}import { useStore } from '@nanostores/react';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;}import { useStore } from '@nanostores/solid';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen() ? <aside>...</aside> : null;}<script> import { isCartOpen } from '../cartStore';</script>
{#if $isCartOpen}<aside>...</aside>{/if}<template> <aside v-if="$isCartOpen">...</aside></template>
<script setup> import { isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
const $isCartOpen = useStore(isCartOpen);</script>import { isCartOpen } from '../cartStore';import { LitElement, html } from 'lit';import { StoreController } from '@nanostores/lit';
export class CartFlyout extends LitElement { private cartOpen = new StoreController(this, isCartOpen);
render() { return this.cartOpen.value ? html`<aside>...</aside>` : null; }}
customElements.define('cart-flyout', CartFlyout);“maps” 사용
섹션 제목: “maps” 사용Maps는 정기적으로 작성하는 객체에 대한 훌륭한 선택입니다! atom이 제공하는 표준 get() 및 set() 도우미와 함께 개별 객체 키를 효율적으로 업데이트하는 .setKey() 함수도 있습니다.
이제 장바구니에 담긴 품목을 추적해 보겠습니다. 중복을 방지하고 “수량”을 추적하기 위해 항목 ID를 키로 사용하여 장바구니를 객체로 저장할 수 있습니다. 이를 위해 Map를 사용하겠습니다.
앞서 cartStore.js에 cartItem 저장소를 추가해 보겠습니다. 원하는 경우 TypeScript 파일로 전환하여 모양을 정의할 수도 있습니다.
import { atom, map } from 'nanostores';
export const isCartOpen = atom(false);
/** * @typedef {Object} CartItem * @property {string} id * @property {string} name * @property {string} imageSrc * @property {number} quantity */
/** @type {import('nanostores').MapStore<Record<string, CartItem>>} */export const cartItems = map({});import { atom, map } from 'nanostores';
export const isCartOpen = atom(false);
export type CartItem = { id: string; name: string; imageSrc: string; quantity: number;}
export const cartItems = map<Record<string, CartItem>>({});이제 컴포넌트가 사용할 addCartItem 도우미를 내보내 보겠습니다.
- 해당 품목이 장바구니에 없으면 시작 수량 1로 품목을 추가하세요.
- 해당 항목이 이미 존재하는 경우, 수량을 1 늘립니다.
...export function addCartItem({ id, name, imageSrc }) { const existingEntry = cartItems.get()[id]; if (existingEntry) { cartItems.setKey(id, { ...existingEntry, quantity: existingEntry.quantity + 1, }) } else { cartItems.setKey( id, { id, name, imageSrc, quantity: 1 } ); }}...type ItemDisplayInfo = Pick<CartItem, 'id' | 'name' | 'imageSrc'>;export function addCartItem({ id, name, imageSrc }: ItemDisplayInfo) { const existingEntry = cartItems.get()[id]; if (existingEntry) { cartItems.setKey(id, { ...existingEntry, quantity: existingEntry.quantity + 1, }); } else { cartItems.setKey( id, { id, name, imageSrc, quantity: 1 } ); }}🙋 여기서 useStore 도우미 대신 .get()을 사용하는 이유는 무엇입니까?
React / Preact / Solid / Vue 예시에서 useStore 도우미를 가져오는 대신 여기서 cartItems.get()을 호출하고 있다는 것을 눈치챘을 것입니다. 이는 useStore가 컴포넌트를 다시 렌더링을 트리거하기 위한 것이기 때문입니다. 즉, 저장소 값이 UI에 렌더링될 때마다 useStore를 사용해야 합니다. 이벤트가 트리거될 때 (이 경우 addToCart) 값을 읽고 해당 값을 렌더링하려고 하지 않으므로 여기서는 useStore가 필요하지 않습니다.
저장소가 준비되면 해당 양식이 제출될 때마다 AddToCartForm 내에서 이 함수를 호출할 수 있습니다. 또한 전체 장바구니 요약을 볼 수 있도록 장바구니 플라이아웃을 열겠습니다.
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // 단순화를 위해 항목 정보를 하드코딩하겠습니다! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // 단순화를 위해 항목 정보를 하드코딩하겠습니다! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // 단순화를 위해 항목 정보를 하드코딩하겠습니다! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}<form on:submit|preventDefault={addToCart}> <slot></slot></form>
<script> import { addCartItem, isCartOpen } from '../cartStore';
// 단순화를 위해 항목 정보를 하드코딩하겠습니다! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart() { isCartOpen.set(true); addCartItem(hardcodedItemInfo); }</script><template> <form @submit="addToCart"> <slot></slot> </form></template>
<script setup> import { addCartItem, isCartOpen } from '../cartStore';
// 단순화를 위해 항목 정보를 하드코딩하겠습니다! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }</script>import { LitElement, html } from 'lit';import { isCartOpen, addCartItem } from '../cartStore';
export class AddToCartForm extends LitElement { static get properties() { return { item: { type: Object }, }; }
constructor() { super(); this.item = {}; }
addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(this.item); }
render() { return html` <form @submit="${this.addToCart}"> <slot></slot> </form> `; }}customElements.define('add-to-cart-form', AddToCartForm);마지막으로 CartFlyout 내부에 장바구니 항목을 렌더링합니다.
import { useStore } from '@nanostores/preact';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen ? ( <aside> {Object.values($cartItems).length ? ( <ul> {Object.values($cartItems).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}import { useStore } from '@nanostores/react';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen ? ( <aside> {Object.values($cartItems).length ? ( <ul> {Object.values($cartItems).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}import { useStore } from '@nanostores/solid';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen() ? ( <aside> {Object.values($cartItems()).length ? ( <ul> {Object.values($cartItems()).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}<script> import { isCartOpen, cartItems } from '../cartStore';</script>
{#if $isCartOpen} {#if Object.values($cartItems).length} <aside> {#each Object.values($cartItems) as cartItem} <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> {/each} </aside> {:else} <p>Your cart is empty!</p> {/if}{/if}<template> <aside v-if="$isCartOpen"> <ul v-if="Object.values($cartItems).length"> <li v-for="cartItem in Object.values($cartItems)" v-bind:key="cartItem.name"> <img :src=cartItem.imageSrc :alt=cartItem.name /> <h3>{{cartItem.name}}</h3> <p>Quantity: {{cartItem.quantity}}</p> </li> </ul> <p v-else>Your cart is empty!</p> </aside></template>
<script setup> import { cartItems, isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);</script>import { LitElement, html } from 'lit';import { isCartOpen, cartItems } from '../cartStore';import { StoreController } from '@nanostores/lit';
export class CartFlyoutLit extends LitElement { private cartOpen = new StoreController(this, isCartOpen); private getCartItems = new StoreController(this, cartItems);
renderCartItem(cartItem) { return html` <li> <img src="${cartItem.imageSrc}" alt="${cartItem.name}" /> <h3>${cartItem.name}</h3> <p>Quantity: ${cartItem.quantity}</p> </li> `; }
render() { return this.cartOpen.value ? html` <aside> ${ Object.values(this.getCartItems.value).length ? html` <ul> ${Object.values(this.getCartItems.value).map((cartItem) => this.renderCartItem(cartItem) )} </ul> ` : html`<p>Your cart is empty!</p>` } </aside> ` : null; }}
customElements.define('cart-flyout', CartFlyoutLit);이제 은하계에서 가장 작은 JS 번들이 포함된 완전한 대화형 전자상거래 예시가 생겼습니다. 🚀
완성된 예시를 컴퓨터에서 또는 StackBlitz를 통해 온라인으로 사용해 보세요!
Recipes