React의 useState는 함수를 호출하지만, Svelte의 $state는 컴파일러가 변환합니다 — 같은 반응성이지만 구현 철학이 완전히 다릅니다.

개념 정의

Runes 는 Svelte 5에서 도입된 반응성 프리미티브입니다. $ 접두사가 붙은 특수한 함수처럼 보이지만, 실제로는 컴파일러가 변환하는 컴파일러 지시어(compiler directive) 입니다.

$state — 반응형 상태

$state는 값이 변경되면 UI가 자동으로 업데이트되는 반응형 변수 를 선언합니다.

SVELTE
<script>
  // 원시 값
  let count = $state(0);
  let name = $state('Svelte');

  // 객체 — 깊은 반응성(deep reactivity) 제공
  let user = $state({
    name: '홍길동',
    age: 25,
    hobbies: ['코딩', '독서']
  });

  function increment() {
    count++;  // 직접 변경 가능! setState() 같은 래퍼 불필요
  }

  function addHobby() {
    // 객체 내부 속성도 직접 변경 가능
    user.hobbies.push('운동');
  }
</script>

<button onclick={increment}>횟수: {count}</button>
<p>{user.name}의 취미: {user.hobbies.join(', ')}</p>
<button onclick={addHobby}>취미 추가</button>

React와의 비교

JAVASCRIPT
// React — setter 함수 필수, 불변성 유지 필요
const [count, setCount] = useState(0);
setCount(prev => prev + 1);

const [user, setUser] = useState({ name: '홍길동', hobbies: [] });
setUser(prev => ({ ...prev, hobbies: [...prev.hobbies, '운동'] }));
SVELTE
<!-- Svelte — 직접 변경, 가변(mutable) 방식 -->
<script>
  let count = $state(0);
  count++;  // 끝!

  let user = $state({ name: '홍길동', hobbies: [] });
  user.hobbies.push('운동');  // 배열 메서드 직접 사용 가능
</script>

$state.raw — 얕은 반응성

깊은 반응성이 필요 없는 대량 데이터에는 $state.raw를 사용합니다.

SVELTE
<script>
  // 내부 속성 변경은 감지하지 않음 — 재할당만 감지
  let items = $state.raw([1, 2, 3]);

  function update() {
    // items.push(4);  // 이건 UI에 반영되지 않음
    items = [...items, 4];  // 재할당해야 반영됨
  }
</script>

$derived — 파생 상태

$derived는 다른 반응형 값으로부터 자동 계산 되는 값을 선언합니다.

SVELTE
<script>
  let count = $state(0);
  let items = $state(['사과', '바나나', '딸기']);

  // count가 바뀌면 자동으로 재계산
  let doubled = $derived(count * 2);
  let isEven = $derived(count % 2 === 0);

  // 배열 기반 파생 값
  let itemCount = $derived(items.length);
  let summary = $derived(`총 ${items.length}개의 과일`);
</script>

<p>원본: {count}, 2배: {doubled}, 짝수: {isEven}</p>
<p>{summary}</p>

$derived.by — 복잡한 계산

표현식 하나로 안 되는 복잡한 로직은 $derived.by를 사용합니다.

SVELTE
<script>
  let items = $state([
    { name: '사과', price: 1000, quantity: 3 },
    { name: '바나나', price: 500, quantity: 5 },
  ]);

  // 복잡한 계산 로직
  let totalPrice = $derived.by(() => {
    let total = 0;
    for (const item of items) {
      total += item.price * item.quantity;
    }
    return total;
  });
</script>

<p>총 금액: {totalPrice.toLocaleString()}원</p>

$effect — 부수 효과

$effect는 반응형 값이 변경될 때 부수 효과(side effect) 를 실행합니다. React의 useEffect와 비슷하지만 의존성 배열을 명시하지 않아도 됩니다.

SVELTE
<script>
  let count = $state(0);
  let query = $state('');

  // count가 변경될 때마다 자동 실행
  // 의존성을 명시하지 않아도 컴파일러가 자동 추적!
  $effect(() => {
    console.log(`현재 카운트: ${count}`);
  });

  // 정리(cleanup) 함수
  $effect(() => {
    const timer = setInterval(() => {
      count++;
    }, 1000);

    // 컴포넌트 언마운트 또는 다음 실행 전에 호출
    return () => clearInterval(timer);
  });

  // 디바운스 검색 예시
  $effect(() => {
    if (!query) return;

    const timeout = setTimeout(() => {
      console.log(`검색: ${query}`);
    }, 300);

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

<input bind:value={query} placeholder="검색어 입력" />

$effect.pre — DOM 업데이트 전 실행

SVELTE
<script>
  let messages = $state([]);

  // DOM이 업데이트되기 전에 실행됩니다
  $effect.pre(() => {
    // 스크롤 위치 저장 같은 작업에 유용
    console.log('DOM 업데이트 전:', messages.length);
  });
</script>

세 Rune의 관계

PLAINTEXT
$state  ──▶  $derived  ──▶  $effect
(원본 상태)   (파생 값)      (부수 효과)
  • $state: 변경 가능한 원본 데이터
  • $derived: $state로부터 자동 계산되는 읽기 전용 값
  • $effect: $state$derived가 바뀔 때 실행되는 부수 효과
SVELTE
<script>
  // 1. 원본 상태
  let celsius = $state(0);

  // 2. 파생 값 — celsius가 바뀌면 자동 재계산
  let fahrenheit = $derived(celsius * 9/5 + 32);

  // 3. 부수 효과 — fahrenheit가 바뀌면 자동 실행
  $effect(() => {
    if (fahrenheit > 100) {
      console.log('경고: 매우 높은 온도!');
    }
  });
</script>

<input type="number" bind:value={celsius} /> °C
<p>= {fahrenheit} °F</p>

주의사항

  1. $effect 안에서 $state를 변경하면 무한 루프 위험
SVELTE
<script>
  let count = $state(0);

  // 이렇게 하면 무한 루프!
  // $effect(() => {
  //   count = count + 1;
  // });

  // $derived를 사용하세요
  let doubled = $derived(count * 2);
</script>
  1. $derived는 읽기 전용
SVELTE
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);

  // doubled = 10;  // 에러! $derived 값은 변경 불가
</script>

면접 포인트

  • "Svelte의 반응성은 어떻게 동작하나요?": 컴파일러가 $state 변수의 사용처를 추적하여, 값 변경 시 해당 DOM만 업데이트하는 코드를 빌드 타임에 생성합니다.
  • "React useEffect와 $effect의 차이는?": useEffect는 의존성 배열을 수동으로 관리하지만, $effect는 컴파일러가 의존성을 자동 추적합니다. 의존성 누락 버그가 원천 차단됩니다.
  • "왜 Runes를 도입했나요?": Svelte 4의 암시적 반응성($:)은 컴포넌트 외부에서 사용할 수 없고, 어떤 값이 반응형인지 코드만 보고 파악하기 어려웠습니다. Runes는 명시적이면서도 어디서든 사용 가능합니다.

정리

$state, $derived, $effect — 이 세 가지만 제대로 이해하면 Svelte 반응성의 80%는 잡은 겁니다. React보다 직관적이면서도 의존성 관리 실수를 컴파일러가 막아주니, "개발자 경험(DX)이 좋다"는 Svelte의 평가가 여기서 나옵니다.

댓글 로딩 중...