$state만으로는 부족한 순간이 옵니다 — 대량 데이터, 디버깅, 성능 튜닝에 필요한 심화 Runes를 알아봅시다.

$state.raw — 얕은 반응성

기본 $state는 객체 내부까지 깊은 반응성을 제공합니다. $state.raw는 재할당만 감지합니다.

SVELTE
<script>
  // 깊은 반응성 — 내부 속성 변경도 감지
  let deep = $state({ items: [1, 2, 3] });
  deep.items.push(4);  // UI 업데이트됨

  // 얕은 반응성 — 재할당만 감지
  let raw = $state.raw({ items: [1, 2, 3] });
  raw.items.push(4);           // UI 업데이트 안 됨!
  raw = { items: [...raw.items, 4] };  // 이렇게 해야 됨
</script>

언제 쓰나요?: 수천 개의 항목이 있는 배열이나, 외부 라이브러리에서 관리하는 객체처럼 깊은 프록시가 성능을 떨어뜨리는 경우에 사용합니다.

SVELTE
<script>
  // 대량 데이터에는 $state.raw가 유리
  let largeDataset = $state.raw(
    Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `항목 ${i}`,
      value: Math.random(),
    }))
  );

  function updateItem(id, newValue) {
    // 전체 재할당 (불변 업데이트)
    largeDataset = largeDataset.map(item =>
      item.id === id ? { ...item, value: newValue } : item
    );
  }
</script>

$state.snapshot — 반응형 프록시의 순수 복사본

SVELTE
<script>
  let user = $state({ name: '홍길동', scores: [90, 85, 92] });

  function logUser() {
    // $state 객체를 console.log하면 Proxy가 보임
    console.log(user);  // Proxy { ... }

    // snapshot으로 순수 객체 얻기
    console.log($state.snapshot(user));  // { name: '홍길동', scores: [90, 85, 92] }
  }

  function sendToAPI() {
    // API에 보낼 때도 snapshot 사용
    fetch('/api/user', {
      method: 'POST',
      body: JSON.stringify($state.snapshot(user)),
    });
  }
</script>

$inspect — 개발 모드 디버깅

SVELTE
<script>
  let count = $state(0);
  let user = $state({ name: '홍길동' });

  // count나 user가 변경될 때마다 콘솔에 출력
  $inspect(count);
  $inspect(user);

  // 커스텀 콜백
  $inspect(count).with((type, value) => {
    // type: 'init' | 'update'
    if (type === 'update') {
      console.log('카운트 변경됨:', value);
    }
  });

  // 디버거 중단점 설정
  $inspect(count).with((type, value) => {
    if (value > 10) debugger;
  });
</script>

** 주의 **: $inspect는 개발 모드에서만 동작합니다. 프로덕션 빌드에서는 자동으로 제거됩니다.

untrack — 의존성 추적 비활성화

SVELTE
<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let logCount = $state(0);

  $effect(() => {
    // count 변경 시 실행되지만,
    // logCount 변경은 추적하지 않음
    console.log(`count: ${count}, logCount: ${untrack(() => logCount)}`);
  });
</script>

$effect와 세밀한 제어

SVELTE
<script>
  import { untrack } from 'svelte';

  let searchQuery = $state('');
  let searchResults = $state([]);
  let isLoading = $state(false);

  // searchQuery만 추적, isLoading과 searchResults는 추적 안 함
  $effect(() => {
    const query = searchQuery;  // 이 줄에서 의존성 등록

    if (!query) {
      untrack(() => {
        searchResults = [];
        isLoading = false;
      });
      return;
    }

    untrack(() => { isLoading = true; });

    const timeout = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${query}`);
      const data = await res.json();
      untrack(() => {
        searchResults = data;
        isLoading = false;
      });
    }, 300);

    return () => clearTimeout(timeout);
  });
</script>

클래스에서 Runes 사용

SVELTE
<script>
  class Counter {
    count = $state(0);
    doubled = $derived(this.count * 2);

    increment() {
      this.count++;
    }

    reset() {
      this.count = 0;
    }
  }

  class TodoList {
    items = $state([]);
    filter = $state('all');

    filtered = $derived.by(() => {
      switch (this.filter) {
        case 'active': return this.items.filter(i => !i.done);
        case 'done': return this.items.filter(i => i.done);
        default: return this.items;
      }
    });

    add(text) {
      this.items.push({ id: Date.now(), text, done: false });
    }

    toggle(id) {
      const item = this.items.find(i => i.id === id);
      if (item) item.done = !item.done;
    }
  }

  const counter = new Counter();
  const todos = new TodoList();
</script>

<button onclick={() => counter.increment()}>
  {counter.count} (x2 = {counter.doubled})
</button>

면접 포인트

  • "$state와 $state.raw의 성능 차이는?": $state는 Proxy를 사용해 깊은 반응성을 제공하므로, 대량 데이터에서는 프록시 생성과 속성 접근에 오버헤드가 있습니다. $state.raw는 프록시를 만들지 않아 메모리와 CPU 사용이 적지만, 불변 업데이트가 필요합니다.
  • "untrack은 왜 필요한가요?": $effect 안에서 특정 반응형 값을 읽되 의존성으로 등록하고 싶지 않을 때 사용합니다. 무한 루프 방지나 불필요한 재실행 방지에 필수적입니다.

정리

Runes 심화 기능은 "대부분의 경우는 기본 $state로 충분하지만, 특수한 상황에서 세밀한 제어가 필요할 때" 사용합니다. $state.raw로 성능 최적화, $inspect로 디버깅, untrack으로 의존성 제어 — 이 도구들을 알면 복잡한 반응성 시나리오도 깔끔하게 처리할 수 있습니다.

댓글 로딩 중...