7. 바인딩

2020. 9. 14. 16:38재주껏 하는 Front-End/Svelte (준비중)

반응형

이번 글에서는 바인딩에 대해서 알아보자. 바인딩이란 컴포넌트의 데이터 모델 변수와 HTML 요소 또는 컴포넌트와 연결하는 작업을 말한다. 전통적인 웹 방식에서는 오직 DOM 객체로 요소에 접근하여 value를 설정해야 했지만, 최근의 UI 프레임워크들은 이러한 과정들을 자동으로 수행하여 연결해준다.

 

양방향 바인딩

 

Svelte와 같은 반응형 UI 프레임워크들은 컴포넌트들에 데이터를 전달할 때 상위 컴포넌트가 하위 컴포넌트에게 전달하는 하양식 구조를 가지고 있다. ReactJS의 경우 양방향 바인딩을 공식적으로 지원하지 않아 단방향 두 개를 구성하여 비슷하게 흉내를 내는 식으로 구현한다.

 

ReactJS가 이런 엄격한 규칙을 정한 이유는 거대한 프로젝트에서 유지보수성이 용이하다는 큰 장점이 있기 때문이며, 이로 인해 ReactJS는 배우기 어려운 프레임워크임과 동시에 가장 안전하고 편한 프레임워크로 평가받고 있다.

 

반면, VueJS는 양방향 바인딩 기능을 제공한다. VueJS도 초반에는 단방향 바인딩만 제공했지만 2.x 버전을 기준으로 양방향 바인딩을 공식 지원하고 있다. 물론 너무 남발하지 말라는 경고문도 같이 있으니 궁금하면 VueJS 문서에서 확인해보자.

 

Svelte 역시 양방향 바인딩을 제공한다. 기본적인 사용 방법은 아래와 같다.

 

<요소 또는 컴포넌트 bind:속성 또는 하위 컴포넌트 Props={변수} />

// 바인딩할 변수의 이름이 요소의 속성 이름과 같으면 축약 가능

let value = 'Hello Svelte'

<input type="text" bind:value>

 

변수의 값이 변하면 자동으로 HTML 요소의 속성 또는 하위 컴포넌트의 Props에 값을 전달한다. 개발자가 DOM을 이용하여 value를 수정하지 않아도 자동으로 값이 업데이트되므로 개발 시 매우 편리하다. 사용 예제는 아래와 같다.

 

<script>
	let name = 'world';
</script>

<input value={name}>

<h1>Hello {name}!</h1>

 

실행 결과는 아래와 같다.

 

 

Number 타입 바인딩

 

DOM은 데이터를 다룰 때 모든 타입을 문자열로 처리한다. HTML 자체가 문서 포맷의 일종이기 때문인데 number 타입의 HTML 요소 또는 컴포넌트들이 값을 처리할 때는 문자열로 변환해야 하는 단점이 있다. 다행히도 Svelte는 요소의 타입이 Number인 경우 자동으로 형 변환을 시켜준다. 기본적인 사용 방법은 아래와 같다.

 

<input type="number 또는 range 등" bind:value={바인딩할 변수} min="..." max="...">

 

예제 코드를 살펴보자.

 

<script>
	let a = 1;
	let b = 2;
</script>

<label>
	<input type=number bind:value={a} min=0 max=10>
	<input type=range bind:value={a} min=0 max=10>
</label>

<label>
	<input type=number bind:value={b} min=0 max=10>
	<input type=range bind:value={b} min=0 max=10>
</label>

<p>{a} + {b} = {a + b}</p>

 

실행 결과는 아래와 같다. 사소한 것이라도 UI 프레임워크가 자동으로 처리해주는 것들이 개발을 얼마나 편리하게 만들어주는지 알 수 있다.

 

 

Checkbox 타입 바인딩

 

Checkbox 타입의 input 요소에 바인딩을 하기 위해서는 checked 속성에 값을 연결하면 된다. 사용 방법은 아래와 같다.

 

<input type="checkbox" bind:checked={바인딩할 변수}>

 

예제 코드를 살펴보자.

 

<script>
	let yes = false;
</script>

<label>
	<input type=checkbox bind:checked={yes}>
	Yes! Send me regular email spam
</label>

{#if yes}
	<p>Thank you. We will bombard your inbox and sell your personal details.</p>
{:else}
	<p>You must opt in to continue. If you're not paying, you're the product.</p>
{/if}

<button disabled={!yes}>
	Subscribe
</button>

 

실행 결과는 아래와 같다.

 

Group 타입 바인딩

 

Group 타입에는 Radio 버튼과 Checkbox 그룹 요소가 있다. 이 요소들은 group이라는 속성 값으로 같은 그룹에 속한 요소인지를 판별한다. 사용 방법은 아래와 같다.

 

1) 라디오 버튼 그룹인 경우

<input type=radio bind:group={바인딩할 변수} value={라디오 버튼 선택 시 입력되는 값}>

// 사용 예제

let isAllow = 'any'

<input type=radio bind:group={isAllow} value="any">
<input type=radio bind:group={isAllow} value="deny">
<input type=radio bind:group={isAllow} value="none">

console.log('Selected allow > ', isAllow) // 라디오 버튼이 선택한 값이 출력됨

 

2) 체크박스 버튼 그룹인 경우

<input type="checkbox" bind:group={바인딩할 배열 변수} value={체크 박스 버튼 선택 시 입력되는 값}>

// 사용 예제

let selectedList = []

<inpub type="checkbox" bind:group={selectedList} value="선택 1">
<inpub type="checkbox" bind:group={selectedList} value="선택 2">
<inpub type="checkbox" bind:group={selectedList} value="선택 3">

console.log('Selected list > ', selectedList) // 체크 박스 버튼이 선택한 값이 출력됨

 

예제 코드를 살펴보자.

 

<script>
	let scoops = 1;
	let flavours = ['Mint choc chip'];

	let menu = [
		'Cookies and cream',
		'Mint choc chip',
		'Raspberry ripple'
	];

	function join(flavours) {
		if (flavours.length === 1) return flavours[0];
		return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
	}
</script>

<h2>Size</h2>

<label>
	<input type=radio bind:group={scoops} value={1}>
	One scoop
</label>

<label>
	<input type=radio bind:group={scoops} value={2}>
	Two scoops
</label>

<label>
	<input type=radio bind:group={scoops} value={3}>
	Three scoops
</label>

<h2>Flavours</h2>

{#each menu as flavour}
	<label>
		<input type=checkbox bind:group={flavours} value={flavour}>
		{flavour}
	</label>
{/each}

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {join(flavours)}
	</p>
{/if}

 

실행 결과는 아래와 같다.

 

 

Textarea 요소 바인딩

 

텍스트 영역 타입은 아래와 같이 연결할 수 있다.

 

<textarea bind:value={바인딩할 변수}/>

 

예제 코드는 아래와 같다.

 

<script>
	let value = `Hello svelte world!! Svelte is all new framework!!`;
</script>

<style>
	textarea { width: 100%; height: 200px; }
</style>

<textarea bind:value></textarea>

<p>{value}</p>

 

실행 결과는 아래와 같다.

 

 

Select 요소 바인딩

 

Select 타입은 아래와 같이 연결할 수 있다.

 

<select bind:value={바인딩할 변수} on:change={값이 변경될 때 호출되는 이벤트 핸들러} />

 

예제 코드는 아래와 같다.

 

<script>
	let questions = [
		{ id: 1, text: `Where did you go to school?` },
		{ id: 2, text: `What is your mother's name?` },
		{ id: 3, text: `What is another personal fact that an attacker could easily find with Google?` }
	];

	let selected;

	let answer = '';

	function handleSubmit() {
		alert(`answered question ${selected.id} (${selected.text}) with "${answer}"`);
	}
</script>

<style>
	input { display: block; width: 500px; max-width: 100%; }
</style>

<h2>Insecurity questions</h2>

<form on:submit|preventDefault={handleSubmit}>
	<select bind:value={selected} on:change="{() => answer = ''}">
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>

	<input bind:value={answer}>

	<button disabled={!answer} type=submit>
		Submit
	</button>
</form>

<p>selected question {selected ? selected.id : '[waiting...]'}</p>

 

실행 결과는 아래와 같다.

 

 

다중 선택 요소 바인딩

 

Select 요소에 multiple 속성을 추가하고 Shift 키를 누른 채로 아이템을 클릭하여 여러 개를 동시에 선택할 수 있다. Checkbox 그룹과 마찬가지로 선택된 값은 배열에 추가된다. 예제 코드를 살펴보자.

 

<script>
	let scoops = 1;
	let flavours = ['Mint choc chip'];

	let menu = [
		'Cookies and cream',
		'Mint choc chip',
		'Raspberry ripple'
	];

	function join(flavours) {
		if (flavours.length === 1) return flavours[0];
		return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
	}
</script>

<h2>Size</h2>

<label>
	<input type=radio bind:group={scoops} value={1}>
	One scoop
</label>

<label>
	<input type=radio bind:group={scoops} value={2}>
	Two scoops
</label>

<label>
	<input type=radio bind:group={scoops} value={3}>
	Three scoops
</label>

<h2>Flavours</h2>

<select multiple bind:value={flavours}>
	{#each menu as flavour}
		<option value={flavour}>
			{flavour}
		</option>
	{/each}
</select>

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {join(flavours)}
	</p>
{/if}

 

선택된 값이 배열에 저장되는 것을 확인할 수 있다. 출력 결과는 아래와 같다.

 

 

contenteditable 속성 바인딩

 

contenteditable 속성은 HTML5에 추가된 것으로 특정 요소의 내용을 편집할 수 있게 해주는 기능이다. 주로 텍스트 에디터 구현 시 자주 사용하는데, Svelte에서는 아래와 같이 데이터 바인딩으로 아주 쉽게 구현할 수 있다. 기본적인 사용 방법은 아래와 같다.

 

<특정 요소 contenteditable="true / false / inherit" bind:innerHTML={바인딩할 변수} />

 

예제 코드는 아래와 같다. <div> 요소를 수정할 수 있도록 설정하고 innerHTML 속성에 변수를 바인딩한 후 마우스로 <div> 요소를 선택하면 Input 요소처럼 값을 넣을 수 있다.

 

<script>
	let html = '<p>Write some text!</p>';
</script>

<div contenteditable="true" bind:innerHTML={html}></div>

<pre>{html}</pre>

<style>
	[contenteditable] {
		padding: 0.5em;
		border: 1px solid #eee;
		border-radius: 4px;
	}
</style>

 

실행 결과는 아래와 같다.

 

 

반복문 바인딩

 

일반적인 요소 또는 컴포넌트뿐만 아니라 반복문을 사용한 요소 및 컴포넌트에도 바인딩을 사용할 수 있다. 아래의 예제를 살펴보자. Svelte로 만든 아주 간단한 Todo 페이지다. 눈여겨봐야 할 것은 each문 내부의 <input> 요소들이다.

 

<script>
	let todos = [
		{ done: false, text: 'finish Svelte tutorial' },
		{ done: false, text: 'build an app' },
		{ done: false, text: 'world domination' }
	];

	function add() {
		todos = todos.concat({ done: false, text: '' });
	}

	function clear() {
		todos = todos.filter(t => !t.done);
	}

	$: remaining = todos.filter(t => !t.done).length;
</script>

<style>
	.done {
		opacity: 0.4;
	}
</style>

<h1>Todos</h1>

{#each todos as todo}
	<div class:done={todo.done}>
		<input
			type=checkbox
			bind:checked={todo.done}
		>

		<input
			placeholder="What needs to be done?"
			bind:value={todo.text}
		>
	</div>
{/each}

<p>{remaining} remaining</p>

<button on:click={add}>
	Add new
</button>

<button on:click={clear}>
	Clear completed
</button>

 

<input> 요소는 checkbox 타입과 textbox 타입 두 가지가 있으며, 이들은 위에서 본 것과 같이 bind:checked, bind:value로 배열에 포함되어 있는 아이템들과 양방향 바인딩으로 연결되어 있음을 확인할 수 있다. 위와 같이 반복문에서도 양방향 바인딩을 사용하여 값을 추가 / 수정 / 삭제할 수 있다.

 

실행 결과는 아래와 같다.

 

 

변경 불가능한 속성 바인딩

 

일부 HTML 요소에는 값을 얻을 수는 있지만 수정은 할 수 없는 속성들이 존재한다. 블록 레벨 요소의 사이즈 값을 가지고 있는 clientWidth, clientHeight, offsetWith, offsetHeight들을 예로 들 수 있다. 이 속성들은 개발자가 절대 편집할 수 없는 값이다. 따라서 개발자는 이 속성들을 가지고 원하는 기능을 자바스크립트로 따로 구현해야 한다.

 

기능 구현을 위해 이 속성들을 가져오기 위해서는 DOM으로 해당 요소를 찾은 후 속성들을 접근해야 한다. 하지만 Svelte에서는 아래와 같은 방법으로 쉽게 값을 가져올 수 있다. 예제 코드를 살펴보자.

 

<script>
	let w;
	let h;
	let size = 42;
	let text = 'edit me';
</script>

<style>
	input { display: block; }
	div { display: inline-block; }
	span { word-break: break-all; }
</style>

<input type=range bind:value={size}>
<input bind:value={text}>

<p>size: {w}px x {h}px</p>

<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px">{text}</span>
</div>

 

위와 같이 블록 레벨 요소에 바인딩을 사용하면 아주 쉽게 값을 가져올 수 있다. 주의할 점은 위에서 봐왔던 양방향 바인딩이 아닌 오로지 값을 조회하는 목적으로만 사용할 수 있다는 것이다. 또한 이 방법을 자주 사용하는 경우 오버 헤드가 발생할 수 있다고 경고하고 있으므로 적절하게 활용하는 것이 좋겠다.

 

실행 결과는 아래와 같다.

 

 

This 바인딩

 

특정 요소 또는 컴포넌트를 변수에 저장할 때 사용하는 방법으로 VueJS의 ref와 비슷하다고 볼 수 있다. 요소를 찾기 위해 DOM을 사용하여 ID 또는 속성 등을 검색할 필요가 없다. 사용 방법은 아래와 같다.

 

const 요소 또는 컴포넌트 값을 연결할 변수

<요소 또는 컴포넌트 bind:this={요소 또는 컴포넌트 값을 연결할 변수} />

 

실제 사용 예제는 아래와 같다. canvas 요소는 DOM에 마운트 돼야만 동작하므로 나중에 살펴볼 onMount 이벤트 핸들러 내부에서 처리된다는 것을 알아두자.

 

<script>
	import { onMount } from 'svelte';

	let canvas;

	onMount(() => {
		const ctx = canvas.getContext('2d');
		let frame;

		(function loop() {
			frame = requestAnimationFrame(loop);

			const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

			for (let p = 0; p < imageData.data.length; p += 4) {
				const i = p / 4;
				const x = i % canvas.width;
				const y = i / canvas.height >>> 0;

				const t = window.performance.now();

				const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
				const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
				const b = 128;

				imageData.data[p + 0] = r;
				imageData.data[p + 1] = g;
				imageData.data[p + 2] = b;
				imageData.data[p + 3] = 255;
			}

			ctx.putImageData(imageData, 0, 0);
		}());

		return () => {
			cancelAnimationFrame(frame);
		};
	});
</script>

<style>
	canvas {
		width: 100%;
		height: 100%;
		background-color: #666;
		-webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
		mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
	}
</style>

<canvas bind:this={canvas}
	width={32}
	height={32}
></canvas>

 

실행 결과는 아래와 같다. 정상적으로 <canvas> 요소의 레퍼런스 값을 가져오는 것을 알 수 있다.

 

 

컴포넌트 양방향 바인딩

 

컴포넌트 양방향 바인딩은 상위 컴포넌트와 하위 컴포넌트 간의 데이터 통신을 쉽게 해주는 기능으로 Props를 사용하여 양방향 바인딩을 제공한다. 기본적인 사용 방법은 아래와 같다.

 

// 상위 컴포넌트. svelte

...

<하위 컴포넌트 bind:하위 컴포넌트 Props={바인딩할 변수} />

...

// 하위 컴포넌트

...

<script>

  export let 하위 컴포넌트 Props 변수 = '기본값'

</script>

...

 

예제 코드를 살펴보자. 상위 컴포넌트인 App.svelte에서 하위 컴포넌트인 Keypad의 props 이름으로 양방향 바인딩을 진행하는 것을 알 수 있다. 하위 컴포넌트에서 값이 변경되면 상위 컴포넌트의 변수도 값이 업데이트되는 것을 알 수 있다.

 

// App.svelte

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

	let pin;
	$: view = pin ? pin.replace(/\d(?!$)/g, '•') : 'enter your pin';

	function handleSubmit() {
		alert(`submitted ${pin}`);
	}
</script>

<h1 style="color: {pin ? '#333' : '#ccc'}">{view}</h1>

<Keypad bind:value={pin} on:submit={handleSubmit}/>


// Keypad.svelte


<script>
	import { createEventDispatcher } from 'svelte';

	export let value = '';

	const dispatch = createEventDispatcher();

	const select = num => () => value += num;
	const clear  = () => value = '';
	const submit = () => dispatch('submit');
</script>

<style>
	.keypad {
		display: grid;
		grid-template-columns: repeat(3, 5em);
		grid-template-rows: repeat(4, 3em);
		grid-gap: 0.5em
	}

	button {
		margin: 0
	}
</style>

<div class="keypad">
	<button on:click={select(1)}>1</button>
	<button on:click={select(2)}>2</button>
	<button on:click={select(3)}>3</button>
	<button on:click={select(4)}>4</button>
	<button on:click={select(5)}>5</button>
	<button on:click={select(6)}>6</button>
	<button on:click={select(7)}>7</button>
	<button on:click={select(8)}>8</button>
	<button on:click={select(9)}>9</button>

	<button disabled={!value} on:click={clear}>clear</button>
	<button on:click={select(0)}>0</button>
	<button disabled={!value} on:click={submit}>submit</button>
</div>

 

실행 결과는 아래와 같다.

 

 

컴포넌트 양방향 바인딩을 너무 남발해서 사용할 경우 프로젝트의 규모가 클 때 소스 코드의 추적이나 유지보수가 어렵다는 단점이 있다. 가능하면 컴포넌트 간 데이터의 흐름을 하양식으로 작성하는 것이 좋으며 양방향 바인딩은 정말 어쩔 수 없거나 1 Depth 정도의 구조에서 사용하는 것이 좋다.

 

이것으로 바인딩에 대해 마치도록 하겠다. 다음에는 컴포넌트의 생명 주기에 대해 알아보겠다. 오늘은 여기까지~

반응형

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

9. 스토어  (0) 2020.10.07
8. 컴포넌트 생명 주기  (0) 2020.09.22
6. 이벤트 처리  (0) 2020.09.08
5. 논리 문법 / Props  (0) 2020.09.04
4. 반응형 문법  (0) 2020.09.02