๐Ÿš€ Svelte ๋น ๋ฅธ ํŒ: ํ† ์ŠคํŠธ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๋งŒ๋“ค๊ธฐ

16540 ๋‹จ์–ด sveltejavascripttoastsui

๋‹น์‹ ์ด ๋งํ•˜๋Š” ํ† ์ŠคํŠธ? ๐Ÿž



์ผ๋ฐ˜์ ์ธ UI ๋””์ž์ธ ํŒจํ„ด์€ "ํ† ์ŠคํŠธ"๋˜๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ผ์–ด๋‚˜๋Š” ์ผ(์˜ˆ: ์–‘์‹ ์ œ์ถœ ์˜ค๋ฅ˜, ์ƒˆ ๋ฉ”์‹œ์ง€ ๋˜๋Š” ์นœ๊ตฌ ์š”์ฒญ ๋“ฑ)์„ ์•Œ๋ฆฌ๋Š” ์ž‘์€ UI ์•Œ๋ฆผ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด ๊ธฐ์‚ฌ์—์„œ๋Š” Svelte์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ ํ† ์ŠคํŠธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.



์งœ์ฆ๋‚œ? ์ฐธ์กฐ REPL here


ํ† ์ŠคํŠธ ์•Œ๋ฆผ์„ ์œ„ํ•œ Svelte ์Šคํ† ์–ด ๋งŒ๋“ค๊ธฐ



ํ† ์ŠคํŠธ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ Svelte ์Šคํ† ์–ด๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ €์žฅ์†Œ์—๋Š” ์ƒˆ ํ† ์ŠคํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ "ํ•ด์ œ"๋  ๋•Œ ์—…๋ฐ์ดํŠธํ•  ๋ฐฐ์—ด์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

import { writable } from 'svelte/store'

export const toasts = writable([])

export const dismissToast = (id) => {
  toasts.update((all) => all.filter((t) => t.id !== id))
}

export const addToast = (toast) => {
  // Create a unique ID so we can easily find/remove it
  // if it is dismissible/has a timeout.
  const id = Math.floor(Math.random() * 10000)

  // Setup some sensible defaults for a toast.
  const defaults = {
    id,
    type: 'info',
    dismissible: true,
    timeout: 3000,
  }

  // Push the toast to the top of the list of toasts
  const t = { ...defaults, ...toast }
  toasts.update((all) => [t, ...all])

  // If toast is dismissible, dismiss it after "timeout" amount of time.
  if (t.timeout) setTimeout(() => dismissToast(id), t.timeout)
}


์ „๋ฐ˜์ ์œผ๋กœ ์ด๊ฒƒ์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋‚˜๋Š” ํ† ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด๊ณ  ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ† ์ŠคํŠธ์— timeout ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ํ† ์ŠคํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด ์‹œ๊ฐ„ ์ดˆ๊ณผ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋ชจ๋“  ํ† ์ŠคํŠธ์— ๋ช‡ ๊ฐ€์ง€ ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•˜๊ณ  ํ† ์ŠคํŠธ์— id๋ฅผ ์ œ๊ณตํ•˜์—ฌ ๋” ์‰ฝ๊ฒŒ ์ถ”๊ฐ€/์ œ๊ฑฐํ•˜๊ณ  Svelte์˜ ํƒœ๊ทธ{#each}๊ฐ€ ๋” ๋‚˜์€ ์ƒ‰์ธ์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.


ํ† ์ŠคํŠธ ๋ถ€๋ชจ ๊ตฌ์„ฑ ์š”์†Œ ๋งŒ๋“ค๊ธฐ




<script lang="ts">
  import Toast from './Toast.svelte'

  import { dismissToast, toasts } from './store'
</script>

{#if $toasts}
  <section>
    {#each $toasts as toast (toast.id)}
      <Toast
        type={toast.type}
        dismissible={toast.dismissible}
        on:dismiss={() => dismissToast(toast.id)}>{toast.message}</Toast>
    {/each}
  </section>
{/if}

<style lang="postcss">
  section {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    display: flex;
    margin-top: 1rem;
    justify-content: center;
    flex-direction: column;
    z-index: 1000;
  }
</style>



ํ† ์ŠคํŠธ ๊ตฌ์„ฑ ์š”์†Œ ๋งŒ๋“ค๊ธฐ



๋‹ค์Œ์œผ๋กœ ์„ฑ๊ณต, ์˜ค๋ฅ˜ ๋ฐ ์ •๋ณด์™€ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ Toast.svelte ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.



<script>
  import { createEventDispatcher } from 'svelte'
  import { fade } from 'svelte/transition'
  import SuccessIcon from './SuccessIcon.svelte'
  import ErrorIcon from './ErrorIcon.svelte'
  import InfoIcon from './InfoIcon.svelte'
  import CloseIcon from './CloseIcon.svelte'

  const dispatch = createEventDispatcher()

  export let type = 'error'
  export let dismissible = true
</script>

<article class={type} role="alert" transition:fade>
  {#if type === 'success'}
    <SuccessIcon width="1.1em" />
  {:else if type === 'error'}
    <ErrorIcon width="1.1em" />
  {:else}
    <InfoIcon width="1.1em" />
  {/if}

  <div class="text">
    <slot />
  </div>

  {#if dismissible}
    <button class="close" on:click={() => dispatch('dismiss')}>
      <CloseIcon width="0.8em" />
    </button>
  {/if}
</article>

<style lang="postcss">
  article {
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.2rem;
    display: flex;
    align-items: center;
    margin: 0 auto 0.5rem auto;
    width: 20rem;
  }
  .error {
    background: IndianRed;
  }
  .success {
    background: MediumSeaGreen;
  }
  .info {
    background: SkyBlue;
  }
  .text {
    margin-left: 1rem;
  }
  button {
    color: white;
    background: transparent;
    border: 0 none;
    padding: 0;
    margin: 0 0 0 auto;
    line-height: 1;
    font-size: 1rem;
  }
</style>


๋ฐ”๋ผ๊ฑด๋Œ€ ์ด ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ถ•๋ฐฐ๋ฅผ ์œ„ํ•œ ์ผ๋ถ€ ์Šคํƒ€์ผ์ผ ๋ฟ์ด๋ฉฐ "ํ•ด์ œ ๊ฐ€๋Šฅ"ํ•˜๊ณ  ์•„์ด์ฝ˜ ๊ตฌ์„ฑ ์š”์†Œ(SVG์ผ ๋ฟ์ž„)๊ฐ€ ์˜ค๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ๋ช‡ ๊ฐ€์ง€ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.


ํ† ์ŠคํŠธ ์•Œ๋ฆผ ๋งŒ๋“ค๊ธฐ



์ด์ œ Svelte ์•ฑ(JS ํŒŒ์ผ ๋˜๋Š” .svelte ํŒŒ์ผ)์˜ ์–ด๋Š ๊ณณ์—์„œ๋‚˜ ํ† ์ŠคํŠธ ์•Œ๋ฆผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { addToast } from "./store";

addToast({
  message: "Hello, World!",
  type: "success",
  dismissible: true,
  timeout: 3000,
});


๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ ์š”์†Œ์˜ ์–ด๋”˜๊ฐ€์—์„œ <Toasts /> ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์˜ˆ: App.svelte ๋˜๋Š” _layout.svelte ๋“ฑ).


๋งˆ๋ฌด๋ฆฌ ๐ŸŒฏ



๊ทธ๊ฒŒ ๋‹ค์•ผ ์—ฌ๋Ÿฌ๋ถ„, ์˜ค๋Š˜ ๋ญ”๊ฐ€๋ฅผ ๋ฐฐ์šฐ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค!

Svelte REPL here์—์„œ ์ „์ฒด ํ† ์ŠคํŠธ ์‹œ์Šคํ…œ์„ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.

์ฝ์–ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!


์ฝ์–ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ์ด ๊ฒŒ์‹œ๋ฌผ์— โค๏ธ, ๐Ÿฆ„ ๋˜๋Š” ๐Ÿ”–๋ฅผ ์ง€์ •ํ•˜์—ฌ ๋‚˜์ค‘์— ๋ถ๋งˆํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ’•

๋‹ค๋ฅธ ํŒ, ์•„์ด๋””์–ด, ํ”ผ๋“œ๋ฐฑ ๋˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๊นŒ? ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”! ๐Ÿ™‹โ€โ™‚๏ธ

Dev.to( ), Twitter( ) ๋ฐ/๋˜๋Š” Github(danawoodman )์—์„œ ์ €๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ์„ธ์š”!

์‚ฌ์ง„ ์ œ๊ณต: Joshua Aragon on Unsplash

์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ