바인딩 — bind:로 양방향 데이터 흐름 만들기
React에서는
value + onChange를 항상 짝으로 써야 했는데, Svelte는bind:value하나로 끝납니다.
개념 정의
양방향 바인딩(Two-way binding) 은 데이터와 UI 요소가 서로를 자동으로 업데이트하는 패턴입니다. Svelte는 bind: 디렉티브를 통해 간결한 양방향 바인딩을 제공합니다.
텍스트 입력
<script>
let name = $state('');
let email = $state('');
</script>
<!-- bind:value로 양방향 바인딩 -->
<input bind:value={name} placeholder="이름" />
<input type="email" bind:value={email} placeholder="이메일" />
<p>이름: {name}, 이메일: {email}</p>
React 대비 코드량 비교
// React — value + onChange 세트 필수
const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />
<!-- Svelte — bind:value 한 줄 -->
<input bind:value={name} />
숫자 입력
<script>
let age = $state(0);
let price = $state(0);
</script>
<!-- type="number"면 자동으로 숫자 타입 유지 -->
<input type="number" bind:value={age} min="0" max="150" />
<input type="range" bind:value={price} min="0" max="100000" step="1000" />
<p>나이: {age} (타입: {typeof age})</p>
<p>가격: {price.toLocaleString()}원</p>
체크박스와 라디오
<script>
let agreed = $state(false);
let selectedColor = $state('red');
let selectedFruits = $state([]);
</script>
<!-- 체크박스 — bind:checked -->
<label>
<input type="checkbox" bind:checked={agreed} />
약관에 동의합니다
</label>
<!-- 라디오 버튼 — bind:group -->
<label><input type="radio" bind:group={selectedColor} value="red" /> 빨강</label>
<label><input type="radio" bind:group={selectedColor} value="blue" /> 파랑</label>
<label><input type="radio" bind:group={selectedColor} value="green" /> 초록</label>
<p>선택된 색상: {selectedColor}</p>
<!-- 체크박스 그룹 — bind:group으로 배열 관리 -->
<label><input type="checkbox" bind:group={selectedFruits} value="apple" /> 사과</label>
<label><input type="checkbox" bind:group={selectedFruits} value="banana" /> 바나나</label>
<label><input type="checkbox" bind:group={selectedFruits} value="grape" /> 포도</label>
<p>선택된 과일: {selectedFruits.join(', ')}</p>
셀렉트 박스
<script>
let selected = $state('');
let multiSelected = $state([]);
const options = [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
];
</script>
<!-- 단일 선택 -->
<select bind:value={selected}>
<option value="">선택하세요</option>
{#each options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
<!-- 다중 선택 -->
<select multiple bind:value={multiSelected}>
{#each options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
textarea
<script>
let content = $state('');
</script>
<textarea bind:value={content} rows="5" placeholder="내용을 입력하세요"></textarea>
<p>글자 수: {content.length}</p>
DOM 속성 바인딩
<script>
let divWidth = $state(0);
let divHeight = $state(0);
let videoCurrentTime = $state(0);
</script>
<!-- 요소 크기 바인딩 (읽기 전용) -->
<div bind:clientWidth={divWidth} bind:clientHeight={divHeight}>
이 div의 크기: {divWidth} x {divHeight}
</div>
<!-- 미디어 바인딩 -->
<video
bind:currentTime={videoCurrentTime}
bind:duration
bind:paused
src="/video.mp4"
/>
this 바인딩 — DOM 요소 참조
<script>
let inputEl = $state(null);
let canvasEl = $state(null);
$effect(() => {
// DOM이 마운트된 후 실행
if (inputEl) {
inputEl.focus();
}
});
$effect(() => {
if (canvasEl) {
const ctx = canvasEl.getContext('2d');
ctx.fillStyle = '#ff3e00';
ctx.fillRect(10, 10, 100, 100);
}
});
</script>
<input bind:this={inputEl} placeholder="자동 포커스" />
<canvas bind:this={canvasEl} width="200" height="200"></canvas>
컴포넌트 바인딩
<!-- Counter.svelte -->
<script>
let { count = $bindable(0) } = $props();
</script>
<button onclick={() => count++}>{count}</button>
<!-- App.svelte -->
<script>
import Counter from './Counter.svelte';
let value = $state(0);
</script>
<!-- 컴포넌트 props에 양방향 바인딩 -->
<Counter bind:count={value} />
<p>부모에서 본 값: {value}</p>
$bindable()로 선언된 prop만 bind:가 가능합니다. 이는 어떤 prop이 양방향인지 명시적으로 표현합니다.
실전 예시 — 회원가입 폼
<script>
let form = $state({
username: '',
email: '',
password: '',
agreed: false,
role: 'user',
});
let isValid = $derived(
form.username.length >= 3 &&
form.email.includes('@') &&
form.password.length >= 8 &&
form.agreed
);
function handleSubmit(e) {
e.preventDefault();
if (!isValid) return;
console.log('제출:', form);
}
</script>
<form onsubmit={handleSubmit}>
<input bind:value={form.username} placeholder="사용자명 (3자 이상)" />
<input type="email" bind:value={form.email} placeholder="이메일" />
<input type="password" bind:value={form.password} placeholder="비밀번호 (8자 이상)" />
<select bind:value={form.role}>
<option value="user">일반 사용자</option>
<option value="admin">관리자</option>
</select>
<label>
<input type="checkbox" bind:checked={form.agreed} />
이용약관 동의
</label>
<button type="submit" disabled={!isValid}>가입하기</button>
</form>
면접 포인트
- "양방향 바인딩의 단점은?": 데이터 흐름이 복잡해질 수 있습니다. React가 단방향 데이터 흐름을 고수하는 이유이기도 합니다. Svelte는
$bindable로 양방향 바인딩 가능한 prop을 명시적으로 제한하여 이 문제를 완화합니다. - "bind:group은 내부적으로 어떻게 동작하나요?": 같은 변수에 바인딩된 라디오/체크박스들을 컴파일러가 그룹으로 인식하여, 변경 시 해당 변수를 자동 업데이트하는 코드를 생성합니다.
정리
bind:는 Svelte의 생산성을 크게 높이는 기능입니다. 특히 폼을 다룰 때 React 대비 코드량이 확 줄어듭니다. 다만 모든 곳에 양방향 바인딩을 남용하면 데이터 흐름 추적이 어려워지니, 폼 입력처럼 정말 필요한 곳에만 사용하는 것이 좋습니다.
댓글 로딩 중...