11. Special Elements

2020. 11. 12. 17:19재주껏 하는 Front-End/Svelte (준비중)

반응형

이번에는 Svelte에서 제공하는 내장 요소들에 대해 알아보자.

 

<svelte:self>

 

특정 컴포넌트 내부에서 컴포넌트 자신을 재귀로 추가해야 하는 경우에 사용한다. 재귀로 컴포넌트 자신을 추가해야 하는 경우 일반적으로 <script> 태그 내에서 컴포넌트를 import 하여 사용하지만, Svelte에서는 <svelte:self> 요소를 사용하여 쉽게 구현할 수 있다.

 

아래의 예제는 폴더와 파일 구조를 트리로 출력하는 컴포넌트의 예제이다. 폴더 내부에 파일이 여러 개 들어가기 때문에 재귀 형태로 나타내야만 한다.

 

1) App.svelte

<script>
	import Folder from './Folder.svelte';

	let root = [
		{
			type: 'folder',
			name: 'Important work stuff',
			files: [
				{ type: 'file', name: 'quarterly-results.xlsx' }
			]
		},
		{
			type: 'folder',
			name: 'Animal GIFs',
			files: [
				{
					type: 'folder',
					name: 'Dogs',
					files: [
						{ type: 'file', name: 'treadmill.gif' },
						{ type: 'file', name: 'rope-jumping.gif' }
					]
				},
				{
					type: 'folder',
					name: 'Goats',
					files: [
						{ type: 'file', name: 'parkour.gif' },
						{ type: 'file', name: 'rampage.gif' }
					]
				},
				{ type: 'file', name: 'cat-roomba.gif' },
				{ type: 'file', name: 'duck-shuffle.gif' },
				{ type: 'file', name: 'monkey-on-a-pig.gif' }
			]
		},
		{ type: 'file', name: 'TODO.md' }
	];
</script>

<Folder name="Home" files={root} expanded/>

 

2) File.svelte

<script>
	export let name;
	$: type = name.slice(name.lastIndexOf('.') + 1);
</script>

<style>
	span {
		padding: 0 0 0 1.5em;
		background: 0 0.1em no-repeat;
		background-size: 1em 1em;
	}
</style>

<span style="background-image: url(tutorial/icons/{type}.svg)">{name}</span>

 

3) Folder.svelte

<script>
	import File from './File.svelte';

	export let expanded = false;
	export let name;
	export let files;

	function toggle() {
		expanded = !expanded;
	}
</script>

<style>
	span {
		padding: 0 0 0 1.5em;
		background: url(tutorial/icons/folder.svg) 0 0.1em no-repeat;
		background-size: 1em 1em;
		font-weight: bold;
		cursor: pointer;
	}

	.expanded {
		background-image: url(tutorial/icons/folder-open.svg);
	}

	ul {
		padding: 0.2em 0 0 0.5em;
		margin: 0 0 0 0.5em;
		list-style: none;
		border-left: 1px solid #eee;
	}

	li {
		padding: 0.2em 0;
	}
</style>

<span class:expanded on:click={toggle}>{name}</span>

{#if expanded}
	<ul>
		{#each files as file}
			<li>
				{#if file.type === 'folder'}
					<svelte:self {...file}/>
				{:else}
					<File {...file}/>
				{/if}
			</li>
		{/each}
	</ul>
{/if}

 

Folder 컴포넌트를 보면 전달된 타입이 'folder'일 경우 <svelte:self>를 사용해서 Folder 컴포넌트 자신을 재귀로 추가하는 것을 확인할 수 있다. 아래는 예제를 실행한 스크린 샷이다.

 

 

<svelte:component>

 

조건에 따라 DOM에 마운트 되어야 하는 컴포넌트를 달리 해야 하는 경우에 사용한다. 즉, 동적 컴포넌트를 만들 때 사용하는 것으로 Props로 this를 제공하며, this에는 마운트 할 컴포넌트 객체를 전달한다. 아래의 예제는 Select Box에서 선택한 컴포넌트가 동적으로 DOM에 마운트 되는 예제이다.

 

1) App.svelte

<script>
	import RedThing from './RedThing.svelte';
	import GreenThing from './GreenThing.svelte';
	import BlueThing from './BlueThing.svelte';

	const options = [
		{ color: 'red',   component: RedThing   },
		{ color: 'green', component: GreenThing },
		{ color: 'blue',  component: BlueThing  },
	];

	let selected = options[0];
</script>

<select bind:value={selected}>
	{#each options as option}
		<option value={option}>{option.color}</option>
	{/each}
</select>

<svelte:component this={selected.component}/>

 

2) RedThing

<style>
	strong { color: red; }
</style>

<strong>red thing</strong>

 

3) GreenThing

<style>
	strong { color: green; }
</style>

<strong>green thing</strong>

 

4) BlueThing

<style>
	strong { color: blue; }
</style>

<strong>blue thing</strong>

 

<svelte:component>를 사용하지 않는다면 if / else 블록으로 모든 컴포넌트를 조건에 따른 분기 처리를 해야만 한다. 이 요소는 개발 시 매우 자주 사용될 것이기 때문에 알아두는 것이 좋다. 아래는 예제 코드를 실행한 결과다.

 

 

<svelte:window>

 

브라우저 최상위 객체인 window를 가리키는 요소로 전역 이벤트를 설정할 때 편리하게 사용할 수 있다. 예를 들어, 브라우저의 리사이즈 이벤트를 받아야 된다고 가정해보자. Svelte가 아닌 프레임 워크 또는 JS 환경에서는 아래와 같은 코드로 전역 이벤트를 설정한다.

 

window.addEventListener('resize', function () { ... })

 

Svelte에서는 아래의 코드로 전역 이벤트를 설정한다.

 

<svelte:window on:resize={ () => {...} } />

 

두 코드가 별 차이 없는 것 같지만 Svelte가 아닌 경우, 페이지가 닫히거나 이동될 때 메모리 누수 방지를 위해 아래의 코드로 이벤트 리스너를 제거해야만 한다.

 

window.removeEventListener('resize', function () { ... })

 

하지만 <svelte:window>를 사용하면 메모리 누수를 신경 쓸 필요가 없어 편리하다.

 

<svelte:window> Bindings

 

<svelte:window>는 전역 이벤트 연결 외에도 아래의 전역 멤버 값을 쉽게 바인딩할 수 있다.

 

요소 설명
innerWidth 브라우저 Content 너비
innerHeight 브라우저 Content 높이
outerWidth 브라우저 Full Size 너비
outerHeight 브라우저 Full Size 높이
scrollX 브라우저 Content 가로 스크롤 픽셀값
scrollY 브라우저 Content 세로 스크롤 픽셀값
online 브라우저의 네트워크 연결 상태를 Boolean으로 리턴

 

scrollX와 scrollY를 제외한 모든 값은 읽기 전용이기 때문에 값을 변경할 수 없다. 아래의 예제는 스크롤을 내리면 Y축 스크롤이 얼마나 되었는지를 출력하는 예제이다.

 

<script>
	const layers = [0, 1, 2, 3, 4, 5, 6, 7, 8];

	let y;
</script>

<svelte:window bind:scrollY={y}/>

<a class="parallax-container" href="https://www.firewatchgame.com">
	{#each layers as layer}
		<img
			style="transform: translate(0,{-y * layer / (layers.length - 1)}px)"
			src="https://www.firewatchgame.com/images/parallax/parallax{layer}.png"
			alt="parallax layer {layer}"
		>
	{/each}
</a>

<div class="text">
	<span style="opacity: {1 - Math.max(0, y / 40)}">
		scroll down
	</span>

	<div class="foreground">
		You have scrolled {y} pixels
	</div>
</div>

<style>
	.parallax-container {
		position: fixed;
		width: 2400px;
		height: 712px;
		left: 50%;
		transform: translate(-50%,0);
	}

	.parallax-container img {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		will-change: transform;
	}

	.parallax-container img:last-child::after {
		content: '';
		position: absolute;
		width: 100%;
		height: 100%;
		background: rgb(45,10,13);
	}

	.text {
		position: relative;
		width: 100%;
		height: 300vh;
		color: rgb(220,113,43);
		text-align: center;
		padding: 4em 0.5em 0.5em 0.5em;
		box-sizing: border-box;
		pointer-events: none;
	}

	span {
		display: block;
		font-size: 1em;
		text-transform: uppercase;
		will-change: transform, opacity;
	}

	.foreground {
		position: absolute;
		top: 711px;
		left: 0;
		width: 100%;
		height: calc(100% - 712px);
		background-color: rgb(32,0,1);
		color: white;
		padding: 50vh 0 0 0;
	}

	:global(body) {
		margin: 0;
		padding: 0;
		background-color: rgb(253, 174, 51);
	}
</style>

 

실행 결과는 아래와 같다.

 

 

<svelte:body>

 

<svelte:window>와 마찬가지로 <body>를 가리키는 특수한 요소다. <svelte:window>와 사용 방법이 크게 다르지 않으며 window에서 사용할 수 없는 이벤트인 mouseentermouseleave 이벤트를 연결할 수 있다.

 

<svelte:body
	on:mouseenter={handleMouseenter}
	on:mouseleave={handleMouseleave}
/>

 

<svelte:head>

 

HTML 문서의 <head> 부분을 동적으로 컨트롤할 수 있는 특수 요소로 <svelte:head> 사이에 <head> 태그에 들어갈 수 있는 태그와 설정들을 넣어주면 된다. 기본적인 사용 방법은 아래와 같다.

 

<svelte:head>
  <title> Svelte Special Elements </title>
  <link rel="stylesheet" herf="...">
  ...
</svelte:head>

 

아래는 페이지의 배경을 어둡게 하는 테마를 적용하는 간단한 예제 코드이다.

 

<svelte:head>
	<link rel="stylesheet" href="tutorial/dark-theme.css">
</svelte:head>

<h1>Hello world!</h1>

 

실행 결과는 아래와 같다.

 

 

<svelte:options>

 

Svelte 컴파일러의 옵션을 정의한다. 아래는 <svelte:options>에서 제공하는 옵션들이다.

 

옵션 설명
immutable={true} 변하지 않는 데이터를 사용할 것이라고 컴파일러에게 알려 간단한 참조 검사를 진행하는 옵션
immutable={false} 기본값으로 엄격하게 값이 변경 되었는지를 체크하는 옵션
accessors={true} 컴포넌트의 Props에 Getter와 Setter를 추가
accessors={false} 기본값으로 Props는 온전히 전달의 역할만 수행함
namespace: "..." 컴포넌트의 네임 스페이스를 지정하는 것으로 <svg>에서 사용함
tag="..." 사용자 지정 컴포넌트로 컴파일 할 때 컴포넌트의 이름

 

이 중에서 immutable 속성의 예제를 살펴보자. 아래의 예제는 Todo 리스트에서 항목을 클릭하면 주변이 반짝 거리는 플래시 효과가 나타나는 예제이다.

 

1) App.svelte

<script>
	import Todo from './Todo.svelte';

	let todos = [
		{ id: 1, done: true, text: 'wash the car' },
		{ id: 2, done: false, text: 'take the dog for a walk' },
		{ id: 3, done: false, text: 'mow the lawn' }
	];

	function toggle(toggled) {
		todos = todos.map(todo => {
			if (todo === toggled) {
				// return a new object
				return {
					id: todo.id,
					text: todo.text,
					done: !todo.done
				};
			}

			// return the same object
			return todo;
		});
	}
</script>

<h2>Todos</h2>
{#each todos as todo}
	<Todo {todo} on:click={() => toggle(todo)}/>
{/each}

 

2) Todo.svelte

<svelte:options immutable={true}/>

<script>
	import { afterUpdate } from 'svelte';
	import flash from './flash.js';

	export let todo;

	let div;

	afterUpdate(() => {
		flash(div);
	});
</script>

<style>
	div {
		cursor: pointer;
		line-height: 1.5;
	}
</style>

<!-- the text will flash red whenever
     the `todo` object changes -->
<div bind:this={div} on:click>
	{todo.done ? '👍': ''} {todo.text}
</div>

 

3) Flash.js

export default function flash(element) {
	requestAnimationFrame(() => {
		element.style.transition = 'none';
		element.style.color = 'rgba(255,62,0,1)';
		element.style.backgroundColor = 'rgba(255,62,0,0.2)';

		setTimeout(() => {
			element.style.transition = 'color 1s, background 1s';
			element.style.color = '';
			element.style.backgroundColor = '';
		});
	});
}

 

Todo 컴포넌트를 보면 상단에 immutable 속성이 적용되어 있음을 볼 수 있다. 이 옵션이 적용된 상태에서 항목을 클릭하면 아래와 같이 출력된다.

 

 

이번에는 Todo 컴포넌트 상단의 <svelte:options>를 제거하고 다시 한번 클릭해보자.

 

 

Svelte의 immutable 옵션을 true로 설정하면 상태가 변경된 항목만 검사하기 때문에 클릭한 Todo 항목만 효과가 적용된다. 반면에, immutable 옵션을 false로 한 경우 (기본값) 에는 하나의 항목이 변경되더라도 전체를 모두 검사하기 때문에 모든 Todo 항목에 효과가 적용된다.

 

컴파일러가 체크하는 항목이 줄어들기 때문에 성능상으로 이점이 있을 것 같은데 공식 API 문서에는 이러한 내용은 언급된 게 없어서 실제로 좋은지는 모르겠다.

 

오늘은 여기까지이며 다음에는 Context API에 대해 알아보도록 하겠다.

반응형

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

13. Module Context, Debug  (0) 2020.11.27
12. Context API  (2) 2020.11.24
10. Slot  (0) 2020.10.29
9. 스토어  (0) 2020.10.07
8. 컴포넌트 생명 주기  (0) 2020.09.22