고급 액션, 스토어 및 트랜지션을 활용한 Svelte 반응형 핵심 마스터하기
Olivia Novak
Dev Intern · Leapcell

소개
프런트엔드 개발의 끊임없이 진화하는 환경에서 성능, 개발자 경험 및 번들 크기는 여전히 가장 중요한 관심사입니다. Svelte와 같은 프레임워크는 일반적으로 반응성과 관련된 많은 런타임 오버헤드를 "컴파일되어" 제거하는 고유한 접근 방식을 제공하며 강력한 경쟁자로 등장했습니다.
Svelte의 핵심 개념인 컴포넌트, 반응성 및 바인딩은 직관적이지만, 잠재력을 최대한 활용하려면 고급 기능을 더 깊이 파고들어야 하는 경우가 많습니다.
특히 Svelte 액션, 스토어 및 트랜지션은 기본 응용 프로그램을 넘어 사용될 때 상호 작용성, 유지 관리성 및 세련된 사용자 경험의 새로운 수준을 열 수 있습니다. 이 기사에서는 이러한 Svelte 기본 요소의 고급 사용법을 탐구하여 더 복잡하고 강력하며 성능이 뛰어난 웹 애플리케이션을 구축하는 방법을 보여줍니다.
Svelte의 고급 기능 심층 분석
고급 기법을 살펴보기 전에 논의의 기반이 되는 핵심 개념을 간략하게 정의해 보겠습니다.
- Svelte 액션: 요소가 마운트될 때 호출되는 함수로,
update
메소드(액션 매개변수가 변경될 때 호출됨)와destroy
메소드(요소가 마운트 해제될 때 호출됨)를 반환할 수 있습니다. 액션은 DOM 상호 작용 또는 요소 자체의 수명 주기 로직을 캡슐화하는 데 매우 강력합니다. - Svelte 스토어: 상태를 보유하고 값이 변경될 때 구독자에게 알리는 객체입니다. Svelte는 간단한 writable, readable 및 derived 스토어를 제공하며, 이는 반응형 상태 관리의 기반을 형성합니다.
- Svelte 트랜지션: 트랜지션은 요소가 DOM에 추가되거나 제거될 때 적용되는 애니메이션입니다.
Svelte는 일련의 내장 트랜지션과 사용자 정의 트랜지션을 생성하기 위한 유연한 API를 제공하여 부드럽고 매력적인 UI 변경을 보장합니다.
고급 Svelte 액션: 요소 동작 조정
기본 액션은 툴팁이나 드래그 앤 드롭과 같은 간단한 작업에는 훌륭하지만, 진정한 힘은 복잡한 요소 동작을 조정하고 외부 라이브러리와 통합하는 데 있습니다.
원칙: DOM 중심 로직 캡슐화
고급 액션의 핵심 아이디어는 모든 DOM 조작 또는 수명 주기에 따른 로직을 재사용 가능한 액션으로 캡슐화하는 것입니다. 이렇게 하면 컴포넌트 스크립트가 깔끔하게 유지되고 데이터 흐름에 집중하는 반면, 액션은 DOM과 상호 작용하는 복잡한 세부 정보를 처리합니다.
예시: "외부 클릭" 리스너 구현
일반적인 UI 패턴은 사용자가 드롭다운이나 모달 외부를 클릭할 때 이를 닫는 것입니다. 이는 Svelte 액션을 사용하여 우아하게 구현할 수 있습니다.
<!-- ClickOutside.svelte --> <script> import { createEventDispatcher } from 'svelte'; import { tick } from 'svelte'; const dispatch = createEventDispatcher(); export function clickOutside(node) { const handleClick = (event) => { if (node && !node.contains(event.target) && !event.defaultPrevented) { dispatch('clickoutside'); } }; // 컴포넌트가 렌더링된 후 이벤트 수신기가 추가되도록 tick을 사용합니다. // 구성 요소가 사용자 작업 직후 직접 마운트되는 경우 즉시 트리거되는 것을 방지합니다. tick().then(() => { document.addEventListener('click', handleClick, true); // true는 캡처 단계용 }); return { destroy() { document.removeEventListener('click', handleClick, true); } }; } </script> <div use:clickOutside on:clickoutside={() => alert('외부를 클릭했습니다!')}> 내부 클릭! </div>
적용 시나리오: 이 액션은 사용자 정의 드롭다운, 탐색 메뉴, 모달 또는 경계를 벗어난 상호 작용에 반응해야 하는 모든 UI 요소를 구축하는 데 매우 유용하며, 모듈성 및 재사용성을 향상시킵니다.
예시: 구성 가능한 동적 툴팁
액션은 매개변수를 허용하여 동적으로 구성할 수도 있습니다. 내용과 배치를 구성할 수 있는 툴팁 액션을 만들어 보겠습니다.
<!-- Tooltip.svelte --> <script> // .tooltip 및 .tooltip-content에 대한 CSS 프레임워크 또는 사용자 정의 스타일을 가정하세요. export function tooltip(node, params) { let tooltipEl; function updateTooltip(newParams) { const { content, placement = 'top' } = newParams; if (!content) return; if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.className = `tooltip tooltip-${placement}`; document.body.appendChild(tooltipEl); node.addEventListener('mouseenter', showTooltip); node.addEventListener('mouseleave', hideTooltip); node.addEventListener('focus', showTooltip); node.addEventListener('blur', hideTooltip); } tooltipEl.innerHTML = content; positionTooltip(tooltipEl, node, placement); } function showTooltip() { if (tooltipEl) tooltipEl.style.display = 'block'; } function hideTooltip() { if (tooltipEl) tooltipEl.style.display = 'none'; } function positionTooltip(tip, target, placement) { const targetRect = target.getBoundingClientRect(); const tipRect = tip.getBoundingClientRect(); let top, left; switch (placement) { case 'top': top = targetRect.top - tipRect.height - 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); break; case 'bottom': top = targetRect.bottom + 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); break; // ... 더 많은 배치를 추가하세요. default: top = targetRect.top - tipRect.height - 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); } tip.style.top = `${top + window.scrollY}px`; tip.style.left = `${left + window.scrollX}px`; } updateTooltip(params); // 초기 설정 return { update(newParams) { updateTooltip(newParams); }, destroy() { if (tooltipEl) { document.body.removeChild(tooltipEl); node.removeEventListener('mouseenter', showTooltip); node.removeEventListener('mouseleave', hideTooltip); node.removeEventListener('focus', showTooltip); node.removeEventListener('blur', hideTooltip); } } }; } </script> <button use:tooltip={{ content: '이것은 Svelte 툴팁입니다!', placement: 'bottom' }}>나를 호버하세요</button>
적용 시나리오: 이 동적 툴팁 액션은 다양한 컴포넌트에서 사용할 수 있으며, 컴포넌트 로직을 어지럽히지 않고 일관되고 구성 가능한 방식으로 컨텍스트 정보를 제공합니다.
고급 Svelte 스토어: 단순한 상태를 넘어서
Svelte 스토어는 간단한 전역 상태를 위해 자주 소개됩니다. 하지만 derived
및 사용자 정의 스토어 기능을 통해 복잡한 반응형 데이터 관리를 위한 강력한 패턴을 제공하며, 특히 비동기 작업 또는 다른 상태 조각 간의 관계를 처리할 때 유용합니다.
원칙: 파생 상태 및 사용자 정의 스토어 로직
고급 스토어 사용은 기존 스토어에서 새 반응형 스토어를 생성(derived
)하고 사용자 정의 스토어 내에서 비동기 작업을 포함한 복잡한 로직을 캡슐화하는 데 중점을 둡니다.
이렇게 하면 컴포넌트 로직이 선언적으로 유지되고 스토어 값의 변경에만 반응하게 됩니다.
예시: "받기 상태" 스토어
데이터를 가져오고 있으며 데이터 자체뿐만 아니라 로딩 상태 및 잠재적 오류도 추적한다고 상상해 보세요.
사용자 정의 스토어는 이 전체 수명 주기를 캡슐화할 수 있습니다.
// stores/fetchStatus.js import { writable, get } from 'svelte/store'; export function createFetchStatusStore(initialData = null) { const { subscribe, set, update } = writable({ data: initialData, loading: false, error: null, }); async function fetchData(url, options = {}) { update(s => ({ ...s, loading: true, error: null })); try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP 오류! 상태: ${response.status}`); } const data = await response.json(); set({ data, loading: false, error: null }); return data; // 외부 데이터 접근 허용 } catch (error) { set({ data: initialData, loading: false, error: error.message }); throw error; // 오류 전파 } } function reset() { set({ data: initialData, loading: false, error: null }); } return { subscribe, fetchData, reset, get value() { // 편의를 위한 getter. return get({ subscribe }); } }; }
<!--컴포넌트에서의 사용--> <script> import { createFetchStatusStore } from './stores/fetchStatus.js'; const userStore = createFetchStatusStore(); let userId = 1; async function loadUser() { try { await userStore.fetchData(`https://jsonplaceholder.typicode.com/users/${userId}`); } catch (e) { console.error('사용자 로드 실패:', e); } } $: if (userId) { // userId 변경 시 반응형 데이터 가져오기. loadUser(); } </script> <div> <h2>사용자 세부 정보</h2> {#if $userStore.loading} <p>사용자 로딩 중...</p> {:else if $userStore.error} <p class="error">오류: {$userStore.error}</p> {:else if $userStore.data} <p>이름: {$userStore.data.name}</p> <p>이메일: {$userStore.data.email}</p> {/if} <input type="number" bind:value={userId} min="1" max="10" /> </div> <style> .error { color: red; } </style>
적용 시나리오: 이 패턴은 모든 비동기 데이터 가져오기, 양식 제출 또는 사용자에게 명확한 피드백(로딩, 성공, 오류)을 표시해야 하는 장기 실행 프로세스를 관리하는 데 이상적입니다.
로직을 중앙 집중화하여 컴포넌트를 더 깔끔하고 렌더링에 더 집중할 수 있습니다.
예시: 복잡한 필터링을 위한 파생 스토어
여러 기준에 따라 필터링하고 정렬해야 하는 항목 목록을 고려해 보세요.
파생 스토어는 반응형이고 효율적인 솔루션을 제공할 수 있습니다.
// stores/itemStore.js import { writable, derived } from 'svelte/store'; const items = writable([ { id: 1, name: 'Apple', category: 'Fruit', price: 1.0 }, { id: 2, name: 'Carrot', category: 'Vegetable', price: 0.5 }, { id: 3, name: 'Banana', category: 'Fruit', price: 1.2 }, { id: 4, name: 'Broccoli', category: 'Vegetable', price: 0.8 }, { id: 5, name: 'Orange', category: 'Fruit', price: 1.1 }, ]); export const searchTerm = writable(''); export const selectedCategory = writable('All'); export const sortBy = writable('name'); // 'name', 'price'. export const filteredAndSortedItems = derived( [items, searchTerm, selectedCategory, sortBy], ([$items, $searchTerm, $selectedCategory, $sortBy]) => { let filtered = $items.filter(item => item.name.toLowerCase().includes($searchTerm.toLowerCase()) && ($selectedCategory === 'All' || item.category === $selectedCategory) ); filtered.sort((a, b) => { if ($sortBy === 'name') { return a.name.localeCompare(b.name); } else if ($sortBy === 'price') { return a.price - b.price; } return 0; }); return filtered; } );
<!--컴포넌트에서의 사용--> <script> import { searchTerm, selectedCategory, sortBy, filteredAndSortedItems } from './stores/itemStore.js'; const categories = ['All', 'Fruit', 'Vegetable']; </script> <div> <input type="text" placeholder="항목 검색..." bind:value={$searchTerm} /> <select bind:value={$selectedCategory}> {#each categories as category} <option value={category}>{category}</option> {/each} </select> <select bind:value={$sortBy}> <option value="name">이름순 정렬</option> <option value="price">가격순 정렬</option> </select> <ul> {#each $filteredAndSortedItems as item (item.id)} <li>{item.name} - {item.category} - ${item.price.toFixed(2)}</li> {/each} </ul> </div>
적용 시나리오: 이는 대시보드, 제품 목록 또는 필터링, 정렬 또는 페이징을 통해 데이터를 대화식으로 탐색해야 하는 모든 응용 프로그램에 적합합니다.
이는 필터의 모든 변경 사항이 명시적인 다시 렌더링 호출 없이 표시되는 데이터를 즉시 업데이트하는 매우 반응적인 UI를 촉진합니다.
고급 Svelte 트랜지션: 완벽한 사용자 경험 만들기
Svelte의 내장 트랜지션 (fade
, slide
, scale
, fly
, blur
, draw
)은 훌륭하지만, 사용자 정의 트랜지션 및 트랜지션 그룹을 사용하면 매우 구체적이고 조정된 애니메이션을 생성하여 진정으로 세련된 사용자 인터페이스를 만들 수 있습니다.
원칙: 중단 동작 및 조정된 애니메이션 사용자 정의
고급 트랜지션에는 종종 사용자 정의 이징 함수, 수명 주기에 대한 정확한 제어 및 여러 트랜지션 효과 조정이 포함됩니다.
핵심 측면은 트랜지션이 빠르게 트리거될 때 작동하는 방식을 관리하여 거슬리는 점프를 방지하는 것입니다.
예시: "스퀴시" 사용자 정의 트랜지션
스프링과 같은 움직임을 사용하는 사용자 정의 squish
트랜지션을 만들어 보겠습니다.
<!--컴포넌트에서의 사용--> <script> import { elasticOut } from 'svelte/easing'; let show = false; function squish(node, { duration = 400 }) { return { duration, easing: elasticOut, css: (t, u) => ` transform: scaleY(${t}) scaleX(${1 + u * 0.1}); opacity: ${t}; ` }; } </script> <style> .squishy { background-color: lightblue; padding: 20px; border-radius: 8px; display: inline-block; margin-top: 20px; } </style> <button on:click={() => (show = !show)}>스퀴시 박스 토글</button> {#if show} <div class="squishy" transition:squish> 안녕하세요, 스퀴시 세계! </div> {/if}
적용 시나리오: squish
와 같은 사용자 정의 트랜지션은 애플리케이션에 고유한 브랜드 느낌을 더하여 요소를 독특하고 기억에 남는 방식으로 애니메이션화하도록 할 수 있습니다.
컨펌, 알림 또는 눈에 띄어야 하는 중요한 UI 요소에 특히 효과적입니다.
예시: 조정된 그룹 트랜지션
여러 요소가 동시에 추가되거나 제거될 때 특정 시퀀스 또는 지연 효과로 애니메이션화하고 싶을 수 있습니다.
Svelte의 crossfade
는 특정 사용 사례를 위한 것이지만, 조정된 트랜지션의 힘을 암시합니다.
더 일반적인 접근 방식은 종종 사용자 정의 트랜지션과 {#each}
블록 및 지연을 결합하는 것입니다.
<!-- CoordinatedList.svelte --> <script> import { fly } from 'svelte/transition'; import { cubicOut } from 'svelte/easing'; let items = ['Item 1', 'Item 2', 'Item 3']; let counter = items.length + 1; function addItem() { items = [...items, `Item ${counter}`]; counter++; } function removeItem(itemToRemove) { items = items.filter(item => item !== itemToRemove); } // 인덱스에 따라 지연을 생성하는 도우미 함수 function delayedFly(node, { delayIn = 0, delayOut = 0, duration = 400, ...rest }) { return { ...fly(node, { delay: delayIn, duration, easing: cubicOut, y: -10, // 약간 위로 날림. ...rest }), out: fly(node, { delay: delayOut, duration, easing: cubicOut, y: 10, // 약간 아래로 날림. ...rest }) }; } </script> <style> ul { list-style: none; padding: 0; } li { background-color: #f0f0f0; margin-bottom: 5px; padding: 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; } button { margin-left: 10px; } </style> <button on:click={addItem}>항목 추가</button> <ul> {#each items as item, i (item)} <li transition:delayedFly={{ delayIn: i * 50, delayOut: 0 }}> {item} <button on:click={() => removeItem(item)}>제거</button> </li> {/each} </ul>
적용 시나리오: 이 지연 애니메이션 기법은 검색 결과 목록, 알림 스택 또는 항목이 동적으로 추가되거나 제거되는 모든 시나리오에 적합하여 시각적 신호와 더 부드러운 사용자 경험을 제공합니다.
delayIn
매개변수는 들어오는 요소에 대한 "폭포" 효과를 만듭니다.
결론
Svelte 액션, 스토어 및 트랜지션은 기초 개념 그 이상입니다. 창의적으로 이해하고 적용될 때 개발자가 매우 상호 작용적이고 성능이 뛰어나며 유지 관리 가능한 웹 애플리케이션을 구축할 수 있도록 하는 강력한 기본 요소입니다.
액션에 DOM 로직을 캡슐화하고, 사용자 정의 및 파생 스토어로 복잡한 상태를 관리하고, 고급 트랜지션으로 완벽한 사용자 경험을 제작함으로써 Svelte 프로젝트를 전문적인 수준으로 끌어올릴 수 있습니다.
이러한 고급 기법을 마스터하면 선언적이고 효율적인 프런트엔드 개발 접근 방식을 사용할 수 있어 보일러플레이트를 최소화하고 유연성을 극대화할 수 있습니다.
궁극적으로 이러한 고급 패턴을 통해 Svelte의 반응형 핵심을 활용함으로써 성능이 뛰어날 뿐만 아니라 구축하고 사용하는 데 즐거움을 주는 애플리케이션을 개발할 수 있습니다.