아일랜드 간 상태 공유
아일랜드 아키텍처 / 부분 수화를 사용하여 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
- 컴포넌트 간 사용자 정의 브라우저 이벤트 전송
Nano Stores 설치
섹션 제목: “Nano Stores 설치”시작하려면 즐겨 사용하는 UI 프레임워크용 도우미 패키지와 함께 Nano Stores를 설치하세요.
npm install nanostores @nanostores/preactnpm install nanostores @nanostores/reactnpm install nanostores @nanostores/solidnpm install nanostoresnpm install nanostores @nanostores/vue여기에서 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>그런 다음 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>”maps” 사용
섹션 제목: “”maps” 사용”이제 장바구니에 담긴 품목을 추적해 보겠습니다. 중복을 방지하고 “수량”을 추적하기 위해 항목 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 } ); }}저장소가 준비되면 해당 양식이 제출될 때마다 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>마지막으로 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>이제 은하계에서 가장 작은 JS 번들이 포함된 완전한 대화형 전자상거래 예시가 생겼습니다. 🚀
완성된 예시를 컴퓨터에서 또는 StackBlitz를 통해 온라인으로 사용해 보세요!
Recipes