로드 함수: 데이터 불러오기

로드 함수: 데이터 불러오기

+page.svelte 컴포넌트(와 그 안의 +layout.svelte 컴포넌트들)를 렌더링하기 전에 데이터가 필요한 경우가 많아요. 이때 load 함수를 정의해서 데이터를 가져와요.

출처: SvelteKit 공식 문서 — Loading data

페이지 데이터

+page.svelte 파일은 같은 디렉터리에 +page.js를 둘 수 있고, 이 파일이 export하는 load 함수의 반환값은 data prop으로 페이지에서 쓸 수 있어요.

/// file: src/routes/blog/[slug]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
	return {
		post: {
			title: `Title for ${params.slug} goes here`,
			content: `Content for ${params.slug} goes here`
		}
	};
}
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

2.16.0 이전 버전에서는 페이지와 레이아웃의 props를 하나씩 타입으로 달아야 했어요:

/// file: +page.svelte
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();

Svelte 4에서는 export let data를 썼죠.

생성된 $types 모듈 덕분에 완전한 타입 안전성을 얻을 수 있어요.

+page.js 파일의 load 함수는 서버와 브라우저 양쪽에서 실행돼요(export const ssr = false와 함께 쓰면 브라우저에서만 실행돼요). load 함수가 (비공개 환경 변수를 쓰거나 데이터베이스에 접근한다든지 해서) 항상 서버에서 실행돼야 한다면 대신 +page.server.js에 넣으면 돼요.

서버에서만 실행되고 데이터베이스에서 데이터를 가져오는, 더 현실적인 블로그 포스트 load 함수를 보면 이렇게 생겼어요:

/// file: src/routes/blog/[slug]/+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
	export function getPost(slug: string): Promise<{ title: string, content: string }>
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
	return {
		post: await db.getPost(params.slug)
	};
}

타입이 PageLoad에서 PageServerLoad로 바뀐 것에 주목하세요. 서버 load 함수는 추가 인자에 접근할 수 있기 때문이에요. +page.js를 쓸지 +page.server.js를 쓸지 이해하려면 Universal vs server를 보면 돼요.

레이아웃 데이터

+layout.svelte 파일도 +layout.js+layout.server.js를 통해 데이터를 불러올 수 있어요.

/// file: src/routes/blog/[slug]/+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
	export function getPostSummaries(): Promise<Array<{ title: string, slug: string }>>
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
	return {
		posts: await db.getPostSummaries()
	};
}
<!--- file: src/routes/blog/[slug]/+layout.svelte --->
<script>
	/** @type {import('./$types').LayoutProps} */
	let { data, children } = $props();
</script>

<main>
	<!-- +page.svelte is `@render`ed here -->
	{@render children()}
</main>

<aside>
	<h2>More posts</h2>
	<ul>
		{#each data.posts as post}
			<li>
				<a href="/blog/{post.slug}">
					{post.title}
				</a>
			</li>
		{/each}
	</ul>
</aside>

LayoutProps는 2.16.0에서 추가됐어요. 더 이전 버전에서는 속성을 하나씩 타입으로 달아야 했어요:

/// file: +layout.svelte
/** @type {{ data: import('./$types').LayoutData, children: Snippet }} */
let { data, children } = $props();

레이아웃 load 함수가 돌려준 데이터는 그 레이아웃에 '속한' 하위 +layout.svelte 컴포넌트와 +page.svelte 컴포넌트에서도 사용할 수 있어요.

/// file: src/routes/blog/[slug]/+page.svelte
<script>
	+++import { page } from '$app/state';+++

	/** @type {import('./$types').PageProps} */
	let { data } = $props();

+++	// we can access `data.posts` because it's returned from
	// the parent layout `load` function
	let index = $derived(data.posts.findIndex(post => post.slug === page.params.slug));
	let next = $derived(data.posts[index + 1]);+++
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

+++{#if next}
	<p>Next post: <a href="/blog/{next.slug}">{next.title}</a></p>
{/if}+++

여러 load 함수가 같은 키로 데이터를 반환하면 마지막 것이 '승리'해요. 레이아웃 load{ a: 1, b: 2 }를, 페이지 load{ b: 3, c: 4 }를 반환하면 결과는 { a: 1, b: 3, c: 4 }가 돼요.

page.data

+page.svelte 컴포넌트와 그 위의 각 +layout.svelte 컴포넌트는 자신의 데이터뿐 아니라 모든 부모의 데이터에도 접근할 수 있어요.

때로는 그 반대가 필요할 수도 있어요. 부모 레이아웃이 페이지 데이터나 자식 레이아웃의 데이터에 접근해야 하는 경우죠. 예를 들어 루트 레이아웃이 +page.js+page.server.jsload 함수에서 반환된 title 속성에 접근하고 싶다면 page.data로 할 수 있어요:

<!--- file: src/routes/+layout.svelte --->
<script>
	import { page } from '$app/state';
</script>

<svelte:head>
	<title>{page.data.title}</title>
</svelte:head>

page.data의 타입 정보는 App.PageData가 제공해요.

$app/state는 SvelteKit 2.12에서 추가됐어요. 더 이전 버전이거나 Svelte 4를 쓰면 $app/stores를 대신 사용하세요. 같은 인터페이스의 page 스토어를 구독할 수 있어요(예: $page.data.title).

Universal vs server

앞서 봤듯이 load 함수에는 두 종류가 있어요.

  • +page.js+layout.js 파일은 서버와 브라우저 양쪽에서 실행되는 universal load 함수를 export해요.
  • +page.server.js+layout.server.js 파일은 서버에서만 실행되는 server load 함수를 export해요.

개념적으로는 같은 것이지만, 알아둬야 할 몇 가지 중요한 차이가 있어요.

어떤 load 함수가 언제 실행될까?

서버 load 함수는 항상 서버에서 실행돼요.

기본적으로 universal load 함수는 사용자가 처음 페이지를 방문할 때 SSR 중 서버에서 실행돼요. 그다음 hydration 중에 다시 실행되는데, 이때 fetch 요청의 응답을 재사용해요. 이후의 universal load 함수 호출은 모두 브라우저에서 일어나요. page options으로 이 동작을 커스터마이즈할 수 있어요. 서버 사이드 렌더링을 끄면 SPA가 되고 universal load 함수는 항상 클라이언트에서 실행돼요.

라우트에 universal과 server load 함수가 모두 있으면 server load가 먼저 실행돼요.

load 함수는 페이지를 prerender하지 않는 한 런타임에 호출돼요. prerender하면 빌드 시점에 호출돼요.

입력

universal과 server load 함수 모두 요청을 설명하는 속성(params, route, url)과 여러 함수(fetch, setHeaders, parent, depends, untrack)에 접근할 수 있어요. 이것들은 다음 섹션들에서 설명해요.

서버 load 함수는 ServerLoadEvent로 호출되는데, 이 이벤트는 RequestEvent에서 clientAddress, cookies, locals, platform, request를 상속받아요.

universal load 함수는 LoadEvent로 호출되는데, 이 이벤트는 data 속성이 있어요. +page.js+page.server.js(또는 +layout.js+layout.server.js) 양쪽에 load 함수가 있으면 서버 load 함수의 반환값이 universal load 함수 인자의 data 속성이 돼요.

출력

universal load 함수는 커스텀 클래스나 컴포넌트 생성자 같은 어떤 값이든 담은 객체를 반환할 수 있어요.

서버 load 함수는 devalue로 직렬화할 수 있는 데이터를 반환해야 해요 — JSON으로 표현할 수 있는 것에 BigInt, Date, Map, Set, RegExp, 반복·순환 참조까지 포함 — 그래야 네트워크로 전송할 수 있거든요. 데이터에 promise를 포함할 수도 있는데, 그 경우 브라우저로 스트리밍돼요. 커스텀 타입을 직렬화/역직렬화해야 한다면 transport hooks를 사용하세요.

언제 무엇을 쓸까?

서버 load 함수는 데이터베이스나 파일시스템에서 직접 데이터에 접근해야 하거나 비공개 환경 변수를 써야 할 때 편리해요.

universal load 함수는 외부 API에서 fetch로 데이터를 가져와야 하고 비공개 자격 증명이 필요 없을 때 유용해요. SvelteKit이 서버를 거치지 않고 API에서 직접 데이터를 가져올 수 있으니까요. 직렬화할 수 없는 것(예: Svelte 컴포넌트 생성자)을 반환해야 할 때도 유용해요.

드물게 둘을 함께 써야 할 때가 있어요. 예를 들어 서버의 데이터로 초기화한 커스텀 클래스의 인스턴스를 반환해야 할 때죠. 둘을 함께 쓰면 서버 load 반환값은 페이지에 직접 전달되지 않고 universal load 함수에(data 속성으로) 전달돼요.

/// file: src/routes/+page.server.js
/** @type {import('./$types').PageServerLoad} */
export async function load() {
	return {
		serverMessage: 'hello from server load function'
	};
}
/// file: src/routes/+page.js
// @errors: 18047
/** @type {import('./$types').PageLoad} */
export async function load({ data }) {
	return {
		serverMessage: data.serverMessage,
		universalMessage: 'hello from universal load function'
	};
}

URL 데이터 사용하기

load 함수는 어떤 식으로든 URL에 의존하는 경우가 많아요. 이를 위해 load 함수는 url, route, params를 제공해요.

url

origin, hostname, pathname, searchParams(파싱된 쿼리 문자열을 URLSearchParams 객체로 담은 것) 같은 속성을 가진 URL 인스턴스예요. url.hash는 서버에서 사용할 수 없으므로 load 중에는 접근할 수 없어요.

일부 환경에서는 서버 사이드 렌더링 중 이 값이 요청 헤더에서 파생돼요. 예를 들어 adapter-node을 쓴다면 URL이 올바르도록 어댑터를 설정해야 할 수도 있어요.

route

src/routes 기준의 현재 라우트 디렉터리 이름을 담고 있어요.

/// file: src/routes/a/[b]/[...c]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ route }) {
	console.log(route.id); // '/a/[b]/[...c]'
}

params

paramsurl.pathnameroute.id에서 파생돼요.

route.id/a/[b]/[...c]이고 url.pathname/a/x/y/z라면 params 객체는 이렇게 생겼어요:

{
	"b": "x",
	"c": "y/z"
}

fetch 요청하기

외부 API나 +server.js 핸들러에서 데이터를 가져오려면 제공된 fetch 함수를 사용할 수 있어요. 이 함수는 네이티브 fetch 웹 API와 동일하게 동작하면서 몇 가지 추가 기능이 있어요.

  • 서버에서 자격 증명이 있는 요청을 만들 수 있어요. 페이지 요청의 cookieauthorization 헤더를 상속받기 때문이에요.
  • 서버에서 상대 요청을 만들 수 있어요. 일반적으로 서버 컨텍스트에서 fetch는 origin이 있는 URL을 요구하거든요.
  • 내부 요청(예: +server.js 라우트)은 서버에서 실행될 때 HTTP 호출 오버헤드 없이 바로 핸들러 함수로 가요.
  • 서버 사이드 렌더링 중에는 응답이 캡처돼 Response 객체의 text, json, arrayBuffer 메서드에 훅을 걸어 렌더링된 HTML에 인라인돼요. filterSerializedResponseHeaders로 명시적으로 포함하지 않는 한 헤더는 직렬화되지 않아요.
  • hydration 중에는 응답이 HTML에서 읽혀 일관성을 보장하고 추가 네트워크 요청을 막아요. 브라우저 fetch를 쓰고 load fetch를 쓰지 않았을 때 브라우저 콘솔에서 경고를 받았다면 그 이유예요.
/// file: src/routes/items/[id]/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
	const res = await fetch(`/api/items/${params.id}`);
	const item = await res.json();

	return { item };
}

쿠키

서버 load 함수는 cookies를 읽고 쓸 수 있어요.

/// file: src/routes/+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
	export function getUser(sessionid: string | undefined): Promise<{ name: string, avatar: string }>
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
	const sessionid = cookies.get('sessionid');

	return {
		user: await db.getUser(sessionid)
	};
}

쿠키는 대상 호스트가 SvelteKit 앱과 같거나 그보다 더 구체적인 하위 도메인일 때만 제공된 fetch 함수로 전달돼요.

예를 들어 SvelteKit이 my.domain.com을 서빙한다면:

  • domain.com은 쿠키를 받지 못해요
  • my.domain.com은 쿠키를 받아요
  • api.domain.com은 쿠키를 받지 못해요
  • sub.my.domain.com은 쿠키를 받아요

credentials: 'include'를 설정하면 다른 쿠키는 전달되지 않아요. SvelteKit은 어떤 쿠키가 어느 도메인에 속하는지 알 수 없고(브라우저가 이 정보를 넘겨주지 않아요), 그래서 어느 것도 전달하는 게 안전하지 않기 때문이에요. 우회하려면 handleFetch 훅을 사용하세요.

헤더

서버와 universal load 함수 모두 setHeaders 함수에 접근할 수 있는데, 서버에서 실행될 때 응답의 헤더를 설정해요. (브라우저에서 실행될 때 setHeaders는 효과가 없어요.) 예를 들어 페이지를 캐시하고 싶을 때 유용해요.

// @errors: 2322 1360
/// file: src/routes/products/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
	const url = `https://cms.example.com/products.json`;
	const response = await fetch(url);

	// Headers are only set during SSR, caching the page's HTML
	// for the same length of time as the underlying data.
	setHeaders({
		age: response.headers.get('age'),
		'cache-control': response.headers.get('cache-control')
	});

	return response.json();
}

같은 헤더를 여러 번 설정하는 것(별도의 load 함수에서조차)은 오류예요. setHeaders로는 주어진 헤더를 한 번만 설정할 수 있어요. setHeadersset-cookie 헤더는 추가할 수 없어요 — cookies.set(name, value, options)를 사용하세요.

부모 데이터 사용하기

때로 load 함수가 부모 load 함수의 데이터에 접근하면 유용한데, await parent()로 할 수 있어요.

/// file: src/routes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
	return { a: 1 };
}
/// file: src/routes/abc/+layout.js
/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
	const { a } = await parent();
	return { b: a + 1 };
}
/// file: src/routes/abc/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
	const { a, b } = await parent();
	return { c: a + b };
}
<!--- file: src/routes/abc/+page.svelte --->
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();
</script>

<!-- renders `1 + 2 = 3` -->
<p>{data.a} + {data.b} = {data.c}</p>

+page.jsload 함수는 두 레이아웃 load 함수에서 합쳐진 데이터를 받는다는 점에 주목하세요. 바로 위 부모만이 아니에요.

+page.server.js+layout.server.js 안에서 parent는 부모 +layout.server.js 파일의 데이터를 반환해요.

+page.js+layout.js에서는 부모 +layout.js 파일의 데이터를 반환해요. 하지만 +layout.js가 없으면 ({ data }) => data 함수로 취급돼서, +layout.js 파일에 '가려지지' 않은 부모 +layout.server.js 파일의 데이터도 반환해요.

await parent()를 쓸 때 waterfall이 생기지 않도록 조심하세요. 여기서 getData(params)parent() 호출 결과에 의존하지 않아서, 렌더링 지연을 피하려면 먼저 호출해야 해요.

/// file: +page.js
// @filename: ambient.d.ts
declare function getData(params: Record<string, string>): Promise<{ meta: any }>

// @filename: index.js
// ---cut---
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
	---const parentData = await parent();---
	const data = await getData(params);
	+++const parentData = await parent();+++

	return {
		...data,
		meta: { ...parentData.meta, ...data.meta }
	};
}

오류

load 중에 오류가 발생하면 가장 가까운 +error.svelte가 렌더링돼요. 예상된 오류에는 @sveltejs/kiterror 헬퍼로 HTTP 상태 코드와 선택적 메시지를 지정해요.

/// file: src/routes/admin/+layout.server.js
// @filename: ambient.d.ts
declare namespace App {
	interface Locals {
		user?: {
			name: string;
			isAdmin: boolean;
		}
	}
}

// @filename: index.js
// ---cut---
import { error } from '@sveltejs/kit';

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
	if (!locals.user) {
		error(401, 'not logged in');
	}

	if (!locals.user.isAdmin) {
		error(403, 'not an admin');
	}
}

error(...)를 호출하면 예외가 던져져서 헬퍼 함수 내부에서 실행을 멈추기 쉽게 해줘요.

예상치 못한 오류가 던져지면 SvelteKit은 handleError를 호출하고 500 내부 오류로 처리해요.

SvelteKit 1.x에서는 오류를 직접 throw해야 했어요.

리다이렉트

사용자를 리다이렉트하려면 @sveltejs/kitredirect 헬퍼로 리다이렉트할 위치를 3xx 상태 코드와 함께 지정해요. error(...)처럼 redirect(...)를 호출하면 예외가 던져져서 헬퍼 함수 내부에서 실행을 멈추기 쉽게 해줘요.

/// file: src/routes/user/+layout.server.js
// @filename: ambient.d.ts
declare namespace App {
	interface Locals {
		user?: {
			name: string;
		}
	}
}

// @filename: index.js
// ---cut---
import { redirect } from '@sveltejs/kit';

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
	if (!locals.user) {
		redirect(307, '/login');
	}
}

try {...} 블록 안에서는 redirect()를 쓰지 마세요. 리다이렉트가 즉시 catch 문을 발동시키거든요.

브라우저에서는 load 함수 밖에서도 $app.navigationgoto로 프로그래매틱하게 네비게이션할 수 있어요.

SvelteKit 1.x에서는 redirect를 직접 throw해야 했어요.

promise로 스트리밍

서버 load를 쓰면 promise가 해결되는 대로 브라우저로 스트리밍돼요. 느리고 필수적이지 않은 데이터가 있을 때 유용해요. 모든 데이터가 준비되기 전에 페이지 렌더링을 시작할 수 있으니까요.

/// file: src/routes/blog/[slug]/+page.server.js
// @filename: ambient.d.ts
declare global {
	const loadPost: (slug: string) => Promise<{ title: string, content: string }>;
	const loadComments: (slug: string) => Promise<{ content: string }>;
}

export {};

// @filename: index.js
// ---cut---
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
	return {
		// make sure the `await` happens at the end, otherwise we
		// can't start loading comments until we've loaded the post
		comments: loadComments(params.slug),
		post: await loadPost(params.slug)
	};
}

이것은 예를 들어 스켈레톤 로딩 상태를 만드는 데 유용해요.

<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

{#await data.comments}
	Loading comments...
{:then comments}
	{#each comments as comment}
		<p>{comment.content}</p>
	{/each}
{:catch error}
	<p>error loading comments: {error.message}</p>
{/await}

데이터를 스트리밍할 때는 promise 거부(rejection)를 올바르게 처리하는 데 주의하세요. 구체적으로, 지연 로드되는 promise가 렌더링이 시작되기 전에(그 시점에 잡히므로) 실패하고 어떤 방식으로든 오류를 처리하지 않으면 서버가 "unhandled promise rejection" 오류로 크래시할 수 있어요. load 함수에서 SvelteKit의 fetch를 직접 쓰면 SvelteKit이 이 경우를 처리해줘요. 다른 promise라면 promise에 noop-catch를 붙여 처리된 것으로 표시하면 충분해요.

/// file: src/routes/+page.server.js
/** @type {import('./$types').PageServerLoad} */
export function load({ fetch }) {
	const ok_manual = Promise.reject();
	ok_manual.catch(() => {});

	return {
		ok_manual,
		ok_fetch: fetch('/fetch/that/could/fail'),
		dangerous_unhandled: Promise.reject()
	};
}

AWS Lambda나 Firebase처럼 스트리밍을 지원하지 않는 플랫폼에서는 응답이 버퍼링돼요. 즉 모든 promise가 해결된 후에야 페이지가 렌더링된단 뜻이에요. 프록시(예: NGINX)를 쓴다면 프록시된 서버의 응답을 버퍼링하지 않는지 확인하세요.

스트리밍 데이터는 JavaScript가 활성화됐을 때만 동작해요. 페이지가 서버 렌더링된다면 universal load 함수에서 promise를 반환하지 않아야 해요. 이 promise는 스트리밍되지 않고, 함수가 브라우저에서 다시 실행될 때 재생성되기 때문이에요.

응답이 스트리밍을 시작하면 헤더와 상태 코드는 바꿀 수 없어요. 따라서 스트리밍되는 promise 안에서는 setHeaders를 하거나 리다이렉트를 던질 수 없어요.

SvelteKit 1.x에서는 최상위 promise가 자동으로 await됐고, 중첩 promise만 스트리밍됐어요.

병렬 로딩

페이지를 렌더링(또는 네비게이션)할 때 SvelteKit은 모든 load 함수를 동시에 실행해서 요청의 waterfall을 피해요. 클라이언트 사이드 네비게이션 중에는 여러 서버 load 함수를 호출한 결과가 하나의 응답으로 묶여요. 모든 load 함수가 반환되면 페이지가 렌더링돼요.

load 함수 다시 실행

SvelteKit은 각 load 함수의 의존성을 추적해서 네비게이션 중에 불필요하게 다시 실행하지 않도록 해요.

예를 들어 이런 한 쌍의 load 함수가 있다고 해볼게요...

/// file: src/routes/blog/[slug]/+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
	export function getPost(slug: string): Promise<{ title: string, content: string }>
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
	return {
		post: await db.getPost(params.slug)
	};
}
/// file: src/routes/blog/[slug]/+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
	export function getPostSummaries(): Promise<Array<{ title: string, slug: string }>>
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
	return {
		posts: await db.getPostSummaries()
	};
}

...+page.server.js의 함수는 /blog/trying-the-raw-meat-diet에서 /blog/i-regret-my-choices로 네비게이션하면 params.slug가 바뀌었으므로 다시 실행돼요. +layout.server.js의 함수는 데이터가 여전히 유효하므로 다시 실행되지 않아요. 다시 말해 db.getPostSummaries()를 두 번 호출하지 않는다는 뜻이에요.

await parent()를 호출하는 load 함수도 부모 load 함수가 다시 실행되면 다시 실행돼요.

의존성 추적은 load 함수가 반환된 이후에는 적용되지 않아요. 예를 들어 중첩 promise 안에서 params.x에 접근해도 params.x가 바뀌어도 함수가 다시 실행되지 않아요. (걱정 마세요. 실수로 이렇게 하면 개발 중에 경고가 나와요.) 대신 load 함수의 본문에서 매개변수에 접근하세요.

검색 파라미터는 나머지 url과 별도로 추적돼요. 예를 들어 load 함수 안에서 event.url.searchParams.get("x")에 접근하면 ?x=1에서 ?x=2로 네비게이션할 때 load 함수가 다시 실행되지만, ?x=1&y=1에서 ?x=1&y=2로 갈 때는 다시 실행되지 않아요.

의존성 추적에서 제외하기

드물게 무언가를 의존성 추적 메커니즘에서 제외하고 싶을 수 있어요. 이때는 제공된 untrack 함수를 쓰면 돼요.

/// file: src/routes/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ untrack, url }) {
	// Untrack url.pathname so that path changes don't trigger a rerun
	if (untrack(() => url.pathname === '/')) {
		return { message: 'Welcome!' };
	}
}

수동 무효화

현재 페이지에 적용되는 load 함수를 invalidate(url)로 다시 실행할 수도 있어요. 이 함수는 url에 의존하는 모든 load 함수를 다시 실행하고, invalidateAll()은 모든 load 함수를 다시 실행해요. 서버 load 함수는 비밀을 클라이언트로 새지 않게 하기 위해 가져온 url에 자동으로 의존하지 않아요.

load 함수가 fetch(url)depends(url)를 호출하면 url에 의존해요. url[a-z]:로 시작하는 커스텀 식별자일 수도 있어요.

/// file: src/routes/random-number/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, depends }) {
	// load reruns when `invalidate('https://api.example.com/random-number')` is called...
	const response = await fetch('https://api.example.com/random-number');

	// ...or when `invalidate('app:random')` is called
	depends('app:random');

	return {
		number: await response.json()
	};
}
<!--- file: src/routes/random-number/+page.svelte --->
<script>
	import { invalidate, invalidateAll } from '$app/navigation';

	/** @type {import('./$types').PageProps} */
	let { data } = $props();

	function rerunLoadFunction() {
		// any of these will cause the `load` function to rerun
		invalidate('app:random');
		invalidate('https://api.example.com/random-number');
		invalidate(url => url.href.includes('random-number'));
		invalidateAll();
	}
</script>

<p>random number: {data.number}</p>
<button onclick={rerunLoadFunction}>Update random number</button>

load 함수는 언제 다시 실행될까?

정리하면 load 함수는 다음 상황에서 다시 실행돼요.

  • 값이 바뀐 params의 속성을 참조할 때
  • 값이 바뀐 url의 속성(예: url.pathname이나 url.search)을 참조할 때. request.url의 속성은 추적되지 않아요
  • url.searchParams.get(...), url.searchParams.getAll(...), url.searchParams.has(...)를 호출하는데 해당 파라미터가 바뀔 때. url.searchParams의 다른 속성에 접근하는 것은 url.search에 접근하는 것과 같은 효과예요
  • await parent()를 호출했는데 부모 load 함수가 다시 실행됐을 때
  • 자식 load 함수가 await parent()를 호출하며 다시 실행 중이고, 그 부모가 서버 load 함수일 때
  • [fetch](#Making-fetch-requests)(universal load만) 또는 depends로 특정 URL에 의존을 선언했는데, 그 URL이 invalidate(url)`로 무효화됐을 때
  • 모든 활성 load 함수가 [invalidateAll()]($app-navigation#invalidateAll)로 강제로 다시 실행됐을 때

paramsurl<a href=".."> 링크 클릭, <form> 상호작용, goto 호출, redirect에 반응해 바뀔 수 있어요.

load 함수를 다시 실행하면 해당 +layout.svelte+page.svelte 안의 data prop이 갱신돼요. 컴포넌트가 재생성되지는 않아요. 결과적으로 내부 상태가 보존돼요. 이것이 원하는 게 아니라면 [afterNavigate]($app-navigation#afterNavigate) 콜백 안에서 필요한 것을 리셋하고/하거나 컴포넌트를 [{#key ...}](../svelte/key) 블록으로 감싸면 돼요.

인증에 미치는 영향

데이터 로딩의 몇 가지 특징은 auth 검사에 중요한 시사점을 줘요.

  • 레이아웃 load 함수는 자식 라우트 사이의 클라이언트 사이드 네비게이션 같은 경우 매 요청마다 실행되지 않아요. (load 함수는 언제 다시 실행되나요?)
  • 레이아웃과 페이지 load 함수는 await parent()를 호출하지 않는 한 동시에 실행돼요. 레이아웃 load가 던지면 페이지 load 함수는 실행되지만, 클라이언트는 반환된 데이터를 받지 못해요.

보호된 코드가 실행되기 전에 auth 검사가 일어나도록 보장하는 여러 전략이 있어요.

데이터 waterfall을 막고 레이아웃 load 캐시를 보존하려면:

  • hooks로 어떤 load 함수가 실행되기 전에 여러 라우트를 보호하세요.
  • 라우트별 보호는 +page.server.js load 함수에 auth 가드를 직접 넣으세요.

+layout.server.js에 auth 가드를 두면 모든 자식 페이지가 보호된 코드 전에 await parent()를 호출해야 해요. 모든 자식 페이지가 await parent()의 반환 데이터에 의존하지 않는 한, 다른 옵션들이 더 성능이 좋아요.

getRequestEvent 사용하기

서버 load 함수를 실행할 때 함수에 인자로 전달되는 event 객체는 getRequestEvent로도 가져올 수 있어요. 이렇게 하면 공유 로직(인증 가드 같은)이 현재 요청에 대한 정보를 전달받지 않고도 접근할 수 있어요.

예를 들어 사용자가 로그인해야 하고 아니면 /login으로 리다이렉트하는 함수가 있다고 해볼게요.

/// file: src/lib/server/auth.js
// @filename: ambient.d.ts
interface User {
	name: string;
}

declare namespace App {
	interface Locals {
		user?: User;
	}
}

// @filename: index.ts
// ---cut---
import { redirect } from '@sveltejs/kit';
import { getRequestEvent } from '$app/server';

export function requireLogin() {
	const { locals, url } = getRequestEvent();

	// assume `locals.user` is populated in `handle`
	if (!locals.user) {
		const redirectTo = url.pathname + url.search;
		const params = new URLSearchParams({ redirectTo });

		redirect(303, `/login?${params}`);
	}

	return locals.user;
}

이제 어떤 load 함수(또는 예를 들어 form action)에서 requireLogin을 호출해 사용자가 로그인했음을 보장할 수 있어요.

/// file: +page.server.js
// @filename: ambient.d.ts

declare module '$lib/server/auth' {
	interface User {
		name: string;
	}

	export function requireLogin(): User;
}

// @filename: index.ts
// ---cut---
import { requireLogin } from '$lib/server/auth';

export function load() {
	const user = requireLogin();

	// `user` is guaranteed to be a user object here, because otherwise
	// `requireLogin` would throw a redirect and we wouldn't get here
	return {
		message: `hello ${user.name}!`
	};
}

더 알아보기