Svelte 5 - @attach

svelte5의 attachment 사용법

use:는 특정한 HTML 요소가 마운트 될 때 지정된 함수(액션이라고 부릅니다.)를 호출하여 해당 요소에 어떤 기능을 부여하거나 외부 라이브러리를 적용할 수 있는 기능입니다. 기존에 사용하던 라이브러리를 변경하지 않고 그대로 사용할 수 있도록 하는 매력적인 기능입니다.

다만, 라이브러리로 매개변수 값을 넘기는 문법이 모호하고, 액션이 리턴하는 값이 업데이트 함수와 정리함수를 메서드로 갖는 객체라는 점에서 몇 가지 불편한 부분들이 있습니다.

Svelte 5에서는 기존의 use: 지시어 사용의 불편한 점을 해소하고 더욱 유연하고 강력한 기능을 제공할 수 있도록 attachment라는 기능을 소개하고 있습니다. 이는 {@attach } 블럭을 통해 사용할 수 있습니다. {@attach}는 기존 use: 지시어와 유사한 기능을 제공하지만 몇 가지 측면에서 완전히 새로운 차이가 있습니다.

먼저 {@attach } 블럭은 이펙트($effect) 내에서 실행됩니다. 어태치먼트 함수는 이펙트와 유사하게 정리함수(teardown 함수)를 리턴합니다.

  1. {@attach } 블럭을 적용한 요소는 DOM에 마운트될 때 어태치먼트 함수를 호출합니다.
  2. 해당 요소가 마운트 해제될 때 어태치먼트 함수가 리턴한 정리함수가 호출됩니다.
  3. attach 블럭 내의 반응형 상태가 변경되면 정리함수가 호출된 후 어태치먼트 함수가 자동으로 재호출됩니다.

이러한 과정을 통해 update() 함수를 리턴해야 하거나, 액션 내에서 별도의 이펙트를 제어해야 하는 번거로움을 제거합니다.

다만, 어태치먼트 함수는 기본적으로 DOM 노드만 인자로 받을 수 있기 때문에 부가적인 파라미터를 정의하기 위해서는 팩토리 패턴을 사용할 필요가 있습니다.

<script>
import tippy from 'tippy.js';

function tooltip(node, message) {
  const tp = tippy(node, {content: message});

  return {
    destroy() {
      tp.destroy();
    }
  };
}

let message = 'hello world!';
</script>

<button use:tooltip={message}>hover me</button>

use:를 사용하여 tippy.js 라이브러리를 사용하는 예시

위 코드는 use: 지시어를 사용해서 버튼 요소에 툴팁 메시지를 띄우는 기능을 처리하는 예입니다. 그리고 ㅇ아래는 동일한 기능을 {@attach} 블럭으로 대체한 예입니다.


<script>
import tippy from 'tippy.js'
function tooltip(content) {
  return (node) => {
    const tp = tippy(node, {content});
    return tp.destroy;
  };
}

let greet = $state('hello');
</script>

<input bind:value={greet} />
<button {@attach tooltip(greet)}>hover me!</button>

같은 기능을 @attach로 대체한 형태

tippy와 같은 외부 라이브러리를 사용할 때 use:를 사용하던 것은 거의 그대로 대체가 됩니다. 다만 어태치먼트 함수는 인자를 하나 밖에 받지 못하기 때문에 위와 같은 팩토리 패턴으로 함수를 작성해야 합니다.

기존에 사용하던 액션을 어태치먼트로 대체하기 위해서는 약간의 수정이 필요한데, 아래와 같은 유틸리티 함수를 사용한다면 기존 액션 함수의 전환 없이 사용이 가능합니다. 뿐만 아니라 별도의 업데이트 로직이 없어도 message 값이 변경되면 툴팁이 강제로 제거된 후 다시 생성되어 자동으로 message 값에 반응하게 됩니다.

export function toAttach(action, ...args) {
	return (node) => {
		let rest = action(node, ...args);
		return rest.destroy || (() => {})
	}
}
function tooltip(node, message) {
  const tp = tippy(node, {content: message});

  return {
    destroy() {
      tp.destroy();
    }
  };
}

..

<button {@attach toAttach(tooltip, message)}>hover me</button>

액션을 어태치먼트로 변경하는 유틸리티 함수는 svelte/attachments 라이브러리의 fromAction을 사용해도 됩니다.

<script>
import { fromAction } from 'svelte/attachments';
..
</script>

<button {@attach fromAction(tooltip, () => message)}>hover me</button>

컴포넌트에 적용하기

use:action과 attachment의 또 다른 가장 큰 차이점은 어태치먼트는 컴포넌트에도 적용할 수 있다는 점입니다. 대신, 자식 컴포넌트 내에서 이 액션을 적용받을 노드가 지정되어야 합니다. 따라서 자식 컴포넌트는 $props()룬을 사용하여 전달 받은 어태치먼트를 해당 요소에 꽂아줄 필요가 있습니다.

이펙트 및 다른 라이프사이클 메소드들의 호출 순서는 다음과 같습니다. 부모 요소에서 지정한 어태치먼트는 자식 컴포넌트의 onMount 보다 앞서 호출됩니다.

  1. $effect.root : 라이프사이클과 무관하게 컴포넌트 로드 시 호출
  2. $effect.pre : DOM 업데이트 이전에 호출
  3. attachment : attachment는 onMount에 앞서서 호출됩니다.
  4. onMount
  5. $effect : onMount와 effect는 컴포넌트 코드 내 순서에 따라서 이펙트가 먼저 호출될 수도 있습니다.

외부 라이브러리 사용해보기

대부분의 외부 라이브러리는 특정한 DOM 노드를 선택하여 해당 노드 내부에 특정한 변경을 가하는 것들이 많습니다. highlight.js 같은 코드 하이라이트 라이브러니나, AG Grid같은 데이터 그리드 라이브러리가 이런 식으로 작동합니다.

<svelte:head> 태그를 사용하면 문서의 HEAD 영역에 스크립트나 스타일시트 파일을 연결할 수 있습니다. 이 태그를 사용하여 CDN을 통해서 서비스되는 스크립트 파일을 연결하고, 라이브러리를 적용할 요소에 use:,{@attach}를 사용하면 됩니다. 아래 예시는 tippy.js를 CDN을 통해 사용하는 방법입니다.

<svelte:head>
	<script src="https://unpkg.com/@popperjs/core@2"></script>
	<script src="https://unpkg.com/tippy.js@6" onload={() => (tp_ready = true)}></script>
</svelte:head>
<script>
	let tp_ready = $state(false);
	let name = $state('hello');
	function tooltip(content) {
		return (node) => {
			const tp = tippy(node, {content});
			return tp.destroy;
		};
	}
</script>
<input bind:value={name} />

<button {@attach tp_ready ? tooltip(name) : null}>hover me</button>