반응성 기초 — $state, $derived, $effect 제대로 이해하기
React의
useState는 함수를 호출하지만, Svelte의$state는 컴파일러가 변환합니다 — 같은 반응성이지만 구현 철학이 완전히 다릅니다.
개념 정의
Runes 는 Svelte 5에서 도입된 반응성 프리미티브입니다. $ 접두사가 붙은 특수한 함수처럼 보이지만, 실제로는 컴파일러가 변환하는 컴파일러 지시어(compiler directive) 입니다.
$state — 반응형 상태
$state는 값이 변경되면 UI가 자동으로 업데이트되는 반응형 변수 를 선언합니다.
<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와의 비교
// React — setter 함수 필수, 불변성 유지 필요
const [count, setCount] = useState(0);
setCount(prev => prev + 1);
const [user, setUser] = useState({ name: '홍길동', hobbies: [] });
setUser(prev => ({ ...prev, hobbies: [...prev.hobbies, '운동'] }));
<!-- Svelte — 직접 변경, 가변(mutable) 방식 -->
<script>
let count = $state(0);
count++; // 끝!
let user = $state({ name: '홍길동', hobbies: [] });
user.hobbies.push('운동'); // 배열 메서드 직접 사용 가능
</script>
$state.raw — 얕은 반응성
깊은 반응성이 필요 없는 대량 데이터에는 $state.raw를 사용합니다.
<script>
// 내부 속성 변경은 감지하지 않음 — 재할당만 감지
let items = $state.raw([1, 2, 3]);
function update() {
// items.push(4); // 이건 UI에 반영되지 않음
items = [...items, 4]; // 재할당해야 반영됨
}
</script>
$derived — 파생 상태
$derived는 다른 반응형 값으로부터 자동 계산 되는 값을 선언합니다.
<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를 사용합니다.
<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와 비슷하지만 의존성 배열을 명시하지 않아도 됩니다.
<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 업데이트 전 실행
<script>
let messages = $state([]);
// DOM이 업데이트되기 전에 실행됩니다
$effect.pre(() => {
// 스크롤 위치 저장 같은 작업에 유용
console.log('DOM 업데이트 전:', messages.length);
});
</script>
세 Rune의 관계
$state ──▶ $derived ──▶ $effect
(원본 상태) (파생 값) (부수 효과)
$state: 변경 가능한 원본 데이터$derived:$state로부터 자동 계산되는 읽기 전용 값$effect:$state나$derived가 바뀔 때 실행되는 부수 효과
<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>
주의사항
- $effect 안에서 $state를 변경하면 무한 루프 위험
<script>
let count = $state(0);
// 이렇게 하면 무한 루프!
// $effect(() => {
// count = count + 1;
// });
// $derived를 사용하세요
let doubled = $derived(count * 2);
</script>
- $derived는 읽기 전용
<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의 평가가 여기서 나옵니다.
댓글 로딩 중...