클라이언트에서 검증하고, 서버에서 다시 검증하는 것 — 이 이중 검증이 보안의 기본입니다.

개념 정의

폼 검증(Form Validation) 은 사용자 입력이 유효한지 확인하는 과정입니다. 클라이언트 검증은 UX를 위해, 서버 검증은 보안을 위해 반드시 모두 필요합니다.

기본 검증 — 순수 Svelte

SVELTE
<script>
  let form = $state({ email: '', password: '', name: '' });

  let errors = $derived.by(() => {
    const e = {};
    if (form.email && !form.email.includes('@')) e.email = '유효한 이메일을 입력하세요';
    if (form.password && form.password.length < 8) e.password = '8자 이상 입력하세요';
    if (form.name && form.name.length < 2) e.name = '2자 이상 입력하세요';
    return e;
  });

  let touched = $state({});
  let isValid = $derived(
    form.email && form.password && form.name &&
    Object.keys(errors).length === 0
  );

  function handleBlur(field) {
    touched[field] = true;
  }
</script>

<form onsubmit={(e) => { e.preventDefault(); console.log(form); }}>
  <div>
    <input bind:value={form.email} onblur={() => handleBlur('email')} placeholder="이메일" />
    {#if touched.email && errors.email}
      <p class="error">{errors.email}</p>
    {/if}
  </div>

  <div>
    <input type="password" bind:value={form.password} onblur={() => handleBlur('password')} placeholder="비밀번호" />
    {#if touched.password && errors.password}
      <p class="error">{errors.password}</p>
    {/if}
  </div>

  <button disabled={!isValid}>가입</button>
</form>

Zod를 활용한 스키마 검증

JAVASCRIPT
// src/lib/schemas.js
import { z } from 'zod';

export const registerSchema = z.object({
  email: z.string().email('유효한 이메일을 입력하세요'),
  password: z.string()
    .min(8, '8자 이상 입력하세요')
    .regex(/[A-Z]/, '대문자를 포함하세요')
    .regex(/[0-9]/, '숫자를 포함하세요'),
  name: z.string().min(2, '2자 이상 입력하세요').max(50),
  agreed: z.literal(true, { errorMap: () => ({ message: '약관에 동의해주세요' }) }),
});
JAVASCRIPT
// src/routes/register/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import { registerSchema } from '$lib/schemas';

export const actions = {
  default: async ({ request }) => {
    const formData = await request.formData();
    const data = Object.fromEntries(formData);
    data.agreed = formData.has('agreed');

    const result = registerSchema.safeParse(data);

    if (!result.success) {
      const fieldErrors = result.error.flatten().fieldErrors;
      return fail(400, { data, errors: fieldErrors });
    }

    await createUser(result.data);
    redirect(303, '/login');
  }
};
SVELTE
<!-- src/routes/register/+page.svelte -->
<script>
  import { enhance } from '$app/forms';
  let { form } = $props();
</script>

<form method="POST" use:enhance>
  <input name="email" value={form?.data?.email ?? ''} />
  {#if form?.errors?.email}
    <p class="error">{form.errors.email[0]}</p>
  {/if}

  <input type="password" name="password" />
  {#if form?.errors?.password}
    <p class="error">{form.errors.password[0]}</p>
  {/if}

  <input name="name" value={form?.data?.name ?? ''} />
  {#if form?.errors?.name}
    <p class="error">{form.errors.name[0]}</p>
  {/if}

  <label>
    <input type="checkbox" name="agreed" />
    약관 동의
  </label>
  {#if form?.errors?.agreed}
    <p class="error">{form.errors.agreed[0]}</p>
  {/if}

  <button>가입</button>
</form>

실시간 검증 + 서버 검증 조합

SVELTE
<script>
  import { enhance } from '$app/forms';
  import { registerSchema } from '$lib/schemas';

  let { form: serverForm } = $props();
  let formData = $state({ email: '', password: '', name: '' });
  let touched = $state({});

  // 클라이언트 실시간 검증
  let clientErrors = $derived.by(() => {
    const result = registerSchema.safeParse(formData);
    if (result.success) return {};
    return result.error.flatten().fieldErrors;
  });

  function showError(field) {
    return touched[field] && (clientErrors[field]?.[0] || serverForm?.errors?.[field]?.[0]);
  }
</script>

<form method="POST" use:enhance>
  <input
    name="email"
    bind:value={formData.email}
    onblur={() => touched.email = true}
  />
  {#if showError('email')}
    <p class="error">{showError('email')}</p>
  {/if}
  <!-- ... 나머지 필드 -->
</form>

면접 포인트

  • "클라이언트 검증만으로 충분하지 않은 이유는?": 클라이언트 코드는 수정 가능합니다. 브라우저 개발자 도구나 API 직접 호출로 검증을 우회할 수 있으므로, 서버 측 검증은 보안상 필수입니다.
  • "Zod 같은 스키마 라이브러리의 장점은?": 스키마를 한 번 정의하면 클라이언트와 서버 양쪽에서 재사용할 수 있고, TypeScript 타입도 자동 추론됩니다.

정리

폼 검증은 "클라이언트에서 즉각 피드백, 서버에서 최종 보안 검증"이라는 이중 구조가 표준입니다. Zod로 스키마를 한 번 정의하고 양쪽에서 재사용하면, 중복 코드 없이 안전한 폼 처리가 가능합니다.

댓글 로딩 중...