Action — use: 디렉티브로 DOM을 직접 다루기
React의 커스텀 Hook이 로직을 재사용한다면, Svelte의 Action은 DOM 조작 로직을 재사용합니다.
개념 정의
Action 은 요소가 DOM에 마운트될 때 실행되는 함수입니다. use: 디렉티브로 적용하며, 재사용 가능한 DOM 관련 로직을 캡슐화합니다. 툴팁, 클릭 외부 감지, 포커스 트랩 같은 기능에 적합합니다.
기본 구조
// action 함수 시그니처
function myAction(node, parameter) {
// node가 DOM에 마운트될 때 실행
return {
update(newParameter) {
// parameter가 변경될 때 실행
},
destroy() {
// node가 DOM에서 제거될 때 실행 (정리)
}
};
}
<div use:myAction={parameter}>
Action이 적용된 요소
</div>
실전 예시 — 클릭 외부 감지
<script>
let showDropdown = $state(false);
// 요소 외부 클릭 감지 Action
function clickOutside(node, callback) {
function handleClick(event) {
if (!node.contains(event.target)) {
callback();
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
</script>
<div class="dropdown-wrapper" use:clickOutside={() => showDropdown = false}>
<button onclick={() => showDropdown = !showDropdown}>메뉴</button>
{#if showDropdown}
<ul class="dropdown">
<li>옵션 1</li>
<li>옵션 2</li>
<li>옵션 3</li>
</ul>
{/if}
</div>
툴팁 Action
<script>
function tooltip(node, text) {
let tooltipEl;
function show() {
tooltipEl = document.createElement('div');
tooltipEl.className = 'tooltip';
tooltipEl.textContent = text;
const rect = node.getBoundingClientRect();
tooltipEl.style.cssText = `
position: fixed;
top: ${rect.top - 30}px;
left: ${rect.left + rect.width / 2}px;
transform: translateX(-50%);
background: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
`;
document.body.appendChild(tooltipEl);
}
function hide() {
tooltipEl?.remove();
}
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update(newText) {
text = newText;
},
destroy() {
hide();
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
}
};
}
let tooltipText = $state('클릭하세요!');
</script>
<button use:tooltip={tooltipText}>
마우스를 올려보세요
</button>
<input bind:value={tooltipText} placeholder="툴팁 텍스트 변경" />
자동 포커스 Action
<script>
function autofocus(node, options = {}) {
const { delay = 0, select = false } = options;
setTimeout(() => {
node.focus();
if (select && node.select) {
node.select();
}
}, delay);
}
</script>
<!-- 마운트 시 자동 포커스 -->
<input use:autofocus placeholder="자동 포커스" />
<!-- 딜레이 후 포커스 + 텍스트 선택 -->
<input use:autofocus={{ delay: 500, select: true }} value="선택될 텍스트" />
롱프레스 Action
<script>
function longpress(node, { duration = 500, onLongpress }) {
let timer;
function start() {
timer = setTimeout(() => {
onLongpress?.();
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
}
function cancel() {
clearTimeout(timer);
}
node.addEventListener('mousedown', start);
node.addEventListener('mouseup', cancel);
node.addEventListener('mouseleave', cancel);
node.addEventListener('touchstart', start);
node.addEventListener('touchend', cancel);
return {
update(newParams) {
duration = newParams.duration ?? duration;
onLongpress = newParams.onLongpress;
},
destroy() {
cancel();
node.removeEventListener('mousedown', start);
node.removeEventListener('mouseup', cancel);
node.removeEventListener('mouseleave', cancel);
node.removeEventListener('touchstart', start);
node.removeEventListener('touchend', cancel);
}
};
}
function handleLongpress() {
alert('롱프레스 감지!');
}
</script>
<button use:longpress={{ duration: 800, onLongpress: handleLongpress }}>
길게 누르세요 (0.8초)
</button>
IntersectionObserver Action
<script>
function inview(node, { threshold = 0.5, once = false }) {
let triggered = false;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !(once && triggered)) {
triggered = true;
node.dispatchEvent(new CustomEvent('inview'));
node.classList.add('in-view');
} else if (!entry.isIntersecting && !once) {
node.classList.remove('in-view');
}
},
{ threshold }
);
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}
let count = $state(0);
</script>
<div style="height: 150vh;">스크롤 해보세요</div>
<div
use:inview={{ threshold: 0.3, once: true }}
oninview={() => count++}
class="animate-target"
>
화면에 나타날 때 애니메이션! (감지 횟수: {count})
</div>
<style>
.animate-target {
opacity: 0; transform: translateY(20px);
transition: all 0.6s ease;
}
.animate-target.in-view {
opacity: 1; transform: translateY(0);
}
</style>
면접 포인트
- "Action과 onMount의 차이는?": onMount는 컴포넌트 단위의 라이프사이클이고, Action은 개별 요소 단위입니다. 같은 Action을 여러 요소에 재사용할 수 있어, DOM 관련 로직의 재사용성이 높습니다.
- "React에서 Action과 비슷한 패턴은?": React에는 직접적인 대응이 없습니다. ref 콜백이나 커스텀 Hook + useEffect 조합으로 비슷하게 구현하지만, Svelte Action이 더 선언적이고 간결합니다.
정리
Action은 "이 요소가 DOM에 존재하는 동안 이 로직을 실행하라"는 선언입니다. 툴팁, 클릭 외부 감지, 무한 스크롤 같은 DOM 관련 기능을 한 번 만들어 두면 use: 한 줄로 어디서든 재사용할 수 있습니다. Svelte의 숨은 보석 같은 기능입니다.
댓글 로딩 중...