006: 폼 처리 및 바인딩
기본 폼 바인딩
Svelte는 폼 요소와의 양방향 바인딩을 쉽게 구현할 수 있습니다. 다양한 입력 요소들과 함께 기본적인 폼 바인딩을 살펴보겠습니다.
컴포넌트 코드:
<script lang="ts">
let formData = $state({
username: '',
email: '',
age: 0,
newsletter: false,
preference: 'light',
interests: []
});
function handleSubmit() {
alert('폼 데이터: ' + JSON.stringify(formData, null, 2));
}
let ids = $state({
username: crypto.randomUUID(),
email: crypto.randomUUID(),
age: crypto.randomUUID(),
preference: crypto.randomUUID(),
newsletter: crypto.randomUUID()
});
</script>
<div class="p-4 border border-orange-500 rounded">
<form onsubmit={e => { e.preventDefault(); handleSubmit(); }}>
<div class="space-y-4">
<div>
<label for={ids.username} class="block text-gray-200 mb-2">사용자명:</label>
<input
id={ids.username}
type="text"
bind:value={formData.username}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
/>
</div>
<div>
<label for={ids.email} class="block text-gray-200 mb-2">이메일:</label>
<input
id={ids.email}
type="email"
bind:value={formData.email}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
/>
</div>
<div>
<label for={ids.age} class="block text-gray-200 mb-2">나이:</label>
<input
id={ids.age}
type="number"
bind:value={formData.age}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
/>
</div>
<div>
<label for={ids.newsletter} class="flex items-center space-x-2 text-gray-200">
<input
id={ids.newsletter}
type="checkbox"
bind:checked={formData.newsletter}
class="form-checkbox text-orange-600"
/>
<span>뉴스레터 구독</span>
</label>
</div>
<div>
<label for={ids.preference} class="block text-gray-200 mb-2">테마 선호도:</label>
<select
id={ids.preference}
bind:value={formData.preference}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
>
<option value="light">라이트 모드</option>
<option value="dark">다크 모드</option>
<option value="system">시스템 설정</option>
</select>
</div>
<div>
<p class="block text-gray-200 mb-2">관심사:</p>
<div class="space-y-2">
{#each ['프론트엔드', '백엔드', '모바일', 'DevOps'] as interest}
<label class="flex items-center space-x-2 text-gray-200">
<input
type="checkbox"
bind:group={formData.interests}
value={interest}
class="form-checkbox text-orange-600"
/>
<span>{interest}</span>
</label>
{/each}
</div>
</div>
</div>
<button
type="submit"
class="mt-6 bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
제출
</button>
</form>
</div>폼 유효성 검사
사용자 입력의 유효성을 검사하고 적절한 피드백을 제공하는 것은 중요합니다. Svelte에서 폼 유효성 검사를 구현하는 방법을 알아보겠습니다.
컴포넌트 코드:
<script lang="ts">
let formData = $state({
username: '',
email: '',
password: '',
confirmPassword: ''
});
let errors = $state({
username: '',
email: '',
password: '',
confirmPassword: ''
});
let touched = $state({
username: false,
email: false,
password: false,
confirmPassword: false
});
let ids = $state({
username: crypto.randomUUID(),
email: crypto.randomUUID(),
password: crypto.randomUUID(),
confirmPassword: crypto.randomUUID()
});
function validateUsername() {
if (!formData.username) {
errors.username = '사용자명을 입력해주세요.';
} else if (formData.username.length < 3) {
errors.username = '사용자명은 3자 이상이어야 합니다.';
} else {
errors.username = '';
}
}
function validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
errors.email = '이메일을 입력해주세요.';
} else if (!emailRegex.test(formData.email)) {
errors.email = '올바른 이메일 형식이 아닙니다.';
} else {
errors.email = '';
}
}
function validatePassword() {
if (!formData.password) {
errors.password = '비밀번호를 입력해주세요.';
} else if (formData.password.length < 8) {
errors.password = '비밀번호는 8자 이상이어야 합니다.';
} else {
errors.password = '';
}
}
function validateConfirmPassword() {
if (!formData.confirmPassword) {
errors.confirmPassword = '비밀번호를 다시 입력해주세요.';
} else if (formData.confirmPassword !== formData.password) {
errors.confirmPassword = '비밀번호가 일치하지 않습니다.';
} else {
errors.confirmPassword = '';
}
}
function handleSubmit(e: Event) {
e.preventDefault();
touched.username = true;
touched.email = true;
touched.password = true;
touched.confirmPassword = true;
validateUsername();
validateEmail();
validatePassword();
validateConfirmPassword();
if (!errors.username && !errors.email && !errors.password && !errors.confirmPassword) {
alert('폼 검증 성공!');
}
}
</script>
<div class="p-4 border border-orange-500 rounded">
<form onsubmit={handleSubmit}>
<div class="space-y-4">
<div>
<label for={ids.username} class="block text-gray-200 mb-2">사용자명:</label>
<input
id={ids.username}
type="text"
bind:value={formData.username}
onblur={() => { touched.username = true; validateUsername(); }}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600 focus:border-orange-500"
/>
{#if touched.username && errors.username}
<p class="text-red-500 text-sm mt-1">{errors.username}</p>
{/if}
</div>
<div>
<label for={ids.email} class="block text-gray-200 mb-2">이메일:</label>
<input
id={ids.email}
type="email"
bind:value={formData.email}
onblur={() => { touched.email = true; validateEmail(); }}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600 focus:border-orange-500"
/>
{#if touched.email && errors.email}
<p class="text-red-500 text-sm mt-1">{errors.email}</p>
{/if}
</div>
<div>
<label for={ids.password} class="block text-gray-200 mb-2">비밀번호:</label>
<input
id={ids.password}
type="password"
bind:value={formData.password}
onblur={() => { touched.password = true; validatePassword(); }}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600 focus:border-orange-500"
/>
{#if touched.password && errors.password}
<p class="text-red-500 text-sm mt-1">{errors.password}</p>
{/if}
</div>
<div>
<label for={ids.confirmPassword} class="block text-gray-200 mb-2">비밀번호 확인:</label>
<input
id={ids.confirmPassword}
type="password"
bind:value={formData.confirmPassword}
onblur={() => { touched.confirmPassword = true; validateConfirmPassword(); }}
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600 focus:border-orange-500"
/>
{#if touched.confirmPassword && errors.confirmPassword}
<p class="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
{/if}
</div>
</div>
<button
type="submit"
class="mt-6 bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
가입하기
</button>
</form>
</div>커스텀 폼 컴포넌트
재사용 가능한 폼 컴포넌트를 만들어 일관된 사용자 경험을 제공하고 코드 중복을 줄일 수 있습니다.
컴포넌트 코드:
<script lang="ts">
import FormInput from './form/FormInput.svelte';
import FormSelect from './form/FormSelect.svelte';
import FormTextarea from './form/FormTextarea.svelte';
let formData = $state({
title: '',
category: 'general',
description: '',
priority: 'medium'
});
const categories = [
{ value: 'general', label: '일반' },
{ value: 'bug', label: '버그' },
{ value: 'feature', label: '기능 요청' },
{ value: 'enhancement', label: '개선 사항' }
];
const priorities = [
{ value: 'low', label: '낮음' },
{ value: 'medium', label: '중간' },
{ value: 'high', label: '높음' }
];
function handleSubmit() {
alert('제출된 데이터:\n' + JSON.stringify(formData, null, 2));
}
</script>
<div class="p-4 border border-orange-500 rounded">
<form onsubmit={handleSubmit}>
<div class="space-y-4">
<FormInput
label="제목"
type="text"
bind:value={formData.title}
required
/>
<FormSelect
label="카테고리"
options={categories}
bind:value={formData.category}
/>
<FormTextarea
label="설명"
bind:value={formData.description}
rows={4}
/>
<FormSelect
label="우선순위"
options={priorities}
bind:value={formData.priority}
/>
</div>
<button
type="submit"
class="mt-6 bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
이슈 생성
</button>
</form>
</div>파일 업로드 처리
파일 업로드는 웹 애플리케이션의 중요한 기능입니다. Svelte에서 파일 업로드를 처리하는 방법을 살펴보겠습니다.
컴포넌트 코드:
<script lang="ts">
let files = $state<FileList | null>(null);
let previewUrl = $state('');
let uploadStatus = $state('');
let progress = $state(0);
let id = $state(crypto.randomUUID());
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
files = input.files;
const file = files[0];
// 이미지 미리보기 생성
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
previewUrl = e.target?.result as string;
};
reader.readAsDataURL(file);
} else {
previewUrl = '';
}
}
}
function simulateUpload() {
if (!files) return;
const file = files[0];
uploadStatus = '업로드 중...';
progress = 0;
// 업로드 시뮬레이션
const interval = setInterval(() => {
progress += 10;
if (progress >= 100) {
clearInterval(interval);
uploadStatus = '업로드 완료!';
setTimeout(() => {
uploadStatus = '';
progress = 0;
files = null;
previewUrl = '';
}, 2000);
}
}, 500);
}
</script>
<div class="p-4 border border-orange-500 rounded">
<div class="space-y-4">
<div>
<label for={id} class="block text-gray-200 mb-2">파일 선택:</label>
<input
{id}
type="file"
accept="image/*"
onchange={handleFileSelect}
class="block w-full text-gray-200
file:mr-4 file:py-2 file:px-4
file:rounded file:border-0
file:text-sm file:font-semibold
file:bg-orange-600 file:text-white
hover:file:bg-orange-500"
/>
</div>
{#if previewUrl}
<div>
<p class="text-gray-200 mb-2">미리보기:</p>
<img src={previewUrl} alt="Preview" class="max-w-xs rounded" />
</div>
{/if}
{#if files}
<div>
<button
onclick={simulateUpload}
class="bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
업로드
</button>
</div>
{/if}
{#if uploadStatus}
<div>
<p class="text-gray-200">{uploadStatus}</p>
<div class="w-full bg-zinc-700 rounded h-2 mt-2">
<div
class="bg-orange-600 h-2 rounded"
style="width: {progress}%"
></div>
</div>
</div>
{/if}
</div>
</div>동적 폼 필드
사용자의 입력에 따라 동적으로 폼 필드를 생성하고 관리하는 방법을 학습합니다.
컴포넌트 코드:
<script lang="ts">
interface Question {
id: number;
type: 'text' | 'radio' | 'checkbox';
question: string;
options?: string[];
answer: string | string[];
}
let questions = $state<Question[]>([
{
id: 1,
type: 'text',
question: '이름을 입력해주세요.',
answer: ''
}
]);
let nextId = $state(2);
function addQuestion(type: Question['type']) {
const newQuestion: Question = {
id: nextId,
type,
question: '',
answer: type === 'checkbox' ? [] : '',
options: type !== 'text' ? ['옵션 1'] : undefined
};
questions = [...questions, newQuestion];
nextId++;
}
function removeQuestion(id: number) {
questions = questions.filter(q => q.id !== id);
}
function addOption(questionId: number) {
questions = questions.map(q => {
if (q.id === questionId && q.options) {
return {
...q,
options: [...q.options, `옵션 ${q.options.length + 1}`]
};
}
return q;
});
}
function removeOption(questionId: number, optionIndex: number) {
questions = questions.map(q => {
if (q.id === questionId && q.options) {
return {
...q,
options: q.options.filter((_, i) => i !== optionIndex)
};
}
return q;
});
}
function handleSubmit() {
alert('제출된 답변:\n' + JSON.stringify(questions, null, 2));
}
</script>
<div class="p-4 border border-orange-500 rounded">
<form onsubmit={e => { e.preventDefault(); handleSubmit(); }}>
<div class="space-y-6">
{#each questions as question (question.id)}
<div class="p-4 border border-zinc-600 rounded">
<div class="flex justify-between items-start mb-4">
<input
type="text"
bind:value={question.question}
placeholder="질문을 입력하세요"
class="flex-1 p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600 mr-2"
/>
<button
type="button"
onclick={() => removeQuestion(question.id)}
class="text-red-500 hover:text-red-400"
>
삭제
</button>
</div>
{#if question.type === 'text'}
<input
type="text"
bind:value={question.answer}
placeholder="답변을 입력하세요"
class="w-full p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
/>
{:else}
<div class="space-y-2">
{#each question.options || [] as option, i}
<div class="flex items-center space-x-2">
{#if question.type === 'radio'}
<input
type="radio"
bind:group={question.answer}
value={option}
class="text-orange-600"
/>
{:else}
<input
type="checkbox"
bind:group={question.answer}
value={option}
class="text-orange-600"
/>
{/if}
<input
type="text"
bind:value={question.options![i]}
class="flex-1 p-2 rounded bg-zinc-700 text-gray-200 border border-zinc-600"
/>
<button
type="button"
onclick={() => removeOption(question.id, i)}
class="text-red-500 hover:text-red-400"
>
삭제
</button>
</div>
{/each}
<button
type="button"
onclick={() => addOption(question.id)}
class="text-orange-600 hover:text-orange-500"
>
+ 옵션 추가
</button>
</div>
{/if}
</div>
{/each}
</div>
<div class="mt-4 space-x-2">
<button
type="button"
onclick={() => addQuestion('text')}
class="bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
+ 텍스트 질문
</button>
<button
type="button"
onclick={() => addQuestion('radio')}
class="bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
+ 단일 선택
</button>
<button
type="button"
onclick={() => addQuestion('checkbox')}
class="bg-orange-600 text-white py-2 px-4 rounded hover:bg-orange-500"
>
+ 다중 선택
</button>
</div>
<button
type="submit"
class="mt-6 bg-green-600 text-white py-2 px-4 rounded hover:bg-green-500"
>
제출
</button>
</form>
</div>실습 과제
고급 설문조사 폼 만들기
지금까지 배운 폼 처리 개념을 활용하여 다음 기능이 있는 설문조사 폼을 구현해보세요:
- 다양한 타입의 입력 필드 (텍스트, 체크박스, 라디오 등)
- 동적으로 추가/제거 가능한 질문
- 실시간 유효성 검사
- 진행 상태 표시
- 데이터 저장 및 불러오기
다음 강의 미리보기
7강: 애니메이션 및 트랜지션
다음 강의에서는 Svelte의 애니메이션과 트랜지션 기능을 자세히 살펴봅니다:
- 컴포넌트 라이프사이클
- 트랜지션 및 애니메이션
- 모션 라이브러리 활용
- 인터랙티브 UI 구현
- CSS 애니메이션과 Svelte 통합
- 커스텀 트랜지션 함수 작성
- 애니메이션 성능 최적화