폼 액션
- sveltekit의 form action은 서버의 데이터를 생성, 수정, 삭제 하기 위한 핵심적인 기능이다. 전통적인 HTML
<form>의 강력함과 sveltekit의 최신 기술을 결합하여, Javascript 코드가 없어도 동작하고 코드 구성이 깔끔하며, 데이터 처리가 매우 편리한 패턴을 제공한다.
- Action은 항상
+page.server.ts 파일 안에 정의 된다.
기본 동작
- 데이터는
[ UI ] -> [ 서버 액션 (actions) ] -> [ 서버 응답 (load) ] -> [ UI 피드백 ] 흐름으로 처리된다.
<form method="post" action="?/login">
<div>login()</div>
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button>Login</button>
<button formaction="?/register">Register</button>
</form>
export const actions = {
login: async () => {
},
register: async () => {
}
}
Action 핸들러
Action의 핸들러 함수는 파라미터로 RequestEvent라는 객체를 받는다. 이 객체에는 요청에 대한 모든 정보가 담겨 있다.
request : 틀어온 요청 자체 (메서드, 헤더, 본문 등)
cookies : 브라우저의 쿠키를 읽고 설정하는 헬퍼
locals : hooks.server.ts에서 설정한 서버 전용 데이터 (인증된 사용자 정보 등)
params : 동적 경로의 파라미터
- 사용자가
<form>에 입력하여 POST 방식으로 보낸 데이터는 request 객체의 formData() 메서드를 통해 읽을 수 있다.
export const actions: Actions = {
login: async ({request, cookies, locals}) => {
const data = await request.formData();
const email = data.get('email')
const password = data.get('password')
}
}
Action의 핸들러에서는 데이터베이스를 업데이트 하거나, 외부 API 의 호출, 이메일 전송, 쿠키 설정 하는 등의 실제 서버 작업을 수행한다.
- 작업이 끝나면, 액션은 클라이언트에게 전달할 데이터를 return 할 수 있으며, 이 데이터는 보통 작업의 성공 여부, 유효성 검사 실패 메시지 등을 담는다.
- fail 헬퍼를 사용해 데이터를 반환하는 것이 일반적이다.,
export const actions: Actions = {
login: async ({request, cookies, locals}) => {
if(!email) {
return fail(400, {error: 'Email is required'})
}
cookies.set('session', 'user-session_id', { path: '/' })
return { success: true, message: 'Logged in successfully' }
}
}
Action의 핸들러에서 반환된 데이터는 해당 페이지 컴포넌트의 export let form; 이라는 prop 값으로 전달 된다.
- 이
prop을 사용해 UI를 동적으로 업데이트 할 수 있다.
$page.form 전역 스토어는 form prop과 동일한 데이터를 담고 있다.
- (1), (2), (3) 모두 form data를 가지고 있다.
<script lang="ts">
import type { PageProps } from "./$types";
const { form } = $props()
import type { ActionData } from './$types';
export let form: ActionData
import { page } from `$app/state`
const { form } = page
</script>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
Vaildation Errors
fail함수는 사용자 경험을 해치지 않으면서 서버 측 유효성 검사를 구현하는 도구이다. 쉽게 설명하여 "사용자가 잘못 입력했더라도, 입력했던 내용을 지우지 않고 그대로 보여주면서 '이 부분이 틀렸다.'고 알려 줄수 있다."
fail함수는 폼 데이터가 유효하지 않을 때, 처리를 중단하고 클라이언트에게 오류 정보를 돌려주기 위해서 사용한다.
fail(statusCode, dataObject)
statusCode : HTTP 오류 상태 코드
dataObject : 클라이언트에게 다시 보낼 데이터, 오류 메시지와 클라이언트가 입력했던 값을 포함 시키는 것이 일반적이다.
- 반환된 데이터는 JSON으로 직렬화 할 수 있어야 한다.
fail함수로 응답을 반환하면, sveltekit은 load 함수를 다시 실행하지 않는다. 이는 불필요한 데이터 로딩을 막아 효율적이며, 사용자가 입력한 폼의 상태를 그대로 유지시켜 준다.
import { fail } from '@sveltejs/kit';
import * as db from '$lib/server/db';
import type { Actions } from './$types';
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email) {
return fail(400, { email, missing: true });
}
const user = await db.getUser(email);
if (!user || user.password !== db.hash(password)) {
return fail(400, { email, incorrect: true });
}
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true };
},
register: async (event) => {
}
} satisfies Actions;
<form method="POST" action="?/login">
{#if form?.missing}<p class="error">The email field is required</p>{/if}
{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
<label>
Email
<input name="email" type="email" value={form?.email ?? ''}>
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>
redirection
redirection은 fail과 동일하게 작동한다.
redirect(303, '/')
Loading data
- svelteki의 액션은 앞서 설명한데로
[ UI ] -> [ 서버 액션 (actions) ] -> [ 서버 응답 (load) ] -> [ UI 피드백 ] 순으로 데이터 로딩이 이루어 진다. sveltekit은 서버에서 모든 요청이 처리되기 전에 실행되는 서버용 미들웨어로 src/hooks.server.js 파일에 작성되는 handle 함수를 지원한다.
handle함수는 주로 사용자의 인증 상태를 확인하고, 쿠키에서 사용자 정보를 읽어와 event.locals 객체에 저장하는 용도로 사용된다. event.locals는 한번의 요청-응답 주기 동안 서버의 여러곳(handle, action, load) 에서 데이터를 공유하기 위한 공간이다.
- 실행 순서 :
[ UI ] -> [ 미들웨어 (handle) ] -> [ 서버 액션 (actions) ] -> [ 서버 응답 (load) ] -> [ UI 피드백 ]
- 위 실행 순서를 기반으로 로그인 기능을 구현한다면 다음과 같은 이슈가 발생할 수 있다.
- 사용자가 로그인을 위해 로그인 페이지에 접근한다.
- 아직 로그인이 되어 있지 않으므로
handle함수에서 인증 정보를 null 또는 미인증 상태로 넘어가며 그에 따라 load 함수는 로그인 페이를 렌더링 한다.
- 로그인 정보 (ID, PW 등)를 입력하고 로그인
form action을 호출하면 handle 함수가 먼저 실행 되지만 로그인 정보가 없으므로 다음 actions 함수가 실행된다.
handle 함수에서는 formData에 접근이 불가 하기 때문에 로그인 처리가 불가능 하다.
handle 함수의 역할은 모든 요청의 가장 앞에서 실행되는 범용 미들웨어 역할로, 특정 페이지의 login 로직을 넣는 것은 적절하지 않다.
actions가 실행되며 사용자 정보를 인증하고 로그인 성공 쿠키를 생성하고 event.locals에 유저 정보를 업데이트 한다.
- 만약 action에서
event.locals를 업데이트 하지 않는다면, actions 함수 실행 후 호출 되는 load함수는 여전히 event.locals안에 유저 정보가 없어 렌더링 되는 페이지는 로그인이 되었다는 것을 알수 없다는 문제가 발생한다.
actions 내부에서 쿠키 처럼 handle이 의존하는 데이터를 변경한다면, event.locals의 값도 actions안에서 직접 수정 해야 한다.
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
console.log('/ hooks.server.ts handle()')
const sessionId = event.cookies.get('sessionId')
if (sessionId) {
event.locals.user = { name: 'ballboy', email: 'handle@handle.com' }
} else {
event.locals.user = null;
}
return resolve(event)
}
use:enhance
use:enhance는 sveltekit이 제공하는 svelte 액션이다. use:enhance를 HTML <form> 요소에 적용하면, 브라우저의 기본 폼 제출 동작을 가로채서 javascript fetch API를 사용한 방식으로 업그레이드 한다. 즉, 전체 페이지 새로고침 없이 폼을 제출하고 페이지를 업데이트 한다.
use:enhance의 효과
- 일반적인 Form 액션은 Form data를 서버로 데이터를 보내고 전체 페이지를 새로고침 하여 새로운 결과 페이지를 보여준다. 이 과정에서 화면이 깜빡이며(flash), 사용자 경험에 부정적인 영향을 미칠 수 있다.
use:enhance를 사용하면 사용자가 Form Data 제출 시 use:enhance가 이벤트를 가로챈 후, fetch를 사용해 백 그라운드에서 Form Data를 서버의 actions로 보낸다. actions이 완료되면, sveltekit은 페이지의 load 함수를 다시 실행하고, 변경된 data와 form prop를 받아 페이지의 필요한 부분만 업데이트 한다.
- 결과적으로 페이지 전체를 새로고침 하지 않아 SPA와 같은 부드러운 페이지 변경이 이루어진다.
<script lang="ts">
import { enhance } from "$app/forms";
const { form } = $props();
</script>
<h1>Contact Us</h1>
{#if form?.success}
<p style="color: green">Thanks for your message</p>
{/if}
<form method="post" use:enhance>
<label for="name">Name</label>
<input id="name" name="name" type="text" />
<label for="message">Message</label>
<textarea id="message" name="message"></textarea>
<button type="submit">Send</button>
</form>
callback 함수로 제출 과정 제어
use:enhance는 callback함수를 전달하여 폼 제출의 전 과정을 직접 제어할 수 있다. 데이터 처리 중 로딩 상태를 표시하거나 성공/실패에 따른 다른 UI를 표시할때 유용하다.
callback함수는 Submit 객체를 인자로 받고 ActionResult를 처리하는 비동기 함수를 반환해야 한다.
callback 함수의 구조
use:enhance = ({formElement, formData, action, cancel, submitter}) => {
return async ({result, update}) => {
}
}
use:enhance의 callback 함수
use:enhance의 callback 함수를 제공하는 순간, sveltekit은 개발자가 폼 제출 후의 과정을 직접 처리하는 것으로 인식하고 기본 동작을 멈춘다.
- sveltekit의 기본 동작이란
load함수를 다시 실행하여 페이지 데이터를 갱신하는 것과 성공적으로 제출된 폼의 입력 필드를 초기화 하는것 두 가지 이다.
callback함수 안에서 기 본 동작을 다시 실행시켜 주는 update나 applyAction을 호출 하지 않으면 폼 제출 후 화면에는 아무런 변화도 일어 나지 않는다.
update 함수는 sveltekit의 기본 업데이트 로직을 수동으로 실행시키는 재개 함수이며, reset, invalidateAll 파라미터를 받는다.
update({reset: true(default) | false })
- true 일 경우 actions이 성공적으로 끝나면 폼을 초기화 한다. (기본 동작)
- false 일 경우 actionsㅇ이 끝나도 사용자가 입력한 값을 폼에 그대로 남겨 둔다.
update({invalidateAll: true | false(default) })
- false 일 경우 현제 페이지의
load 함수만 다시 실행한다.
- 현제 페이지를 포함한 모든 레이아웃의
load함수를 모두 실행한다.
applyAction(result) 함수는 result 객체를 보고 알아서 적절한 동작을 수행 해주는 함수로 result.type에 다라 동작이 달라진다. update를 포함한 더 넒은 개념의 헬퍼 함수라고 할 수 있다.
result.type이 'success' 또는 'failure'일 경우 내부적으로 update()를 호출하여 페이지를 갱신한다.
result.type이 'redirect' 일 경우 클라이언트 사이드는 리다이렉트를 수행한다.
result.type이 'error' 일 경우 가장 가까운 +error.svelte 페이지를 렌더링 한다.