Post

SvelteKit Remote Functions - 서버 호출이 이렇게 쉬워진다고?

SvelteKit Remote Functions - 서버 호출이 이렇게 쉬워진다고?

SvelteKit을 쓰다 보면 서버에서 데이터를 읽거나, 폼을 제출하거나, 버튼 클릭으로 서버 작업을 실행하는 일이 자주 있다.

Remote Function은 이런 작업들을 로컬 함수를 호출하듯이 쓸 수 있게 해주는 SvelteKit의 기능이다.


Remote Function이란?

.remote 확장자를 가진 파일에서 query, form, command로 감싼 함수들을 말한다.

1
src/routes/blog/data.remote.ts

이 파일 안의 함수들은 서버에서만 실행되지만, 클라이언트에서 그냥 import해서 호출할 수 있다.


세 가지 Remote Function

함수용도
query데이터 읽기 (SELECT)
form폼 제출 (HTML form 연동)
command서버 작업 실행 (버튼 클릭 등)

1. query — 데이터 읽기

기존 방식

+page.server.js에서 load()를 만들고, 컴포넌트에서 $props()로 받아야 했다.

1
2
3
4
5
6
7
// src/routes/blog/+page.server.js
import * as db from '$lib/server/database';

export async function load() {
  const posts = await db.getPosts();
  return { posts }; // 반드시 return 해야 함
}
1
2
3
4
5
6
7
8
<!-- src/routes/blog/+page.svelte -->
<script>
  let { data } = $props(); // load()의 return 값을 여기서 받음
</script>

{#each data.posts as post}
  <p>{post.title}</p>
{/each}

Remote Function 방식

1
2
3
4
5
6
7
// src/routes/blog/data.remote.ts
import { query } from '$app/server';
import * as db from '$lib/server/database';

export const getPosts = query(async () => {
  return await db.getPosts();
});
1
2
3
4
5
6
7
8
9
10
<!-- src/routes/blog/+page.svelte -->
<script>
  import { getPosts } from './data.remote';

  const posts = await getPosts(); // 그냥 바로 호출!
</script>

{#each posts as post}
  <p>{post.title}</p>
{/each}

load(), return, $props() 없이 바로 호출할 수 있다.


2. form — 폼 제출

기존 방식

+page.server.jsactions를 따로 정의하고, 폼에서 action 속성으로 연결해야 했다.

유효성 검사도 서버와 클라이언트에 각각 따로 작성해야 했다.

1
2
3
4
5
6
7
// src/routes/admin/users/schema.ts
import * as v from 'valibot';

export const AddUserSchema = v.object({
  contact: v.pipe(v.string(), v.minLength(1, '연락처를 입력해주세요')),
  type: v.picklist(['email', 'phone'], '올바른 타입을 선택해주세요'),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/routes/admin/users/+page.server.js
import * as v from 'valibot';
import { AddUserSchema } from './schema';

export const actions = {
  addUser: async ({ request, locals }) => {
    // 인증 체크를 매번 직접 작성해야 함
    if (!locals.session || locals.session.role !== 'admin') {
      redirect(303, '/login');
    }

    const formData = await request.formData();
    const raw = Object.fromEntries(formData);

    // 서버에서 한 번 더 검사
    const result = v.safeParse(AddUserSchema, raw);
    if (!result.success) {
      return fail(400, { errors: v.flatten(result.issues) });
    }

    await db.insert(userTable).values(result.output);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- src/routes/admin/users/+page.svelte -->
<script>
  let errors = {};

  // 클라이언트에서 또 한 번 검사
  function handleSubmit(e) {
    const raw = Object.fromEntries(new FormData(e.target));
    const result = v.safeParse(AddUserSchema, raw);
    if (!result.success) {
      errors = v.flatten(result.issues);
      e.preventDefault();
    }
  }
</script>

<form method="POST" action="?/addUser" onsubmit={handleSubmit}>
  <input name="contact" />
  {#if errors.contact}<span>{errors.contact}</span>{/if}

  <input name="type" />
  {#if errors.type}<span>{errors.type}</span>{/if}

  <button type="submit">추가</button>
</form>

스키마를 정의해도 서버/클라이언트 양쪽에 검사 코드를 따로 연결해야 해서 번거롭다.

Remote Function 방식

form(Schema, handler) 형태로 스키마를 넘기면, 클라이언트 사이드 validation이 자동으로 적용된다.

1
2
3
4
5
6
7
// src/routes/admin/users/schema.ts
import * as v from 'valibot';

export const AddUserSchema = v.object({
  contact: v.pipe(v.string(), v.minLength(1, '연락처를 입력해주세요')),
  type: v.picklist(['email', 'phone'], '올바른 타입을 선택해주세요'),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/routes/admin/users/data.remote.ts
import { form } from '$app/server';
import { AddUserSchema } from './schema';

export const addUser = form(AddUserSchema, async (data, issue) => {
  // 인증 체크를 한 줄로
  requireSession('admin');

  // data는 이미 스키마 검증이 끝난 상태
  const existing = await db.query.userTable.findFirst({
    where: { contact: data.contact, type: data.type },
  });

  if (existing) {
    invalid(issue.contact('이미 등록된 사용자입니다'));
  }

  const [user] = await db
    .insert(userTable)
    .values({ contact: data.contact, type: data.type })
    .returning({ id: userTable.id });

  return { id: user!.id };
});
1
2
3
4
5
6
7
8
9
10
11
<!-- src/routes/admin/users/+page.svelte -->
<script>
  import { addUser } from './data.remote';
</script>

<!-- use:addUser 하나로 클라이언트 validation + 서버 제출 모두 처리 -->
<form use:addUser>
  <input name="contact" />
  <input name="type" />
  <button type="submit">추가</button>
</form>

스키마를 form()에 한 번만 넘기면 클라이언트 validation, 서버 검증, 에러 처리가 한 곳에서 연결된다.


3. command — 버튼 클릭으로 서버 작업 실행

폼 제출이 아니라, 버튼 클릭 같은 이벤트로 서버 작업을 실행할 때 쓴다.

기존 방식

API 엔드포인트를 따로 만들고, fetch로 직접 호출해야 했다.

1
2
3
4
5
6
// src/routes/api/post/delete/+server.js
export async function POST({ request }) {
  const { id } = await request.json();
  await db.deletePost(id);
  return new Response('ok');
}
1
2
3
4
5
6
7
8
9
10
11
12
<!-- src/routes/blog/+page.svelte -->
<script>
  async function handleDelete(id) {
    await fetch('/api/post/delete', {
      method: 'POST',
      body: JSON.stringify({ id }),
      headers: { 'Content-Type': 'application/json' }
    });
  }
</script>

<button onclick={() => handleDelete(post.id)}>삭제</button>

Remote Function 방식

1
2
3
4
5
6
7
// src/routes/blog/data.remote.ts
import { command } from '$app/server';
import * as db from '$lib/server/database';

export const deletePost = command(async ({ id }: { id: number }) => {
  await db.deletePost(id);
});
1
2
3
4
5
6
<!-- src/routes/blog/+page.svelte -->
<script>
  import { deletePost } from './data.remote';
</script>

<button onclick={() => deletePost({ id: post.id })}>삭제</button>

API 파일도 없고, fetch도 없고, URL도 없다.


정리

기존 방식은 서버 코드와 클라이언트 코드가 여러 파일에 나뉘어져 있어서 관리가 번거롭다.

Remote Function은 서버 로직을 .remote 파일 하나에 모아두고, 클라이언트에서 그냥 import해서 쓸 수 있게 해준다.

| |기존 방식|Remote Function| |—|—|—| |파일 수|여러 개|.remote 하나| |URL|직접 입력|필요 없음| |JSON 변환|직접 해야 함|자동| |코드 위치|여기저기 분산|한 곳에 모임|

Reference

Remote Functions - SvelteKit

This post is licensed under CC BY 4.0 by the author.