14. 폼과 액션
SvelteKit은 폼 처리에 있어 혁신적인 접근 방식을 제공합니다.
기존 SPA 프레임워크들이 폼 제출 시 JavaScript에 의존하는 것과 달리, SvelteKit은 HTML 표준 폼을 기반으로 하여 JavaScript가 없어도 작동하는 점진적 개선을 제공합니다.
Form Actions와 use:enhance
를 통해 서버 사이드 처리와 클라이언트 사이드 개선이 완벽하게 통합되어, 견고하면서도 사용자 친화적인 폼 시스템을 구축할 수 있습니다.
14.1 Form 기본
전통적인 폼 처리
SvelteKit의 폼 시스템은 웹의 기본 원칙을 존중합니다.
일반적인 HTML <form>
요소를 사용하여 서버로 데이터를 전송하며, JavaScript가 비활성화되어도 완전히 작동합니다.
이러한 접근 방식은 웹 접근성과 견고성을 크게 향상시키며, 네트워크 문제나 JavaScript 오류 상황에서도 애플리케이션이 계속 작동할 수 있도록 보장합니다.
기본 폼 구조
<script>
let { data } = $props();
</script>
<form method="POST">
<div>
<label for="name">이름:</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">이메일:</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="message">메시지:</label>
<textarea
id="message"
name="message"
required
></textarea>
</div>
<button type="submit">전송</button>
</form>
{#if data.success}
<p>메시지가 전송되었습니다!</p>
{/if}
<style>
form {
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
input,
textarea {
padding: 0.5rem;
border: 1px solid #ddd;
}
button {
padding: 0.75rem;
background: #3b82f6;
color: white;
border: none;
}
</style>
// +page.server.js
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
console.log('받은 데이터:', { name, email, message });
return { success: true };
},
};
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
Progressive Enhancement
Progressive Enhancement는 기본 HTML 기능 위에 JavaScript를 점진적으로 추가하는 접근 방식입니다.
폼이 먼저 HTML 표준으로 작동하도록 구현하고, JavaScript가 사용 가능할 때 사용자 경험을 개선합니다.
SvelteKit의 use:enhance
는 이러한 철학을 완벽하게 구현하여, 전체 페이지 새로고침 없이 폼을 제출하고 결과를 처리할 수 있습니다.
use:enhance
적용
단계 | JavaScript 없음 | JavaScript 있음 |
---|---|---|
폼 제출 | 전체 페이지 새로고침 | AJAX 요청 |
응답 처리 | 서버 렌더링 | 클라이언트 업데이트 |
로딩 상태 | 브라우저 기본 | 커스텀 UI |
에러 처리 | 서버 리다이렉트 | 인라인 메시지 |
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let isSubmitting = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
isSubmitting = true;
return async ({ update }) => {
await update();
isSubmitting = false;
};
}}
>
<div>
<label for="name">이름:</label>
<input
id="name"
name="name"
type="text"
disabled={isSubmitting}
value={form?.name ?? ''}
required
/>
</div>
<div>
<label for="email">이메일:</label>
<input
id="email"
name="email"
type="email"
disabled={isSubmitting}
value={form?.email ?? ''}
required
/>
{#if form?.errors?.email}
<span class="error">{form.errors.email}</span>
{/if}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '전송 중...' : '전송'}
</button>
</form>
{#if form?.success}
<div class="success">메시지가 전송되었습니다!</div>
{/if}
<style>
.error { color: #dc2626; font-size: 0.875rem; }
.success { background: #f0fdf4; color: #166534; padding: 1rem; }
input:disabled { opacity: 0.6; }
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
14.2 Form Actions
+page.server.js의 액션
Form Actions는 SvelteKit의 핵심 기능으로, +page.server.js
파일에서 폼 제출을 처리하는 서버 사이드 함수입니다.
각 액션은 비동기 함수로 정의되며, 요청 객체를 통해 폼 데이터에 접근하고 응답을 반환합니다.
액션은 기본 액션(default)과 명명된 액션(named actions)으로 구분되며, 하나의 페이지에서 여러 종류의 폼 처리를 담당할 수 있습니다.
기본 액션과 명명된 액션
// +page.server.js
import { fail } from '@sveltejs/kit';
export async function load() {
return {
posts: [
{ id: 1, title: '첫 번째 게시글' },
{ id: 2, title: '두 번째 게시글' },
],
};
}
export const actions = {
// 기본 액션 - action 속성 없이 폼 제출
default: async ({ request }) => {
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
if (!title || !content) {
return fail(400, { missing: true, title, content });
}
console.log('새 게시글:', { title, content });
return { success: true };
},
// 명명된 액션 - action="?/update"로 호출
update: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id');
const title = formData.get('title');
if (!id || !title) {
return fail(400, {
error: '필수 필드가 누락되었습니다.',
});
}
console.log('게시글 수정:', { id, title });
return { updated: true };
},
delete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id');
console.log('게시글 삭제:', id);
return { deleted: true };
},
};
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
여러 액션 처리
하나의 페이지에서 여러 종류의 폼을 처리해야 할 때 명명된 액션을 사용합니다.
각 액션은 고유한 이름을 가지며, 폼의 action
속성에 ?/actionName
형식으로 지정하여 호출합니다.
이를 통해 로그인/회원가입, CRUD 작업, 설정 변경 등 다양한 기능을 하나의 페이지에서 체계적으로 관리할 수 있습니다.
로그인/회원가입 예제
// +page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
login: async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return fail(400, { email, missing: true });
}
if (
email === 'admin@example.com' &&
password === 'password'
) {
cookies.set('sessionid', 'fake-session', {
path: '/',
maxAge: 60 * 60 * 24 * 7,
});
throw redirect(303, '/dashboard');
}
return fail(400, { email, incorrect: true });
},
register: async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return fail(400, { email, missing: true });
}
if (email === 'admin@example.com') {
return fail(400, { email, emailExists: true });
}
console.log('새 사용자 생성:', { email });
return {
success: true,
message: '회원가입이 완료되었습니다.',
};
},
};
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
검증과 에러 처리
Form Actions에서 데이터 검증과 에러 처리는 fail()
함수를 통해 수행됩니다.
검증 실패 시 적절한 HTTP 상태 코드와 함께 오류 정보를 반환하며, 클라이언트에서는 form
prop을 통해 이를 받아 처리합니다.
이러한 방식으로 서버 사이드 검증과 클라이언트 사이드 사용자 경험을 완벽하게 통합할 수 있습니다.
검증 시스템
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let currentTab = $state('login');
</script>
<div class="auth-container">
<div class="tabs">
<button
class:active={currentTab === 'login'}
onclick={() => currentTab = 'login'}
>
로그인
</button>
<button
class:active={currentTab === 'register'}
onclick={() => currentTab = 'register'}
>
회원가입
</button>
</div>
{#if currentTab === 'login'}
<form method="POST" action="?/login" use:enhance>
<h2>로그인</h2>
{#if form?.missing}
<p class="error">이메일과 비밀번호를 입력해주세요.</p>
{/if}
{#if form?.incorrect}
<p class="error">이메일 또는 비밀번호가 올바르지 않습니다.</p>
{/if}
<input
name="email"
type="email"
placeholder="이메일"
value={form?.email ?? ''}
required
/>
<input name="password" type="password" placeholder="비밀번호" required />
<button type="submit">로그인</button>
</form>
{/if}
{#if currentTab === 'register'}
<form method="POST" action="?/register" use:enhance>
<h2>회원가입</h2>
{#if form?.emailExists}
<p class="error">이미 가입된 이메일입니다.</p>
{/if}
<input name="email" type="email" placeholder="이메일" required />
<input name="password" type="password" placeholder="비밀번호" required />
<button type="submit">회원가입</button>
</form>
{/if}
{#if form?.success}
<div class="success">{form.message}</div>
{/if}
</div>
<style>
.auth-container { max-width: 400px; margin: 0 auto; padding: 2rem; }
.tabs { display: flex; margin-bottom: 2rem; }
.tabs button { padding: 1rem; background: none; border: none; }
.tabs button.active { color: #3b82f6; border-bottom: 2px solid #3b82f6; }
form { display: flex; flex-direction: column; gap: 1rem; }
.error { color: #dc2626; padding: 0.5rem; background: #fef2f2; }
.success { color: #166534; padding: 1rem; background: #f0fdf4; }
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
14.3 고급 폼 처리
use:enhance
use:enhance
는 SvelteKit의 강력한 기능으로, 기본 HTML 폼 동작을 유지하면서 JavaScript가 사용 가능할 때 사용자 경험을 크게 개선합니다.
전체 페이지 새로고침을 방지하고, 로딩 상태 표시, 낙관적 업데이트, 커스텀 응답 처리 등 다양한 기능을 제공합니다.
콜백 함수를 통해 폼 제출 전후의 동작을 완전히 제어할 수 있습니다.
낙관적 업데이트
<script>
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
let { data } = $props();
let optimisticTodos = $state([...data.todos]);
$effect(() => {
optimisticTodos = [...data.todos];
});
</script>
<div class="todo-app">
<form
method="POST"
action="?/create"
use:enhance="{({"
formData
})=""
>
{ const description = formData.get('description'); //
낙관적 업데이트 const tempId = Date.now();
optimisticTodos = [...optimisticTodos, { id: tempId,
description: description.toString(), temp: true }];
return async ({ result }) => { if (result.type ===
'success') { await invalidateAll(); optimisticTodos =
data.todos;
document.querySelector('input[name="description"]').value
= ''; } else { // 실패 시 롤백 optimisticTodos =
optimisticTodos.filter(t => t.id !== tempId); } }; }} >
<input
name="description"
placeholder="할 일 입력"
required
/>
<button type="submit">추가</button>
</form>
<ul>
{#each optimisticTodos as todo (todo.id)}
<li class:temp="{todo.temp}">
<span>{todo.description}</span>
<form
method="POST"
action="?/delete"
use:enhance="{()"
=""
>
{ // 즉시 UI에서 제거 optimisticTodos =
optimisticTodos.filter(t => t.id !== todo.id);
return async ({ result }) => { if (result.type !==
'success') { optimisticTodos = [...data.todos]; } };
}} >
<input type="hidden" name="id" value="{todo.id}" />
<button type="submit">삭제</button>
</form>
</li>
{/each}
</ul>
</div>
<style>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 2rem;
}
form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
padding: 0.5rem;
border: 1px solid #ddd;
margin-bottom: 0.5rem;
}
li.temp {
opacity: 0.6;
background: #f9f9f9;
}
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
파일 업로드
파일 업로드는 웹 애플리케이션에서 자주 사용되는 기능으로, SvelteKit에서는 multipart/form-data
와 Form Actions를 통해 간단하게 처리할 수 있습니다.
업로드 진행률 표시, 파일 유형 검증, 크기 제한 등 실무에서 필요한 기능들을 포함한 완전한 파일 업로드 시스템을 구현할 수 있습니다.
간단한 파일 업로드
// +page.server.js
import { fail } from '@sveltejs/kit';
import { writeFileSync } from 'fs';
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = [
'image/jpeg',
'image/png',
'image/gif',
];
export const actions = {
upload: async ({ request }) => {
const formData = await request.formData();
const file = formData.get('file');
if (
!file ||
!(file instanceof File) ||
file.size === 0
) {
return fail(400, { error: '파일을 선택해주세요.' });
}
if (file.size > MAX_SIZE) {
return fail(400, {
error: '파일 크기가 너무 큽니다 (최대 5MB).',
});
}
if (!ALLOWED_TYPES.includes(file.type)) {
return fail(400, {
error: '지원하지 않는 파일 형식입니다.',
});
}
try {
const filename = `${Date.now()}_${file.name}`;
const buffer = Buffer.from(await file.arrayBuffer());
writeFileSync(`./static/uploads/${filename}`, buffer);
return {
success: true,
filename,
message: '파일이 업로드되었습니다.',
};
} catch (error) {
return fail(500, {
error: '업로드 중 오류가 발생했습니다.',
});
}
},
};
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let selectedFile = $state(null);
let isUploading = $state(false);
function handleFileSelect(event) {
selectedFile = event.target.files?.[0] || null;
}
</script>
<div class="upload-container">
<form
method="POST"
action="?/upload"
enctype="multipart/form-data"
use:enhance="{()"
=""
>
{ isUploading = true; return async ({ update }) => {
await update(); isUploading = false; selectedFile =
null; }; }} >
<div class="file-input">
<input
name="file"
type="file"
accept="image/*"
onchange="{handleFileSelect}"
disabled="{isUploading}"
required
/>
{#if selectedFile}
<p>선택된 파일: {selectedFile.name}</p>
{/if}
</div>
<button
type="submit"
disabled="{isUploading"
||
!selectedFile}
>
{isUploading ? '업로드 중...' : '업로드'}
</button>
</form>
{#if form?.success}
<div class="success">
<p>{form.message}</p>
<img
src="/uploads/{form.filename}"
alt="업로드된 이미지"
/>
</div>
{/if} {#if form?.error}
<div class="error">{form.error}</div>
{/if}
</div>
<style>
.upload-container {
max-width: 500px;
margin: 0 auto;
padding: 2rem;
}
.file-input {
margin-bottom: 1rem;
}
.success {
background: #f0fdf4;
color: #166534;
padding: 1rem;
}
.success img {
max-width: 100%;
height: auto;
margin-top: 1rem;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 1rem;
}
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
실시간 검증
실시간 검증은 사용자가 입력하는 동안 즉시 피드백을 제공하여 사용자 경험을 크게 개선합니다. 클라이언트 사이드 검증으로 빠른 응답을 제공하면서도, 서버 사이드 검증으로 보안과 데이터 무결성을 보장하는 하이브리드 접근 방식을 사용합니다. 디바운싱과 함께 서버 검증을 수행하여 성능과 사용자 경험 사이의 균형을 맞춥니다.
실시간 이메일 검증
// +page.server.js
import { json, fail } from '@sveltejs/kit';
export async function POST({ request }) {
const { email } = await request.json();
await new Promise(resolve => setTimeout(resolve, 300)); // 네트워크 시뮬레이션
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return json({
valid: false,
message: '유효한 이메일 형식이 아닙니다.',
});
}
if (email === 'admin@example.com') {
return json({
valid: false,
message: '이미 가입된 이메일입니다.',
});
}
return json({
valid: true,
message: '사용 가능한 이메일입니다.',
});
}
export const actions = {
register: async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return fail(400, {
error: '모든 필드를 입력해주세요.',
});
}
return {
success: true,
message: '회원가입이 완료되었습니다!',
};
},
};
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let email = $state('');
let emailValidation = $state({
valid: null,
message: '',
checking: false,
});
let validationTimer = null;
async function validateEmail(emailValue) {
if (!emailValue) {
emailValidation = {
valid: null,
message: '',
checking: false,
};
return;
}
emailValidation.checking = true;
try {
const response = await fetch('', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailValue }),
});
const result = await response.json();
emailValidation = { ...result, checking: false };
} catch (error) {
emailValidation = {
valid: false,
message: '검증 중 오류가 발생했습니다.',
checking: false,
};
}
}
function handleEmailInput(value) {
email = value;
if (validationTimer) {
clearTimeout(validationTimer);
}
emailValidation = {
valid: null,
message: '',
checking: false,
};
validationTimer = setTimeout(() => {
validateEmail(value);
}, 500);
}
</script>
<div class="register-form">
<form method="POST" action="?/register" use:enhance>
<h2>회원가입</h2>
<div class="field">
<label for="email">이메일:</label>
<input
id="email"
name="email"
type="email"
value="{email}"
class:valid="{emailValidation.valid"
=""
=""
="true}"
class:invalid="{emailValidation.valid"
=""
=""
="false}"
oninput="{e"
=""
/>
handleEmailInput(e.target.value)} required />
<div class="validation">
{#if emailValidation.checking}
<span class="checking">확인 중...</span>
{:else if emailValidation.message}
<span
class:valid="{emailValidation.valid}"
class:invalid="{!emailValidation.valid}"
>
{emailValidation.message}
</span>
{/if}
</div>
</div>
<div class="field">
<label for="password">비밀번호:</label>
<input
id="password"
name="password"
type="password"
required
/>
</div>
<button
type="submit"
disabled="{emailValidation.valid"
!=""
="true}"
>
회원가입
</button>
</form>
{#if form?.success}
<div class="success">{form.message}</div>
{/if}
</div>
<style>
.register-form {
max-width: 400px;
margin: 0 auto;
padding: 2rem;
}
.field {
margin-bottom: 1.5rem;
}
input {
width: 100%;
padding: 0.5rem;
border: 2px solid #ddd;
}
input.valid {
border-color: #10b981;
}
input.invalid {
border-color: #ef4444;
}
.validation {
min-height: 1.5rem;
margin-top: 0.25rem;
font-size: 0.875rem;
}
.validation .valid {
color: #10b981;
}
.validation .invalid {
color: #ef4444;
}
.validation .checking {
color: #f59e0b;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.success {
background: #f0fdf4;
color: #166534;
padding: 1rem;
margin-top: 1rem;
}
</style>
실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!
정리
SvelteKit의 폼과 액션 시스템을 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:
핵심 요약
- Form 기본: HTML 표준 폼을 기반으로 한 점진적 개선으로 견고하고 접근 가능한 폼 시스템 구축
- Form Actions:
+page.server.js
에서 서버 사이드 폼 처리를 담당하는 강력한 액션 시스템 - 고급 처리:
use:enhance
, 파일 업로드, 실시간 검증을 통한 현대적 사용자 경험 제공
실무 활용 팁
- 항상 JavaScript 없이도 작동하도록 구현하고
use:enhance
로 개선 - 서버 사이드 검증과 클라이언트 사이드 사용자 경험을 적절히 조합
- 낙관적 업데이트와 에러 처리로 반응성 있는 UI 구현
다음 단계: 15장 "API 라우트"에서는 SvelteKit의 서버 사이드 API 구축 방법을 알아보겠습니다. REST API 설계부터 데이터베이스 연동, 인증과 보안까지 백엔드 개발의 모든 것을 마스터해보세요!