React에서는 value + onChange를 항상 짝으로 써야 했는데, Svelte는 bind:value 하나로 끝납니다.

개념 정의

양방향 바인딩(Two-way binding) 은 데이터와 UI 요소가 서로를 자동으로 업데이트하는 패턴입니다. Svelte는 bind: 디렉티브를 통해 간결한 양방향 바인딩을 제공합니다.

텍스트 입력

SVELTE
<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 대비 코드량 비교

JSX
// React — value + onChange 세트 필수
const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />
SVELTE
<!-- Svelte — bind:value 한 줄 -->
<input bind:value={name} />

숫자 입력

SVELTE
<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>

체크박스와 라디오

SVELTE
<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>

셀렉트 박스

SVELTE
<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

SVELTE
<script>
  let content = $state('');
</script>

<textarea bind:value={content} rows="5" placeholder="내용을 입력하세요"></textarea>
<p>글자 수: {content.length}</p>

DOM 속성 바인딩

SVELTE
<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 요소 참조

SVELTE
<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>

컴포넌트 바인딩

SVELTE
<!-- Counter.svelte -->
<script>
  let { count = $bindable(0) } = $props();
</script>

<button onclick={() => count++}>{count}</button>
SVELTE
<!-- 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이 양방향인지 명시적으로 표현합니다.

실전 예시 — 회원가입 폼

SVELTE
<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 대비 코드량이 확 줄어듭니다. 다만 모든 곳에 양방향 바인딩을 남용하면 데이터 흐름 추적이 어려워지니, 폼 입력처럼 정말 필요한 곳에만 사용하는 것이 좋습니다.

댓글 로딩 중...