[Svelte 시리즈 11편] API 통합과 낙관적 UI 흐름

English version

폼을 만들었다면 이제 실제 서버와 연결해 빠른 반응을 유지해야 합니다.

시작 전에 정리하기

  • 무엇을 만든다: /projects 페이지에서 서버의 프로젝트 목록을 불러오고, 새 항목 생성과 별표 토글을 낙관적으로 처리하는 CRUD 흐름.
  • 왜 중요한가: load/invalidate 짝을 익히면 새로고침 없이 서버 상태를 맞출 수 있고, 낙관적 UI는 느린 네트워크에서도 사용감을 지켜 줍니다.
  • 주의할 점: depends 키와 invalidate 키를 정확히 맞추고, 낙관적 업데이트 전에 스냅샷을 저장해 실패 시 즉시 롤백해야 합니다.

이번 글에서 새로 나오는 용어

  1. load 함수: 페이지가 렌더링되기 전에 실행되어 초기 데이터를 불러오는 SvelteKit 서버 훅입니다.
  2. invalidate: 특정 키에 연결된 load 결과를 다시 실행하도록 요청해 화면을 새로고침 없이 갱신합니다.
  3. 낙관적 UI: 서버 응답을 기다리지 않고 예상 결과를 먼저 화면에 그려 체감 속도를 높이는 전략입니다.
  4. 스냅샷 롤백: 낙관적 업데이트 전에 리스트를 복사해 두었다가 실패 시 원본으로 되돌리는 안전 장치입니다.

개념

API(Application Programming Interface, 응용 프로그램 인터페이스)는 서버와 데이터를 주고받는 약속입니다. load 함수는 페이지가 렌더링될 때 API를 호출해 초기 데이터를 가져옵니다. invalidate 함수는 이미 실행한 load를 다시 실행하도록 요청하는 키 기반 트리거입니다. 낙관적 UI(optimistic UI, 먼저 보여 주는 인터페이스)는 서버 응답을 기다리지 않고 예상 결과를 화면에 그립니다. 이런 전략을 쓸 때는 스냅샷을 만들어 실패하면 곧바로 롤백할 준비를 해야 합니다.

서버·클라이언트 경계 다시 보기

  • +page.server.tsloadactions는 서버에서만 실행되어 DB나 비밀 키를 만집니다.
  • +page.svelte는 브라우저에서 렌더링되며, 서버가 돌려준 data를 받습니다. 여기서 invalidate를 호출하면 서버 load가 다시 실행됩니다.
  • 낙관적 토글은 브라우저 상태(projectsStore)를 먼저 바꾸고, 실패하면 서버 응답에 따라 복구합니다. 즉, “먼저 보여주고 나중에 확인” 순서를 의도적으로 만든다는 점을 기억하세요.

D2로 보는 데이터 흐름

브라우저 폼SvelteKit form action/api/projects RESTPrisma/Database+page.server loadwritable(projectsStore)Project list UI submitfetch POST /api/projectsinsertresult JSON201 responseinvalidate('data:projects')fetch GET /api/projectsdata.projectssubscribefetch POST /api/projects/:id/staroptimistic toggleerror => rollback snapshot

👉 미니 요약: 브라우저 폼이 액션을 호출하고, 액션이 API를 거친 뒤 invalidateload를 다시 실행해 목록을 새로고침 없이 맞춥니다. 별표 토글은 스냅샷을 저장해 두고 실패하면 즉시 롤백합니다.

코드

프로젝트 목록을 불러오고, 새 항목 등록 시 리스트를 갱신하며, 별표 토글을 낙관적으로 처리하는 흐름입니다.

// src/routes/projects/+page.server.ts

export const load = async ({ fetch, depends }) => {
  depends('data:projects');
  const res = await fetch('/api/projects');
  if (!res.ok) {
    return { projects: [], error: '프로젝트를 불러오지 못했습니다.' };
  }
  return { projects: await res.json() };
};

export const actions = {
  create: async ({ request, fetch }) => {
    const form = Object.fromEntries(await request.formData());
    if (!form.title) {
      return fail(400, { errors: { title: '제목은 필수입니다.' } });
    }
    const res = await fetch('/api/projects', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    });
    if (!res.ok) {
      return fail(500, { errors: { title: '생성에 실패했습니다.' } });
    }
    return { created: true };
  }
};
<!-- src/routes/projects/+page.svelte -->
<script>
  import { enhance } from '$app/forms';
  import { invalidate } from '$app/navigation';
  import { page } from '$app/stores';
  import { derived, writable, get } from 'svelte/store';
  export let data;

  const projectsStore = writable(data.projects);
  const pending = derived(page, ($page) => $page.form?.errors);

  async function toggleStar(id) {
    const snapshot = get(projectsStore);
    projectsStore.update((list) => list.map((p) => (p.id === id ? { ...p, starred: !p.starred } : p)));
    const res = await fetch(`/api/projects/${id}/star`, { method: 'POST' });
    if (!res.ok) projectsStore.set(snapshot);
  }
</script>

<form method="POST" action="?/create"
  use:enhance(({ result }) => {
    if (result.type === 'success') invalidate('data:projects');
  })>
  <label>
    <span class="sr-only">프로젝트 이름</span>
    <input name="title" placeholder="프로젝트 이름" aria-invalid={pending?.title ? 'true' : 'false'} required />
  </label>
  {#if pending?.title}
    <p class="form-error">{pending.title}</p>
  {/if}
  <button type="submit">추가</button>
</form>

<ul>
  {#each $projectsStore as project (project.id)}
    <li>
      <button type="button" on:click={() => toggleStar(project.id)} aria-pressed={project.starred}>
        {project.starred ? '★' : '☆'}
      </button>
      {project.title}
    </li>
  {/each}
</ul>
http://localhost:5173/projects
DEV

UI Preview

새 항목과 별표가 즉시 반영

Submit을 누르면 리스트가 깜박이지 않고 새 프로젝트가 추가됩니다. 별표 토글도 서버 응답을 기다리지 않고 반영되며, 실패 시 이전 상태로 복구됩니다.

load+invalidateoptimistic

List

Project Orbit

Star 버튼은 낙관적으로 토글.

Create

POST /api/projects

생성 성공 후 invalidate('data:projects').

Optimistic

스냅샷 롤백

서버 오류면 즉시 이전 배열을 복원.

👉 미니 요약: 화면에서 보이는 세 탭은 모두 같은 데이터 원본을 바라봅니다. 생성 폼은 성공 후 invalidate를 날리고, 리스트는 토글 시 즉시 반응하지만 오류가 나면 저장해 둔 배열을 돌려놓습니다.

depends('data:projects')invalidate('data:projects')가 짝을 이루어 새 항목을 만든 뒤 페이지 전체를 다시 불러오지 않고 리스트만 갱신합니다. 동시에 낙관적 업데이트가 별표 토글을 즉시 반영합니다.

왜 중요한가

교내 서비스나 컨테스트 대시보드에서 네트워크 지연이 길면 사용자가 반복 클릭을 하게 됩니다. 낙관적 UI로 먼저 결과를 보여주면 조급함을 줄이고, 실패했을 때만 롤백해 신뢰를 지킬 수 있습니다. 또한 invalidate 구조를 익히면 페이지 새로고침 없이 데이터를 동기화할 수 있어 팀 협업 시에도 반응형 UX를 유지할 수 있습니다.

실습

  • 따라 하기: loadinvalidate('data:projects') 버튼을 연결해 새로고침 없이 목록을 갱신한다.
  • 확장하기: toggleStar처럼 낙관적 업데이트를 추가하고 실패 시 알림을 띄운다.
  • 디버깅: 데이터가 갱신되지 않으면 depends 키와 invalidate 키가 일치하는지 확인한다.
  • 완료 기준: 새 항목 생성, 별표 토글, 수동 새로고침 흐름을 1분 안에 시연할 수 있고 실패 시 상태가 복구된다.

마무리

API 통합과 낙관적 UI는 속도와 신뢰의 균형을 맞춥니다. 다음 편에서는 이 패턴을 미니 대시보드에 녹여 여러 컴포넌트를 한 화면에 배치해 보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요