16. Transitions

2020. 12. 14. 15:18재주껏 하는 Front-End/Svelte (준비중)

반응형

이번 글에서는 Svelte의 트랜지션에 대해 알아보자. 트랜지션이란, 어떠한 이벤트가 발생하였을 때 화면에 출력하는 여러 가지 특수 효과를 말한다.

 

트랜지션과 애니메이션은 화면에 특수한 효과를 출력한다는 점은 같지만 트랜지션은 효과를 출력하기 위해 사전에 이벤트나 작업이 필요하며 애니메이션은 사전 작업 없이 자동으로 효과를 출력한다는 점이 다르다. 트랜지션은 화면에 출력하는 특수 효과이기 때문에 사용 방법이 애니메이션과 거의 동일하며 사용자 정의 트랜지션 효과도 설정할 수 있다.

 

Transitions 기본 사용 방법

 

Svelte에서 트랜지션을 사용하는 방법은 아래와 같다.

 

import { 트랜지션 효과 } from 'svelte/transition';

<탬플릿 요소 transition:트랜지션 효과>... </탬플릿 요소>

 

트랜지션 효과는 아래와 같이 효과를 사용자가 정의할 수 있도록 포맷을 제공한다.

 

transition = (node: HTMLElement, params: any) => {
  delay : 지연 시간,
  duration : 효과 지속 시간,
  easing : easing 함수, // https://svelte.dev/docs#svelte_easing 참고
  css : CSS 문자열을 리턴하는 함수,
  tick : 반복 호출 시 실행되는 함수
}

 

사용 방법이나 포맷이 이전 글에서 살펴본 애니메이션과 동일한 것을 알 수 있다. 그러나 트랜지션에는 애니메이션에 없는 이벤트가 존재한다. 아래는 Svelte 트랜지션에서 제공하는 이벤트이다.

 

1. introstart : 요소가 추가될 때 트랜지션이 시작되면 호출한다.
2. introend : 요소가 추가될 때 트랜지션이 종료되면 호출한다.
3. outrostart : 요소가 제거될 때 트랜지션이 시작되면 호출한다.
4. outroend : 요소가 제거될 때 트랜지션이 종료되면 호출한다.

// 사용 예시

<p
  transition:fly="{{ y: 200, duration: 2000 }}"
  on:introstart="{() => status = 'intro started'}"
  on:outrostart="{() => status = 'outro started'}"
  on:introend="{() => status = 'intro ended'}"
  on:outroend="{() => status = 'outro ended'}"
> Flies in and out </p>

 

위에서 언급했지만 애니메이션의 경우 사전 이벤트나 작업 없이 자동으로 실행하는 효과이기 때문에 상태와 상관없이 동작하므로 상태에 관련된 이벤트를 제공하지 않는다. 하지만 트랜지션은 사전에 이벤트나 작업으로 효과가 시작되기 때문에 위와 같이 상태 이벤트를 제공한다.

 

아래의 예제 코드를 실행해보자. 트랜지션이 동작하는 상태에 따라 각 이벤트의 콜백 함수들이 실행되어 status 변수에 문자열을 넣어준다.

 

<script>
	import { fly } from 'svelte/transition';
	let visible = true;
	let status = ''
</script>

<label>
	<input type="checkbox" bind:checked={visible}>
	visible
</label>

{#if visible}
	<p
		transition:fly="{{ y: 200, duration: 2000 }}"
		on:introstart="{() => status = 'intro started'}"
		on:outrostart="{() => status = 'outro started'}"
		on:introend="{() => status = 'intro ended'}"
		on:outroend="{() => status = 'outro ended'}"
	>
		트랜지션 대상 탬플릿 요소
	</p>
{/if}

<p>트랜지션 이벤트 상태 : { status }</p>

 

실행 결과는 아래와 같다.

 

Hide / Show 효과

 

Svelte에서 제공하는 내부 트랜지션은 fade, blur, fly, slide, scale, draw, crossfade가 있으며 각 트랜지션의 정보는 아래의 링크에서 확인할 수 있다.

 

svelte.dev/docs#svelte_transition

 

API Docs • Svelte

Before we begin This page contains detailed API reference documentation. It's intended to be a resource for people who already have some familiarity with Svelte. If that's not you (yet), you may prefer to visit the interactive tutorial or the examples befo

svelte.dev

 

IN / OUT 디렉티브

 

트랜지션의 시작과 끝을 다른 효과로 지정하고 싶을 때 사용한다. 위에서 살펴본 예제 코드를 활용하여 IN / OUT 디렉티브를 사용해보자. IN / OUT 디렉티브 사용 시 transition 디렉티브는 생략한다.

 

<script>
	import { fly, fade } from 'svelte/transition';
	let visible = true;
</script>

<label>
	<input type="checkbox" bind:checked={visible}>
	visible
</label>

{#if visible}
	<p in:fly="{{y: 200, duration: 2000}}" out:fade>
		트랜지션 대상 탬플릿 요소
	</p>
{/if}

 

실행 결과는 아래와 같다.

 

 

사용자 정의 CSS 트랜지션

 

Svelte의 트랜지션은 transition 디렉티브에 효과를 정의하는 함수를 설정하는 것이기 때문에 사용자가 임의로 효과를 만들 수 있다.

 

아래의 코드는 Visible 체크 박스를 체크 시 트랜지션이 적용된 <div> 요소가 사용자 정의 트랜지션 효과를 내며 화면에 출력하는 예제 코드이다. easing 함수 중 elasticOut을 사용하여 사용자 정의 트랜지션 효과인 Spin을 적용하였다.

 

<script>
	import { fade } from 'svelte/transition';
	import { elasticOut } from 'svelte/easing';

	let visible = true;

	function spin(node, { duration }) {
		return {
			duration,
			css: t => {
				const eased = elasticOut(t);

				return `
					transform: scale(${eased}) rotate(${eased * 1080}deg);
					color: hsl(
						${~~(t * 360)},
						${Math.min(100, 1000 - 1000 * t)}%,
						${Math.min(50, 500 - 500 * t)}%
					);`
			}
		};
	}
</script>

<style>
	.centered {
		position: absolute;
		left: 40%;
		top: 50%;
	}

	span {
		font-size: 2em;
	}
</style>

<label>
	<input type="checkbox" bind:checked={visible}>
	visible
</label>

{#if visible}
	<div class="centered" in:spin="{{duration: 8000}}" out:fade>
		<span>사용자 정의 트랜지션 ...</span>
	</div>
{/if}

 

실행한 결과는 아래와 같다.

 

 

위의 예제 코드에서 사용한 사용자 정의 효과인 Spin 함수를 살펴보자. Spin 함수에서 눈여겨봐야 할 부분은 CSS 부분으로 효과가 재생되는 시간 값을 가지고 있는 매개변수 t를 사용하여 트랜지션 효과가 끝날 때까지 회전과 글자의 색상을 지속적으로 변경하는 것을 알 수 있다.

 

function spin(node, { duration }) {
	return {
		duration,
		css: t => {
			const eased = elasticOut(t);
			return `
				transform: scale(${eased}) rotate(${eased * 1080}deg);
				color: hsl(
					${~~(t * 360)},
					${Math.min(100, 1000 - 1000 * t)}%,
					${Math.min(50, 500 - 500 * t)}%
				)`
	}
}

 

위와 같이 트랜지션 함수를 수정하여 사용자 정의 효과를 만들 수 있다.

 

사용자 정의 자바스크립트 트랜지션

 

대부분의 효과나 스타일은 CSS만으로도 충분하지만 특정 효과들은 자바스크립트가 반드시 필요한 경우가 있다. 아래의 예제는 요소 내부의 텍스트를 하나씩 순서대로 출력하는 트랜지션이다.

 

<script>
	let visible = false;

	function typewriter(node, { speed = 50 }) {
		const text = node.textContent
		const duration = text.length * speed

		return {
			duration,
			tick: t => {
				const i = text.length * t
				node.textContent = text.slice(0, i)
			}
		};
	}
</script>

<label>
	<input type="checkbox" bind:checked={visible}>
	visible
</label>

{#if visible}
	<p in:typewriter>
		이 트랜지션은 CSS만으로 만들 수 없습니다.
	</p>
{/if}

 

실행한 결과는 아래와 같다.

 

 

위와 같이 트랜지션 디렉티브에 전달하는 함수 또는 객체를 상황에 맞게 값을 변경하여 자바스크립트로 트랜지션 효과를 만들 수도 있다.

 

local 키워드

 

일반적으로 트랜지션은 블록 컨테이너가 추가되거나 제거될 때 효과가 재생된다. 이때, 트랜지션이 적용된 요소가 속해 있는 모든 요소에 효과가 적용된다. 하지만 상황에 따라서는 트랜지션이 적용된 요소에만 효과를 적용해야 하는 경우가 생길 수도 있다.

 

아래의 예제 코드는 배열의 아이템을 잘라 새로운 배열을 리턴하여 화면에 출력하는 예제로 <div> 요소에 트랜지션이 적용되어 있다.

 

<script>
	import { slide } from 'svelte/transition';

	let showItems = true;
	let i = 5;
	let items = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
</script>

<style>
	div {
		padding: 0.5em 0;
		border-top: 1px solid #eee;
	}
</style>

<label>
	<input type="checkbox" bind:checked={showItems}>
	show list
</label>

<label>
	<input type="range" bind:value={i} max=10>

</label>

{#if showItems}
	{#each items.slice(0, i) as item}
		<div transition:slide>
			{item}
		</div>
	{/each}
{/if}

 

실행 결과는 아래와 같다.

 

 

위의 예제에서 눈여겨봐야 할 것은 상단의 show list 체크 박스이다. 체크 박스를 체크하거나 해제하면 트랜지션이 적용된 <div> 요소를 포함하고 있는 모든 요소에 효과가 나타나는 것을 볼 수 있다.

 

이번에는 트랜지션이 적용된 요소의 값이 변할 때만 효과가 나타나도록 변경해보자. 템플릿 코드를 아래와 같이 수정하자. 트랜지션 효과에 or 연산자로 local 키워드가 추가되었다.

 

...

{#if showItems}
	{#each items.slice(0, i) as item}
		<div transition:slide|local>
			{item}
		</div>
	{/each}
{/if}

 

다시 한번 실행해보자.

 

 

show list 체크 박스를 체크하거나 해제했을 때는 트랜지션이 적용되지 않았지만, 슬라이더를 이용하여 <div> 요소의 추가 / 삭제를 진행할 때는 트랜지션이 적용되는 것을 확인할 수 있다.

 

CrossFade 트랜지션

 

Svelte에 내장된 다양한 트랜지션 중에서 CrossFade는 서로 다른 컨테이너 요소의 값을 교환하는 효과를 낼 때 사용한다. 다른 트랜지션들과 달리 블록 컨테이너가 2개 이상 필요하기 때문에 사용 방법이 다른 효과와는 다르다.

 

아래의 예제 코드는 이전 글에서 사용한 Todolist 예제로 애니메이션을 제거하고 CrossFade 효과를 적용한 코드이다.

 

<script>	
	import { quintOut } from 'svelte/easing';
	import { crossfade } from 'svelte/transition';
	
	const [send, receive] = crossfade({
		duration: d => Math.sqrt(d * 200),

		fallback(node, params) {
			const style = getComputedStyle(node);
			const transform = style.transform === 'none' ? '' : style.transform;

			return {
				duration: 600,
				easing: quintOut,
				css: t => `
					transform: ${transform} scale(${t});
					opacity: ${t}
				`
			};
		}
	});
	
	let uid = 1;

	let todos = [
		{ id: uid++, done: false, description: 'Svelte 학습하기' },
		{ id: uid++, done: false, description: '세차 하기' },
		{ id: uid++, done: true,  description: '블로그에 글 올리기' },
		{ id: uid++, done: false, description: '호수 한바퀴 돌고 오기' }
	];

	function add(input) {
		const todo = {
			id: uid++,
			done: false,
			description: input.value
		};

		todos = [todo, ...todos];
		input.value = '';
	}

	function remove(todo) {
		todos = todos.filter(t => t !== todo);
	}

	function mark(todo, done) {
		todo.done = done;
		remove(todo);
		todos = todos.concat(todo);
	}
</script>

<div class='board'>
	<input
		placeholder="계획한 일을 추가하세요."
		on:keydown={e => e.key === 'Enter' && add(e.target)}
	>

	<div class='left'>
		<h2>할 일</h2>
		{#each todos.filter(t => !t.done) as todo (todo.id)}
			<label
						in:receive="{{key: todo.id}}"
						out:send="{{key: todo.id}}"
			>
				<input type=checkbox on:change={() => mark(todo, true)}>
				{todo.description}
				<button on:click="{() => remove(todo)}">remove</button>
			</label>
		{/each}
	</div>

	<div class='right'>
		<h2>완료</h2>
		{#each todos.filter(t => t.done) as todo (todo.id)}
			<label
						 class="done"
						 in:receive="{{key: todo.id}}"
			       out:send="{{key: todo.id}}"
			>
				<input type=checkbox checked on:change={() => mark(todo, false)}>
				{todo.description}
				<button on:click="{() => remove(todo)}">remove</button>
			</label>
		{/each}
	</div>
</div>

<style>
	.board {
		display: grid;
		grid-template-columns: 1fr 1fr;
		grid-gap: 1em;
		max-width: 36em;
		margin: 0 auto;
	}

	.board > input {
		font-size: 1.4em;
		grid-column: 1/3;
	}

	h2 {
		font-size: 2em;
		font-weight: 200;
		user-select: none;
		margin: 0 0 0.5em 0;
	}

	label {
		position: relative;
		line-height: 1.2;
		padding: 0.5em 2.5em 0.5em 2em;
		margin: 0 0 0.5em 0;
		border-radius: 2px;
		user-select: none;
		border: 1px solid hsl(240, 8%, 70%);
		background-color:hsl(240, 8%, 93%);
		color: #333;
	}

	input[type="checkbox"] {
		position: absolute;
		left: 0.5em;
		top: 0.6em;
		margin: 0;
	}

	.done {
		border: 1px solid hsl(240, 8%, 90%);
		background-color:hsl(240, 8%, 98%);
	}

	button {
		position: absolute;
		top: 0;
		right: 0.2em;
		width: 2em;
		height: 100%;
		background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
		background-size: 1.4em 1.4em;
		border: none;
		opacity: 0;
		transition: opacity 0.2s;
		text-indent: -9999px;
		cursor: pointer;
	}

	label:hover button {
		opacity: 1;
	}
</style>

 

위의 코드를 분석해보자. 먼저 Svelte의 내장 트랜지션인 CrossFade 함수를 사용하기 위해 svelte/transition 모듈을 Import 한다. 다음 CrossFade 함수를 호출하는데 파라미터로 트랜지션 옵션을 정의한 객체를 전달한다. 전달한 트랜지션 객체에는 duration과 fallback 메서드를 정의한다.

 

CrossFade 함수를 호출하면 send, receive 트랜지션 객체가 리턴되며, 트랜지션이 적용될 요소인 <label>에 in, out 지시자를 이용하여 효과를 적용한다. 중요한 것은 "할 일" 쪽에 있는 <label>과 "완료" 쪽에 있는 <label> 모두 in, out 지시자로 트랜지션을 적용해야 한다는 것이다. (그래야만 Send > Receive 순으로 효과가 재생된다.)

 

위의 예제를 실행한 결과는 아래와 같다.

 

 

위와 같이 Svelte의 CrossFade 트랜지션으로 아이템이 이동하는 효과를 쉽게 적용할 수 있다.

 


 

지금까지 Svelte의 트랜지션에 대해 알아보았다. 다음 글에서는 마지막 문법인 Actions에 대해서 알아보도록 하겠다.

 

오늘은 여기까지~

반응형

'재주껏 하는 Front-End > Svelte (준비중)' 카테고리의 다른 글

15. Animations  (0) 2020.12.08
14. Motion  (0) 2020.12.01
13. Module Context, Debug  (0) 2020.11.27
12. Context API  (2) 2020.11.24
11. Special Elements  (0) 2020.11.12