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 통합
- 커스텀 트랜지션 함수 작성
- 애니메이션 성능 최적화