React의 커스텀 Hook이 로직을 재사용한다면, Svelte의 Action은 DOM 조작 로직을 재사용합니다.

개념 정의

Action 은 요소가 DOM에 마운트될 때 실행되는 함수입니다. use: 디렉티브로 적용하며, 재사용 가능한 DOM 관련 로직을 캡슐화합니다. 툴팁, 클릭 외부 감지, 포커스 트랩 같은 기능에 적합합니다.

기본 구조

JAVASCRIPT
// action 함수 시그니처
function myAction(node, parameter) {
  // node가 DOM에 마운트될 때 실행

  return {
    update(newParameter) {
      // parameter가 변경될 때 실행
    },
    destroy() {
      // node가 DOM에서 제거될 때 실행 (정리)
    }
  };
}
SVELTE
<div use:myAction={parameter}>
  Action이 적용된 요소
</div>

실전 예시 — 클릭 외부 감지

SVELTE
<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

SVELTE
<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

SVELTE
<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

SVELTE
<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

SVELTE
<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의 숨은 보석 같은 기능입니다.

댓글 로딩 중...