심심한 개발자의 취미생활

데이터 로드

  • +page.svelte 컴포넌트를 렌더링하기 전에 +layout.svelte 데이터를 가져와야 하는 경우가 많다. 이는 load 함수를 정의하여 수행된다.

Page data

  • +page.svelte 파일은 로드 함수를 내보내는 형제 파일이 있을 수 있으며, 반환 값은 $props()를 통해 페이지에서 사용할 수 있다.
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = ({ params }) => {
	return {
		post: {
			title: `Title for ${params.slug} goes here`,
			content: `Content for ${params.slug} goes here`
		}
	};
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
	import type { PageProps } from './$types';

	let { data }: PageProps = $props();
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
  • +page.ts 파일 내의 load 함수는 서버와 브라우저에서 모두 실행된다. (export const ssr = false가 설정 된 경우는 브라우저에서만 실행된다.)
  • load함수가 항상 서버에서만 실행되어야 하는 경우는 동일한 경로의 +page.server.ts 파일에 작성한다.
// src/routes/blog/[slug]/+page.server.ts
import * as db from '$lib/server/database';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
	return {
		post: await db.getPost(params.slug)
	};
};
  • 서버 함수가 추가 인수에 접근할 수 있으므로 PageLoad형에서 PageServerLoad로 변경된다.

Layout data

  • +layout.ts파일 또는 +layout.server.ts파일은 +layout.svelte 를 통해 데이터를 로드할 수 있다.
// src/routes/blog/[slug]/+layout.server.ts
import * as db from '$lib/server/database';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async () => {
	return {
		posts: await db.getPostSummaries()
	};
};
<script lang="ts">
	import type { LayoutProps } from './$types';

	let { data, children }: LayoutProps = $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>
  • layout에서 load 함수를 통해 리턴된 데이터는 하위 +layout.svelte+page.svelte 구성 요소 뿐만 아니라 해당 구성 요소가 속하는 레이아웃에서도 사용할 수 있다.
  • layoutpage에서 동일한 변수 명으로 반환한 데이터는 page에서 반환된 데이터로 출력된다.

page.data

  • +page.svelte+layout.svelte
  • 자식은 부모의 데이터에 접근 할 수 있지만, 그 반대는 기본적을 불가능 하다. 하지만 때로는 상향식으로 데이터가 필요한 경우가 있는데 이러한 데이터 흐름의 역전을 가능하게 해주는 것이 바로 $page스토어 이다.
  • sveltekit은 앱의 현재 페이지 상태에 대한 정보를 담고 있는 $page라는 특별한 스토어를 제공하는데 이 스토어 안에는 여러 유용한 정보가 있다. 그중 $page.data 는 현재 활성화된 라우트에 대한 모든 load 함수의 반환 값을 합친 객체이다.
// src/routes/sub/+page.js
export const load = () => {
    const post = {
        title: 'load test title',
        content: 'load test content'
    }

    return {
        title: post.title,
		post: post
    }
}
<!-- src/routes/+layout.js -->
<script lang="ts">
	import { page } from "$app/state";
	import { onMount } from "svelte";

	let { children } = $props();

	onMount(() => {
		console.log("main >>", page);
	});
</script>

<!-- sub/+page.svelte 페이지 로딩시 <h1>load test title</h1> 출력 -->
<h1>{page.data.title}</h1>

{@render children?.()}

부모 데이터의 사용

  • await parent()를 사용하면 자식 load함수에서 부모 load함수의 데이터에 엑세스 할 수 있다.
// src/routes/+layout.ts
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = () => {
	return { a: 1 };
};
// src/routes/item/+layout.ts
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async ({ parent }) => {
	const { a } = await parent();
	return { b: a + 1 };
};
// src/routes/item/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ parent }) => {
	const { a, b } = await parent();
	return { c: a + b };
};
// src/routes/item/+page.svelte
<script lang="ts">
	import type { PageProps } from './$types';

	let { data }: PageProps = $props();
</script>

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

`parent()`의 기본 동작

  • parent() 함수는 server파일과 universal 파일에서 각각 다르게 동작하며, 서로의 데이터 체인을 침범하지 않는 것이 기본 원칙이다.
    • +layout.server.ts => +layout.server.ts => +page.server.ts => load - parent()
    • +layout.ts => +layout.ts => +page.ts => load - parent()

'투명한 통로'와 '가려짐(shadowing)' 현상

  • sveltekit은 중간에 +layout.ts 파일이 없으면 그 위치에 서버에서 온 데이터를 그대로 통과 시키는 투명한 함수 ({ data }) => data가 있다고 간주 한다.
src/routes/
└── sales/
    ├── +layout.server.ts  // (A)
    └── report/
        ├── +layout.ts     // (B)
        └── +page.server.ts  // (C)
  • 데이터 흐름
    • sales/+layout.server.ts => report/+page.server.ts (C) => load - parent()
    • sales/+layout.server.ts (A) => sales/({data}) => data (투명한 통로) => report/+layout.ts (B) => load - parent()
  • 만약 sales/+layout.ts 파일을 추가한다면 report/+layout.ts에서 parent() 호출 할 경우 새로 생신 sales/+layout.ts에서 load함수가 반환하는 값을 받게 된다. sales/+layout.tsload에서 다른 서버 데이터를 가공하거나 다른 값을 반환 한다면 sales/+layout.server.ts (A)의 데이터는 report/+layout.ts (B)에 직접 도달하지 못하고 '가려 지게'된다.

성능 최적화

  • parent()함수는 부모 load함수들이 모두 실행 될 때까지 기다려야 하므로 시간이 걸릴 수 있다.
// 잘못된 패턴 - parent가 완료 될 때까지 getData()를 시작하지 못한다.
export const load = async () => {
	// 1. 부모 데이터가 올때까지 대기
	const parentData = await parent();
	// 2. 부모 데이터가 수신 되어야 데이터를 가져오기 시작
	const myData = await getData(params)
	return {...parentData, myData}
}
// 올바른 패턴 - parent()의 결과에 의존하지 않는 비동기 작업은 미리 시작해서 병렬로 실행해야 한다.
export const load = async () => {
	// 1. 데이터를 가져 오는 작업을 즉시 시작 (Promise를 변수에 저장)
	const myData = getData(params)
	// 2. 부모 데이터를 기다리는 동안, myData를 백그라운드에서 실행
	const parentData = await parent();
	// 3. 두 작업이 모두 완료 되기를 기다림
	return {...parentData, myData: await myData}
}

Universal vs server

  • load 함수는 서버와 브라우저 모두에서 실행되는 +page.ts, +layout.ts와 서버에서만 실행되는 +page.server.ts, +layout.server.ts 두 가지가 있다. 기본적으로는 범용 load함수는 사용자가 페이지를 처음 방문할 때 SSR 중에 서버에서 실행된다. 그런 다음 하이드 레이션 중에 다시 실행 되어 페치 요청의 응답을 재사용한다. 이후 모든 범용 함수는 브라우저에서 실행된다.
  • 다만 페이지 옵션을 통해 서버측 렌더링을 비활성화 하면 load 범용 함수는 항상 클라이언트에서 실행된다.
  • 유니버설과 서버 load 기능이 모두 포함된 경우라면 서버 load가 먼저 실행 된다.
  • load는 일반적으로 런타임에 호출 되지만, 페이지를 미리 렌더링 하는 경우에는 빌드 시점에 호출 된다.

구조의 분리

  • load함수가 실행 환경에 따라 분리 되는 구조는 관심사의 분리를 가능하게 한다.
    • Server load어떻게 데이터를 안전하게 가져올 것인가?
    • Universal load무엇을 페이지에 보여줄 것인가?
    • Server load는 데이터의 원천 소스를 다루고 Universal load는 그 데이터를 받아 최종적으로 프론트 엔드에 전달할 데이터를 가공하고 필터링 하는 게이트 웨이 역할을 하는 것이다.

Argument Input

  • 두 실행 환경의 load 함수가 한 페이지에 동시에 존재할 경우 Server load 함수의 반환 값은 Universal load함수 인자의 data 속성이 된다.
  • 두 실행 환경의 load 함수는 대부분의 인자를 공유하지만 실행 환경에 따라 특별한 인자를 가진다.

공통 인자

  • Universal Load 함수는 Server Load 함수의 대부분의 인자를 공유하고 이 인자 들은 LoadEvent (Universal)ServerLoadEvent (Server)객체에 포함되어 있다.
    • params : 라우트 파라미터 객체
    • route : 현재 라우트에 대한 정보 객체
    • url : 현재 페이지의 URL에 대한 정보를 담고 있는 표준 URL 객체
    • fetch : 서버와 클라이언트 양쪽에서 모두 동작하는 fetch 함수. 일반 fetch와 달리 서버에서 실행될 때도 상대 경로를 사용할 수 있고, 쿠키와 같은 인증 정보를 자동으로 처리해준다.
    • setHeaders : 응답 헤더를 설정하는 함수, 주로 캐싱 제어에 사용된다.
    • parent : 부모 load함수에서 반환된 데이터를 가져오는 함수 const data = await parent() 와 같이 사용된다.
    • depends : 특정 데이터에 대한 의존성을 수동으로 등록하는 함수, 데이터 무효화 및 재실행을 제어할 때 사용한다.
    • untrack : load함수 내에서 반응성 추적을 피하고 싶을 때 사용하는 함수.

`Server Load` 에만 있는 인자들

  • ServerLoadEventLoadEvent의 모든 속성을 상속 받으며, 서버에서만 접근 가능하고 절대 브라우저에 노출 되어서는 안 되는 민감한 정보들을 추가로 가진다.
    • clientAddress : 사용자의 IP 주소
    • cookies : 들어오는 요청의 쿠키를 읽거나 나가는 응답에 쿠키를 설정할 수 있는 헬퍼. cookies.get('sessionId'), cookies.set('theme', 'dark')
    • locals : 미들웨어에서 요청에 추가한 커스텀 데이터. 주로 인증된 사용자 정보등을 담는 데 사용된다.
    • platform : Vercel, Netlify, Cloudflare Workers 등 배포 환경에 따른 컨텍스트 정보.
    • request : SvelteKit에 의해 가공되기 전의 원시 표준 Request 객체

Output

  • Universal loadServer load 함수의 반환 값은 각 함수가 실행되는 환경과 네트워크 경계의 존재의 유무 때문에 차이가 발생한다.
    • Universal load: 서버와 브라우저, 즉 데이터를 소비하는 컴포넌트와 같은 공간에서 실행되 수 있어, 네트워크 전송을 위한 직렬화 과정이 필요 없기에 거의 모든 유형의 값을 반환할 수 있다.
    • Server load: 오직 서버에서만 실행되며, 그 결과는 네트워크를 통해 브라우저로 전송된다. 따라서 반환 값은 반드시 네트워크로 전송 가능한 형태, 즉 직렬화가 가능한 데이터여야만 한다.
  • 만약 devalue가 지원하지 않는 커스텀 타입을 꼭 네트워크로 전송해야 한다면, Transport Hooks(hooks.server.ts에 정의)를 사용하여 해당 타입을 어떻게 직렬화 하고 역 직렬화를 할지 sveltekit에 직접 설정할 수 있다.

`Universal load`함수의 반환 값

  • Universal load 함수는 반환 값에 거의 제약이 없다. 서버 렌더링 시에는 load 함수와 페이지 컴포넌트는 동일한 서버 프로세스 내에서 중간에 네트워크를 거치지 않는다. 클라이언트 렌더링 시에는 사용자가 페이지를 이동할 때 Universal load 함수와 페이지 컨포넌트는 동일한 브라우저 환경 내에서 실행되므로 여기서도 값을 직접 전달할 수 있다.
    • 일반적인 JSON 데이터
    • Date, Map, Set, RegExp 객체
    • 커스텀 클래스의 인스턴스 (new MyClass())
    • svelte 컴포넌트 생성자
    • 함수

`Server load`함수의 반환 값

  • Server load는 서버에서 실행되고, 그 결과는 네트워크를 통해 브라우저로 전송된다. 이 과정에서 메모리에 있는 복잡한 데이터 구조는 네트워크로 보낼 수 있는 텍스트 형태로 변환되어야 하는데, 이 변환 과정을 직렬화 라고 한다.
  • sveltekit은 JSON.stringify 보다 강력한 devalue라는 라이브러리를 사용하여 직렬화를 수행한다.
    • JSON 으로 표현 가능한 모든 것
    • BitInt
    • Date
    • Map
    • Set
    • RegExp
    • 순환 참조 또는 반복 참조가 있는 객체
    • undefined
    • Promise
// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
	return {
		serverMessage: 'hello from server load function'
	};
};
// src/routes/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ data }) => {
	return {
		serverMessage: data.serverMessage,
		universalMessage: 'hello from universal load function'
	};
};

URL Data

  • load 함수는 URL에 따라 기능이 달라 질수 있다. 이를 위해 load함수는 url, route, params 데이터를 제공한다.

params

  • params는 동적 라우트 세그먼트의 값을 담고 있는 객체이다. 파일이나 폴더 이름을 대괄호로 묶어 생성한 동적 경로의 실제 값에 접근할 때 사용한다.
  • 주요 사용 사례
    • 특정 ID에 해당 하는 데이터를 가져올 때.
// 파일 경로 : src/routes/blog/[post]/+page.js
// 사용자 접속 URL : /blog/posting1

// params 객체
{
	'post': 'posting1'
}
---

// 파일 경로 : src/route/blog/[post]/[...tag]
// 사용자 접속 URL : /blog/posting2/js/svelte

// params 객체
{
	'post': 'posting2',
	'tag': 'js/svelte'
}

URL

  • 현재 페이지의 URL에 대한 모든 정보를 담고 있는 Javascript URL 객체 이다. params가 경로의 일부만 제공하는 것과 달리 url은 URL에 대한 접근을 제공한다.
  • 주요 사용 사례
    • 쿼리 파라미터 가져오기 (url.searchParams(...))
    • 전체 경로가 필요한 경우
  • URL 객체
URL {
  	href: 'http://localhost:5173/test?a=1&b=2',
  	origin: 'http://localhost:5173',
  	protocol: 'http:',
  	username: '',
  	password: '',
  	host: 'localhost:5173',
  	hostname: 'localhost',
  	port: '5173',
  	pathname: '/test',
  	search: '?a=1&b=2',
  	searchParams: URLSearchParams { 'a' => '1', 'b' => '2' },
  	hash: ''
}
// 파일 경로 : src/route/post/+page.ts
// 사용자 접속 URL : /post?category=javascript&limit=10

url.searchParams.get('category') // javascript
url.searchParams.get('limit') // 10

route

  • route는 현재 요청과 일치하는 sveltekit의 라우트 패턴 정보를 담고 있는 객체이다. 실제 URL 경로가 아닌, sveltekit이 내부적으로 사용하는 라우드 ID에 접근할 때 사용한다.
  • 주요 사용 사례
    • 캐싱(caching) : 라우트 기반으로 캐시 키를 생성할 때
    • 분석(analytics) : 어떤 라우트 패턴이 사용되었는지 추적할 때
    • 레이아웃 분기 처리 : 특정 라우트 그룹에 따라 다른 레이아웃 로직을 적용하고 싶을 때
    • paramsurl에 비해 사용 빈도는 낮지만, 고급 제어가 필요할 때 유용하다.
// 파일 경로 : src/route/blog/[slug]/+page.js
// 사용자 접속 URL : /blog/posting1

{
	'id': '/blog/[slug]'
}