Svelte 5의 $props()는 구조 분해 한 줄로 모든 props를 받습니다 — export let보다 훨씬 깔끔합니다.

개념 정의

Props(Properties) 는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 단방향 데이터 흐름입니다. Svelte 5에서는 $props() rune으로 props를 선언합니다.

기본 Props 선언

SVELTE
<!-- Greeting.svelte -->
<script>
  // 구조 분해로 props 받기
  let { name, age } = $props();
</script>

<p>{name}님 ({age}세), 환영합니다!</p>
SVELTE
<!-- App.svelte -->
<script>
  import Greeting from './Greeting.svelte';
</script>

<Greeting name="홍길동" age={25} />

기본값 설정

SVELTE
<script>
  let {
    name = '게스트',        // 기본값: '게스트'
    variant = 'primary',   // 기본값: 'primary'
    size = 'md',           // 기본값: 'md'
    disabled = false,      // 기본값: false
  } = $props();
</script>

<button class="btn btn-{variant} btn-{size}" {disabled}>
  {name}
</button>

나머지 Props (Rest Props)

SVELTE
<!-- Input.svelte -->
<script>
  // 특정 props만 빼고 나머지는 그대로 전달
  let { label, error, ...restProps } = $props();
</script>

<div class="field">
  {#if label}
    <label>{label}</label>
  {/if}
  <!-- 나머지 props를 input에 전달 -->
  <input {...restProps} class:error />
  {#if error}
    <span class="error-msg">{error}</span>
  {/if}
</div>

<style>
  .error { border-color: red; }
  .error-msg { color: red; font-size: 0.8rem; }
</style>
SVELTE
<!-- 사용 -->
<Input
  label="이메일"
  type="email"
  placeholder="example@email.com"
  required
  error={emailError}
/>

콜백 Props로 자식 → 부모 통신

SVELTE
<!-- SearchBar.svelte -->
<script>
  let { onSearch, onClear, placeholder = '검색...' } = $props();
  let query = $state('');

  function handleSubmit(e) {
    e.preventDefault();
    onSearch?.(query);  // 옵셔널 체이닝으로 안전하게 호출
  }
</script>

<form onsubmit={handleSubmit}>
  <input bind:value={query} {placeholder} />
  <button type="submit">검색</button>
  {#if query}
    <button type="button" onclick={() => { query = ''; onClear?.(); }}>
      초기화
    </button>
  {/if}
</form>
SVELTE
<!-- App.svelte -->
<script>
  import SearchBar from './SearchBar.svelte';
  let results = $state([]);

  async function handleSearch(query) {
    const res = await fetch(`/api/search?q=${query}`);
    results = await res.json();
  }

  function handleClear() {
    results = [];
  }
</script>

<SearchBar onSearch={handleSearch} onClear={handleClear} />

타입 안전한 Props (TypeScript)

SVELTE
<!-- UserCard.svelte -->
<script lang="ts">
  interface Props {
    name: string;
    email: string;
    avatar?: string;
    role?: 'admin' | 'user' | 'editor';
    onSelect?: (id: string) => void;
  }

  let {
    name,
    email,
    avatar = '/default-avatar.png',
    role = 'user',
    onSelect
  }: Props = $props();
</script>

<div class="card" onclick={() => onSelect?.(email)}>
  <img src={avatar} alt={name} />
  <h3>{name}</h3>
  <p>{email}</p>
  <span class="badge badge-{role}">{role}</span>
</div>

Props 반응성

$props()로 받은 값은 자동으로 반응형 입니다. 부모에서 값이 바뀌면 자식도 자동 업데이트됩니다.

SVELTE
<!-- Counter.svelte -->
<script>
  let { initialCount = 0 } = $props();
  // props를 로컬 상태로 복사해서 사용
  let count = $state(initialCount);
</script>

<button onclick={() => count++}>
  초기값: {initialCount}, 현재: {count}
</button>
SVELTE
<!-- App.svelte -->
<script>
  import Counter from './Counter.svelte';
  let start = $state(0);
</script>

<input type="number" bind:value={start} />
<Counter initialCount={start} />

**주의 **: props 값을 로컬 $state에 복사하면, 이후 부모에서 변경해도 로컬 상태는 업데이트되지 않습니다. 동기화가 필요하면 $derived$effect를 사용해야 합니다.

실전 패턴 — 컴포넌트 컴포지션

SVELTE
<!-- DataTable.svelte -->
<script>
  let {
    data = [],
    columns = [],
    onRowClick,
    sortable = false,
    emptyMessage = '데이터가 없습니다',
  } = $props();

  let sortKey = $state('');
  let sortDir = $state('asc');

  let sortedData = $derived.by(() => {
    if (!sortKey) return data;
    return [...data].sort((a, b) => {
      const mul = sortDir === 'asc' ? 1 : -1;
      return String(a[sortKey]).localeCompare(String(b[sortKey])) * mul;
    });
  });

  function toggleSort(key) {
    if (sortKey === key) {
      sortDir = sortDir === 'asc' ? 'desc' : 'asc';
    } else {
      sortKey = key;
      sortDir = 'asc';
    }
  }
</script>

<table>
  <thead>
    <tr>
      {#each columns as col}
        <th onclick={() => sortable && toggleSort(col.key)}>
          {col.label}
          {#if sortKey === col.key}
            {sortDir === 'asc' ? '▲' : '▼'}
          {/if}
        </th>
      {/each}
    </tr>
  </thead>
  <tbody>
    {#each sortedData as row}
      <tr onclick={() => onRowClick?.(row)}>
        {#each columns as col}
          <td>{row[col.key]}</td>
        {/each}
      </tr>
    {:else}
      <tr><td colspan={columns.length}>{emptyMessage}</td></tr>
    {/each}
  </tbody>
</table>

면접 포인트

  • "Svelte 5에서 export let이 사라진 이유는?": export let은 JavaScript 표준에서 벗어난 문법이었고, 어떤 것이 prop인지 코드에서 명확하지 않았습니다. $props()는 명시적이고 구조 분해와 자연스럽게 결합됩니다.
  • "단방향 vs 양방향 데이터 흐름?": 기본 Props는 단방향(부모→자식)입니다. 양방향이 필요하면 $bindable을 사용하지만, 대부분은 콜백 props로 자식→부모 통신을 처리하는 것이 데이터 흐름 추적에 유리합니다.

정리

Props는 컴포넌트 간 통신의 기본입니다. Svelte 5의 $props()는 구조 분해, 기본값, rest props를 JavaScript 표준 문법 그대로 사용하게 해줍니다. 콜백 props 패턴까지 익히면 대부분의 부모-자식 통신 시나리오를 깔끔하게 처리할 수 있습니다.

댓글 로딩 중...