5. 이벤트 처리
Svelte 5는 이벤트 처리 방식에 중요한 변화를 도입하여 더 직관적이고 성능이 뛰어난 방식으로 사용자 상호작용을 처리할 수 있게 되었습니다.
기존의 on:
지시문에서 표준 DOM 이벤트 속성으로 전환하면서 웹 표준에 더 가깝고 TypeScript 호환성이 크게 향상되었습니다.
이 장에서는 DOM 이벤트부터 커스텀 이벤트, 폼 처리까지 Svelte 5의 모든 이벤트 처리 방법을 완전히 마스터해보겠습니다.
5.1 DOM 이벤트 핸들링
Svelte 5의 새로운 이벤트 문법
Svelte 5는 이벤트 처리 방식을 크게 개선했습니다.
기존의 on:click
문법에서 표준 DOM 속성인 onclick
을 직접 사용하는 방식으로 변경되어 더 직관적이고 웹 표준에 부합합니다.
이러한 변화는 TypeScript 지원을 크게 개선하고 개발자 경험을 향상시킵니다.
Svelte 4 vs Svelte 5 비교
특징 | Svelte 4 | Svelte 5 |
---|---|---|
기본 이벤트 | <button on:click={fn}> | <button onclick={fn}> |
이벤트 수식어 | on:click | preventDefault | 핸들러 내부에서 직접 처리 |
TypeScript | 제한적 타입 지원 | 완전한 DOM 이벤트 타입 지원 |
인라인 핸들러 | on:click={() => count++} | onclick={() => count++} |
기본 이벤트 핸들링
<script>
let count = $state(0);
function handleClick() {
count += 1;
}
</script>
<div class="counter">
<p>카운트: {count}</p>
<!-- 함수 참조로 이벤트 핸들러 연결 -->
<button onclick="{handleClick}">클릭 (+1)</button>
<!-- 인라인 이벤트 핸들러 -->
<button onclick="{()" ="">
count += 5}>빠른 증가 (+5)
</button>
<!-- 더블클릭으로 리셋 -->
<button ondblclick="{()" ="">count = 0}>리셋</button>
</div>
<style>
.counter {
text-align: center;
padding: 2rem;
}
button {
margin: 0.5rem;
padding: 0.75rem 1.5rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
다양한 DOM 이벤트
<script>
let mousePos = $state({ x: 0, y: 0 });
let inputValue = $state('');
let keyPressed = $state('');
function handleMouseMove(event) {
mousePos.x = event.clientX;
mousePos.y = event.clientY;
}
function handleKeyDown(event) {
keyPressed = event.key;
}
</script>
<div>
<!-- 마우스 이벤트 -->
<div class="mouse-area" onmousemove="{handleMouseMove}">
마우스 위치: ({mousePos.x}, {mousePos.y})
</div>
<!-- 키보드 이벤트 -->
<input
type="text"
placeholder="여기에 타이핑해보세요"
bind:value="{inputValue}"
onkeydown="{handleKeyDown}"
/>
<p>입력: "{inputValue}" | 마지막 키: "{keyPressed}"</p>
</div>
<style>
.mouse-area {
height: 100px;
background: #667eea;
color: white;
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
}
</style>
preventDefault와 stopPropagation
Svelte 5에서는 이벤트 수식어가 제거되고 표준 JavaScript 메서드를 직접 사용합니다. 이는 더 명시적이고 예측 가능한 동작을 제공하며, 모든 로직이 한 곳에 모이게 됩니다. 특히 폼 처리나 링크 클릭 방지 등에서 중요한 역할을 합니다.
<script>
let logs = $state([]);
function log(message) {
logs = [message, ...logs.slice(0, 2)]; // 최근 3개만 유지
}
function handleFormSubmit(event) {
event.preventDefault();
log('폼 제출됨 (새로고침 방지)');
}
function handleLinkClick(event) {
event.preventDefault();
log('링크 클릭됨 (네비게이션 방지)');
}
function handleParentClick() {
log('부모 클릭');
}
function handleChildClick(event) {
event.stopPropagation();
log('자식 클릭 (전파 중단)');
}
</script>
<div>
<!-- 폼 제출 방지 -->
<form onsubmit="{handleFormSubmit}">
<input type="text" placeholder="텍스트 입력" />
<button type="submit">제출</button>
</form>
<!-- 링크 클릭 방지 -->
<a href="https://example.com" onclick="{handleLinkClick}">
테스트 링크 (클릭해도 이동 안됨)
</a>
<!-- 이벤트 전파 제어 -->
<div class="parent" onclick="{handleParentClick}">
부모 요소
<div class="child" onclick="{handleChildClick}">
자식 요소 (전파 차단)
</div>
</div>
<!-- 로그 출력 -->
<div class="logs">
{#each logs as logEntry}
<div>{logEntry}</div>
{/each}
</div>
</div>
<style>
.parent {
background: #dbeafe;
padding: 1rem;
margin: 1rem 0;
cursor: pointer;
}
.child {
background: #fef2f2;
padding: 0.5rem;
margin: 0.5rem;
}
.logs {
background: #f9fafb;
padding: 1rem;
margin-top: 1rem;
}
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
5.2 커스텀 이벤트
createEventDispatcher에서 콜백 Props로의 변화
Svelte 5는 컴포넌트 간 이벤트 통신 방식을 크게 개선했습니다.
기존의 createEventDispatcher
와 on:
문법에서 콜백 props 방식으로 변경되어 더 직관적이고 타입 안전한 컴포넌트 통신이 가능해졌습니다.
이는 React의 콜백 props 패턴과 유사하여 다른 프레임워크 경험이 있는 개발자들에게도 친숙합니다.
Svelte 4 vs Svelte 5 비교
특징 | Svelte 4 | Svelte 5 |
---|---|---|
이벤트 발송 | dispatch('eventName', data) | callback(data) 직접 호출 |
이벤트 수신 | <Component on:eventName={handler}> | <Component callback={handler}> |
타입 안전성 | 제한적 (문자열 기반) | 완전한 TypeScript 지원 |
새로운 콜백 Props 패턴
<!-- Counter.svelte - 자식 컴포넌트 -->
<script>
let { initialValue = 0, onIncrement, onReset } = $props();
let count = $state(initialValue);
function increment() {
count += 1;
onIncrement?.(count); // 옵셔널 체이닝으로 안전하게 호출
}
function reset() {
count = initialValue;
onReset?.(count);
}
</script>
<div class="counter">
<div class="display">{count}</div>
<button onclick="{increment}">+1</button>
<button onclick="{reset}">리셋</button>
</div>
<style>
.counter {
border: 2px solid #e2e8f0;
padding: 1rem;
text-align: center;
}
.display {
font-size: 2rem;
font-weight: bold;
margin: 1rem 0;
}
button {
margin: 0 0.25rem;
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
}
</style>
<!-- CounterApp.svelte - 부모 컴포넌트 -->
<script>
import Counter from './Counter.svelte';
let events = $state([]);
function handleIncrement(value) {
events = [`증가: ${value}`, ...events.slice(0, 2)];
}
function handleReset(value) {
events = [`리셋: ${value}`, ...events.slice(0, 2)];
}
</script>
<div>
<h2>콜백 Props 예제</h2>
<Counter
initialValue="{10}"
onIncrement="{handleIncrement}"
onReset="{handleReset}"
/>
<div class="events">
<h3>이벤트 로그:</h3>
{#each events as event}
<div>{event}</div>
{/each}
</div>
</div>
<style>
.events {
margin-top: 1rem;
padding: 1rem;
background: #f8fafc;
}
</style>
복잡한 데이터 전달
콜백 props를 통해 복잡한 데이터 구조도 안전하게 전달할 수 있습니다. 객체나 배열 형태의 데이터를 전달하거나, 여러 개의 콜백을 통해 다양한 이벤트를 처리할 수 있습니다.
<!-- TaskItem.svelte -->
<script>
let { task, onToggle, onDelete } = $props();
function handleToggle() {
onToggle?.({
id: task.id,
completed: !task.completed,
});
}
function handleDelete() {
onDelete?.({
id: task.id,
title: task.title,
});
}
</script>
<div class="task-item">
<span class:completed="{task.completed}"
>{task.title}</span
>
<button onclick="{handleToggle}">
{task.completed ? '완료취소' : '완료'}
</button>
<button onclick="{handleDelete}">삭제</button>
</div>
<style>
.task-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem;
border: 1px solid #e2e8f0;
margin: 0.5rem 0;
}
.completed {
text-decoration: line-through;
opacity: 0.6;
}
button {
padding: 0.25rem 0.5rem;
background: #6b7280;
color: white;
border: none;
border-radius: 4px;
}
</style>
<!-- TaskApp.svelte -->
<script>
import TaskItem from './TaskItem.svelte';
let tasks = $state([
{ id: 1, title: 'Svelte 5 학습', completed: false },
{ id: 2, title: '이벤트 처리 실습', completed: true },
]);
function handleToggle(data) {
const task = tasks.find(t => t.id === data.id);
if (task) {
task.completed = data.completed;
}
}
function handleDelete(data) {
tasks = tasks.filter(t => t.id !== data.id);
}
</script>
<div>
<h2>할 일 목록</h2>
{#each tasks as task (task.id)}
<TaskItem
{task}
onToggle="{handleToggle}"
onDelete="{handleDelete}"
/>
{/each}
</div>
실습해보기: Svelte REPL에서 이 콜백 시스템을 직접 실험해보세요!
5.3 폼 처리
양방향 데이터 바인딩
Svelte의 bind:
지시문은 폼 처리를 매우 간단하게 만듭니다.
입력 필드와 컴포넌트 상태 간의 양방향 연결을 자동으로 처리하여 별도의 이벤트 핸들러 없이도 실시간으로 데이터가 동기화됩니다.
다양한 입력 요소 타입에 대해 적절한 바인딩 방식을 제공합니다.
기본 폼 바인딩
<script>
let formData = $state({
name: '',
email: '',
age: '',
gender: '',
interests: [],
newsletter: false,
});
function handleSubmit(event) {
event.preventDefault();
console.log('제출 데이터:', formData);
alert('폼 제출됨 (콘솔 확인)');
}
</script>
<div class="form-container">
<form onsubmit="{handleSubmit}">
<h2>회원가입 폼</h2>
<!-- 텍스트 입력 -->
<div class="field">
<label>이름:</label>
<input type="text" bind:value="{formData.name}" />
</div>
<div class="field">
<label>이메일:</label>
<input type="email" bind:value="{formData.email}" />
</div>
<!-- 숫자 입력 -->
<div class="field">
<label>나이:</label>
<input type="number" bind:value="{formData.age}" />
</div>
<!-- 셀렉트 -->
<div class="field">
<label>성별:</label>
<select bind:value="{formData.gender}">
<option value="">선택하세요</option>
<option value="male">남성</option>
<option value="female">여성</option>
</select>
</div>
<!-- 체크박스 그룹 -->
<div class="field">
<label>관심사:</label>
<div>
<label
><input
type="checkbox"
bind:group="{formData.interests}"
value="sports"
/>
운동</label
>
<label
><input
type="checkbox"
bind:group="{formData.interests}"
value="music"
/>
음악</label
>
</div>
</div>
<!-- 단일 체크박스 -->
<div class="field">
<label>
<input
type="checkbox"
bind:checked="{formData.newsletter}"
/>
뉴스레터 구독
</label>
</div>
<button type="submit">제출</button>
</form>
<!-- 실시간 미리보기 -->
<div class="preview">
<h3>입력 데이터:</h3>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
</div>
<style>
.form-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
max-width: 800px;
}
.field {
margin-bottom: 1rem;
}
input,
select {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 4px;
}
.preview {
background: #f8fafc;
padding: 1rem;
}
</style>
폼 검증
폼 검증은 사용자 경험의 핵심 요소입니다. Svelte에서는 반응형 상태와 계산된 값을 활용하여 실시간 검증과 사용자 친화적인 오류 메시지를 쉽게 구현할 수 있습니다.
<script>
let formData = $state({
username: '',
email: '',
password: '',
});
let touched = $state({
username: false,
email: false,
password: false,
});
// 검증 규칙
let validation = $derived({
username: {
isValid: formData.username.length >= 3,
message: '사용자명은 3글자 이상',
},
email: {
isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
formData.email
),
message: '올바른 이메일 형식 아님',
},
password: {
isValid: formData.password.length >= 6,
message: '비밀번호는 6글자 이상',
},
});
let isFormValid = $derived(
validation.username.isValid &&
validation.email.isValid &&
validation.password.isValid
);
function handleSubmit(event) {
event.preventDefault();
// 모든 필드를 터치 상태로 설정
Object.keys(touched).forEach(
key => (touched[key] = true)
);
if (isFormValid) {
alert('회원가입 완료!');
}
}
</script>
<form onsubmit="{handleSubmit}" class="validation-form">
<h2>검증 폼</h2>
<div class="field">
<label>사용자명:</label>
<input
type="text"
bind:value="{formData.username}"
onblur="{()"
=""
/>
touched.username = true} class:error={touched.username
&& !validation.username.isValid} /> {#if
touched.username && !validation.username.isValid}
<span class="error-message"
>{validation.username.message}</span
>
{/if}
</div>
<div class="field">
<label>이메일:</label>
<input
type="email"
bind:value="{formData.email}"
onblur="{()"
=""
/>
touched.email = true} class:error={touched.email &&
!validation.email.isValid} /> {#if touched.email &&
!validation.email.isValid}
<span class="error-message"
>{validation.email.message}</span
>
{/if}
</div>
<div class="field">
<label>비밀번호:</label>
<input
type="password"
bind:value="{formData.password}"
onblur="{()"
=""
/>
touched.password = true} class:error={touched.password
&& !validation.password.isValid} /> {#if
touched.password && !validation.password.isValid}
<span class="error-message"
>{validation.password.message}</span
>
{/if}
</div>
<button type="submit" disabled="{!isFormValid}">
{isFormValid ? '가입하기' : '입력 정보 확인'}
</button>
</form>
<style>
.validation-form {
max-width: 400px;
padding: 2rem;
border: 1px solid #e2e8f0;
}
.field {
margin-bottom: 1rem;
}
input {
width: 100%;
padding: 0.5rem;
border: 2px solid #e5e7eb;
border-radius: 4px;
}
input.error {
border-color: #ef4444;
}
.error-message {
color: #ef4444;
font-size: 0.875rem;
}
button:disabled {
background: #e5e7eb;
cursor: not-allowed;
}
</style>
제출 처리
폼 제출 시 데이터 검증, 로딩 상태 관리, 에러 처리 등을 포함한 완전한 폼 처리 예제입니다.
<script>
let formData = $state({
email: '',
message: '',
});
let isSubmitting = $state(false);
let submitStatus = $state(''); // 'success', 'error', ''
async function handleSubmit(event) {
event.preventDefault();
if (!formData.email || !formData.message) {
alert('모든 필드를 입력해주세요.');
return;
}
isSubmitting = true;
submitStatus = '';
try {
// API 호출 시뮬레이션 (2초 대기)
await new Promise(resolve =>
setTimeout(resolve, 2000)
);
submitStatus = 'success';
// 성공 시 폼 리셋 (3초 후)
setTimeout(() => {
formData.email = '';
formData.message = '';
submitStatus = '';
}, 3000);
} catch (error) {
submitStatus = 'error';
} finally {
isSubmitting = false;
}
}
</script>
<form onsubmit="{handleSubmit}" class="contact-form">
<h2>문의하기</h2>
{#if submitStatus === 'success'}
<div class="success">메시지가 전송되었습니다!</div>
{:else if submitStatus === 'error'}
<div class="error">
전송에 실패했습니다. 다시 시도해주세요.
</div>
{/if}
<div class="field">
<label>이메일:</label>
<input
type="email"
bind:value="{formData.email}"
disabled="{isSubmitting}"
required
/>
</div>
<div class="field">
<label>메시지:</label>
<textarea
bind:value="{formData.message}"
rows="4"
disabled="{isSubmitting}"
required
></textarea>
</div>
<button type="submit" disabled="{isSubmitting}">
{isSubmitting ? '전송 중...' : '전송하기'}
</button>
</form>
<style>
.contact-form {
max-width: 500px;
padding: 2rem;
}
.success {
background: #f0fdf4;
color: #166534;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
}
input,
textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 4px;
}
input:disabled,
textarea:disabled {
background: #f9fafb;
}
button {
background: #3b82f6;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
}
button:disabled {
background: #9ca3af;
}
</style>
실습해보기: Svelte REPL에서 이 폼 처리 예제들을 직접 실험해보세요!
정리
Svelte 5의 이벤트 처리 시스템을 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:
핵심 요약
- DOM 이벤트:
on:
지시문에서 표준 DOM 속성으로 전환하여 더 직관적이고 타입 안전한 이벤트 처리 - 커스텀 이벤트:
createEventDispatcher
에서 콜백 props로 변경하여 명시적이고 타입 안전한 컴포넌트 통신 - 폼 처리:
bind:
지시문을 통한 양방향 데이터 바인딩과 실시간 검증으로 사용자 친화적인 폼 구현
실무 활용 팁
- 이벤트 수식어 대신 핸들러 내부에서
preventDefault()
,stopPropagation()
직접 사용 - 콜백 props를 활용하여 컴포넌트 간 명확하고 타입 안전한 통신 구현
- 반응형 상태와 계산된 값을 활용한 실시간 폼 검증으로 UX 향상
다음 단계: 6장 "조건부 렌더링과 리스트"에서는 Svelte의 템플릿 제어 구조를 알아보겠습니다. {#if}
, {#each}
, {#await}
블록을 통한 동적 UI 렌더링을 마스터해보세요!