sveltekit API 요청
- 외부 API 나
+server.ts핸들러에서 데이터를 가져 오려면 sveltekit에서 제공되는fetch함수를 사용한다. sveltekit에서 제공된fetch함수는 몇 가지 추가 기능을 제외하면 기본 웹 fetch API 와 동일하게 작동한다.
서버에서 인증된 요청 가능
- 사용자가 브라우저를 통해 페이지를 요청할 때, 브라우저는 Cookie 헤더(로그인 세션 정보 등)나 Authorization 헤더(API 토큰 등)를 함께 보냅니다.
load함수 내에서 fetch를 사용하여 앱 내부의 다른 API 엔드 포인트를 호출하면, sveltekit은 원래의 페이지 요청에 포함되었던 cookie와 Authorization 헤더를 자동으로 복사하여 새로운 요청에 활용한다.
- 사용자가 브라우저를 통해 페이지를 요청할 때, 브라우저는 Cookie 헤더(로그인 세션 정보 등)나 Authorization 헤더(API 토큰 등)를 함께 보냅니다.
서버에서 상대 경로 요청 가능
- 일반적으로 API 호출시
https://api.example.com/data와 같이 전체 URL을 명시해야 하지만 sveltekit은 앱의 origin(http://localhost:5173)을 알고 있기에 fetch('/api/data')와 같이 상대 경로를 사용할 수 있다.
- 일반적으로 API 호출시
내부 요청 시 HTTP 오버 헤드 없음
- 서버에서 실행되는
load함수가 자신의 앱 내부에 있는+server.tsAPI 라우트를 fetch 할 때, sveltekit은 네트워크 HTTP 요청을 거치지 않고 해당 핸들러 함수(GET,POST등)을 직접 호출하고 그 결과를 받아 온다.
- 서버에서 실행되는
서버 사이드 렌더링시 응답이 HTML 에 포함됨
- 서버 사이드 렌더링 과정에서
load함수가 fetch를 실행하면 sveltekit은 응답을 생성되는 HTML 문서의<script type="application/json">과 같은 태그 안에 텍스트 형태로 직접 삽입한다.
- 서버 사이드 렌더링 과정에서
하이드레이션 시 데이터 재사용
- 4번과 관련하여 브라우저가 서버로 부터 받은 HTML과 Javascript를 실행하여 하이드레이션 하는 과정에서
load함수가 실행될 시 4번 과정에서 삽입된 데이터가 있는지 먼저 확인 후 있을 경우 데이터를 활용하고 없을 경우fetch를 실행한다.
- 4번과 관련하여 브라우저가 서버로 부터 받은 HTML과 Javascript를 실행하여 하이드레이션 하는 과정에서
Cookies
서버의
load기능은cookies를 가져오고 설정할 수 있지만 보안 정책으로 요청을 보내는 대상(host)이 현재 sveltekit 애플리케이션과 동일한 도메인이거나 하위 도매인 (subdomain)일 경우에만 사용자의 쿠키를 자동으로 요청에 포함시켜 전달한다.sveltekit이
https://my.app.com에서 실행 된다면 아래와 같이 정책이 적용된다쿠키가 전달 되는 경우
- 동일한 호스트
fetch('/api/userinfo') / https://my.app.com/api/userinfo- 앱 내부의 API 호출하는 것은 신뢰할 수 있는 통신으로 간주되어 쿠키가 전달된다.
- 하위 도메인
fetch('https://api.my.app.com/data')api.myapp.com은myapp.com의 하위 도메인이므로, 같은 애플리메이션 인프라의 일부로 간주하여 쿠키가 전달된다.
- 동일한 호스트
쿠키가 전달 되지 않는 경우
- 완전히 다른 외부 호스트
fetch('https://api.your.app.com/data')- 제 3자의 도메인에 쿠키 정보를 유출하지 않기 위한 핵심적인 보안 조치로 쿠키가 전달 되지 않는다.
- 상위 도메인
fetch('app.com')- 더 구체적인 하위 도메인이 아닌 상위 도메인으로의 요청은 다른 서비스로 간주되어 신뢰 경계를 벗어난 것으로 판단, 쿠키가 전달 되지 않는다.
- 완전히 다른 외부 호스트
만약 외부 API에 인증 정보(API 키 등)를 보내야 한다면 수동으로 명시하여 헤더에 담아 보내야 합니다.
Headers
load함수가 서버에서 실행 될 때, 해당 페이지의 HTTP 응답 헤더를 설정하기 위한setHeaders함수를 제공합니다.setHeaders함수는 응답에 헤더를 제어하는 sveltekit의 공식적인 방법이지만 브라우저에서 실행시 아무런 효과가 없습니다.- 주요 사용 사례로는 페이지 캐싱(caching) 제어를 위한
Cache-Control설정이다.
setHeaders({
'Cache-Control': 'public, max-age=60'
// 'Content-Language', 'ETag' 등
})- 동일한 헤더를 별도의
load함수에서 여러번 설정하는 경우 에러가 발생한다. setHeaders 함수를 사용하여 한번만 설정 할 수 있다. set-cookie를 사용하여 헤더를 추가할 수 없다. 대신cookies.set(name, value, options)를 사용한다.
Errors
load함수 실행 중 오류가 발생하면 가장 가까운+error.svelte가 렌더링 된다.@sveltejs/kit의error헬퍼를 사용하면 HTTP 상태 코드와 상태 메시지를 지정할 수 있다.
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals }) => {
if (!locals.user) {
error(401, 'not logged in');
}
if (!locals.user.isAdmin) {
error(403, 'not an admin');
}
};- 예상 하지 못한 오류가 발생한 경우 sveltekit은 서버 콘솔에 오슈 스택 트레이스를 기록하고 사용자에세는 일반적인 500 error 메시지를 보여준다. 그리고 오류를 최종적으로 처리 하기 전에
src/hooks.server.ts - handleError함수를 호출한다.handleError는 예상 하지 못한 모든 서버 오류에 대한 글로벌 오류 처리기로 이 훅을 사용하여 오류 처리 방식을 커스터 마이징 할 수 있다.
Redirects
- 사용자를 리다이렉션 하기위해선
@sveltejs/kit의redirect헬퍼를 사용하여3XX상태 코드와 함께 리다이렉션 위치를 지정한다. error(...)와 마찬가지로redirect(...)를 호출하면 예외가 발생하여 내부 헬퍼 함수 내부에서 실행을 쉽게 중지 할 수 있다.
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals }) => {
if (!locals.user) {
redirect(307, '/login'); // redirect 함수는 예외 처리 블럭(try - catch) 외부에 작성해야 한다. 내부에서 사용시 catch문이 실행된다.
}
};- 브라우저에서는
$app.navigation의goto함수를 사용하여load함수 외부에서 페이지 이동 및 리다이렉션이 가능하다.
Promise 기반 스트리밍
- 일반적인 SSR에서는 페이지에서 요구하는 모든 데이터가 준비 될때까지 HTML 페이지 렌더링을 대기하다보니 데이터 수신 속도가 느린 데이터로 인해 페이지의 로딩이 지연된다는 문제점이 있었다. sveltekit에서는 이 문제를 해결하기 위해 server
load함수에서 Promise를 직접 반환하는 것이 가능하다. - 동작 방식 -
+layout.server.ts,+layout.svelte에서도 동일하게 작동한다.- 서버 (
+page.server.ts)의load함수 내에서 여러 데이터를 요청할때 빠르고 필수 적인 데이터는await를 사용하여 즉시 값을 얻고 느리고 부가적인 데이터는await없이Promise 객체자체를 반환한다. sveltekit은await된 데이터만 기다린 후, 페이지의 기본 HTML을 브라우저로 전송하고 서버에서 Promise가 완료 되면 브라우저로 HTTP 스트림을 통해 데이터를 streaming 한다. - 클라이언트 (
+page.svelte)의 페이지에서는 svelte의 내장 기능은{#await}블록을 사용하여 Promise를 처리한다.
- 서버 (
// .server.ts
return {
post: await postPromise,
streamed: {
comments: commentsPromise
}
}// .svelte
{#await data.streamed.comments}
// 대기 중 UI
{:then comments}
// 수신 완료 시 UI
{:catch error}
// 수신 실패 시 UI
{/await}병렬 처리
- 클라이언트는 페이지 로딩 또는 이동 - 렌더링시 모든
.server.ts에 요청을 보내지 않고 브라우저에서 서버로 하나의 특별한 요청만 보낸다. - sveltekit은 페이지를 렌더링 하는데 필요한 모든
load함수 들을 미리 파악하여 동시에 실행을 시작한다. - 실행이 완료 되면 서버는 모두 모아 하나의 큰 JSON 객체로 묶어 브라우저로 응답한다.
`load`함수 의존성 자동 추적
- sveltekit은 각
load함수가 어떤 데이터에 의존하는지 자동으로 추적한다. 그리고 네비게인션이 발생했을 때, 의존하는 데이터가 변경된load함수만 다시 실행한다. - sveltekit은
load함수의 인자로 전달되는 객체들의 사용을 지켜보며 의존성을 파악한다.params객체load함수가param.id를 사용했다면 이load함수는 id 파라미터에 의존하게 되며 URL 의 id값이 변경될때 다시 실행된다.- 만약 param 객체 전체를 사용했다면 내부 데이터 하나라도 변경될시 다시 실행된다.
url객체load함수가 url.searchParams를 사용했다면 URL 의 쿼리 스트링이 변경될 때 다시 실행된다.url.pathname을 사용했다면, 경로가 변경될 때 다시 실행된다.
parent함수load함수가await parent()를 호출했다면, 부모load함수의 결과 데이터에 의존하게 되어 부모load함수가 다시 실행되면 결과 값이 바뀌면 다시 실행된다.
종속성 추적 해제
untrack()함수는 sveltekit의 자동 의존성 추적 시스템에서 의도적으로 특정 의존성을 제외 하고 싶을떄 사용한다.untrack()함수는 콜백 함수를 인자로 받는데untrack의 콜백 함수 내에서 접근하는 모든 의존성은 sveltekit의 의존성 추적기에서 무시 된다.
수동 의존성 관리 - `depends()`, `invalidate()`
- URL에 나타나지 않는 쿠키에 저장된 테마 정보나, 폼 제출(action)을 통해 변경 되는 데이터에 의존하는 경우 sveltekit은 수동으로 의존성을 관리할 수 있다.
depends()와invalidate()는 하나의 쌍(구독과 발행)으로 함께 사용되며 수동 의존성 관리가 올바르게 동작한다.depends(식별자)load함수 내에서 호출하며,load함수가 특정 데이터에 의존한다고 명시적으로 표시한다.depends('app:theme')나depends('data:posts')와 같이 자유롭게 정할수 있다.
invalidate(식별자)- 주로
+page.svelte컴포넌트 내부나action의 결과로 호출되며invalidate('app:theme')가 호출되면,depends('app:theme')를 사용했던 모든load함수들은 다음 네비게이션 시 강제로 다시 실행된다.
- 주로
`load`함수가 다시 실행되는 경우 정리
데이터 로딩시 인증 측면에서의 고려사항
- sveltekit에서는
+layout.server.ts의load함수에 인증 로직을 넣는 것만으로는 충분하지 않다.- layout의
load함수는 항상 실행되지 않는다.load함수는 의존하는 데이터가 변경되지 않으면 성능을 위해 재실행을 하지 않는다. 만약 사용자의 세션이 만료되었더라도,load함수가 재실행되지 않으며 인증 체크 로직을 건너뛰게 되어 보호된 페이지나 데이터에 접근하는 보안 사고가 발생할 수 있다.
load함수는 병렬로 실행된다.+layout.server.ts의load가 인증 체크를 실패했을 시 redirect를 throw 하지만 동시에+page.server.ts의load가 실행을 시작하며 redirect가 완전치 처리 되기 전에 데이터베이스에서 민감한 정보를 조회할 수 있다.
- layout의
올바른 인층 처리 전략
Hooks 사용 (권장)
- 모든 서버 요청에 대해 라우팅 및
load함수가 실행되기 전에 실행되는 중앙 미들웨어src/hooks.server.ts파일의 handle 함수를 사용한다.handle함수 내에서 사용자의 쿠키를 확인하여 인증 상태를 확인한 후, 보호된 경로에 접근하는데 인증되지 않았다면,load가 실행되기 전에 redirect를 throw하여 처리한다. 인증 되었다면 사용자 정보를event.locals에 담아 이후 실행될load함수나 API 엔드 포인트에서 쉽게 사용할 수 있도록 한다.
- 장점
- 중앙 집중 - 인증 로직이 한 곳에 모여 관리가 편하다.
- 완벽한 보호 - 모든 요청에 대해 가장 먼저 실행 되므로, 기존 문제점을 모두 원칙적으로 차단한다.
- 효율성 -
event.locals를 통해 사용자 정보를 한번만 조회하고 재사용할 수 있다
- 모든 서버 요청에 대해 라우팅 및
각
+page.server.ts에서 직접 확인- 보호가 필요한 모든
+page.server.ts의load함수 시작 부분에서 직접 인증 체크 로직을 넣는다. - 장점 - 어떤 페이지가 보호되는지 매우 명시적이고, 특정 페이지에만 특별한 권한을 부여할 경우 유용하다.
- 단점 - 코드 중복과 유지보수가 복잡해진다.
- 보호가 필요한 모든
+layout.server.ts+await parent()+layout.server.ts에 인증 체크 로직을 넣고, 그 아래 모든 자식+page.server.ts들이load함수 시작 부분에서await parent()를 반드시 호출 하도록 강제한다.- 장단점 - 설명 안한다
getRequestEvent 사용
getRequestEvent는 sveltekit에서 제공하는 고급 기능으로, 서버 측 코드의 어느 곳에서나 현재 요청에 대한 RequestEvent 객체에 접근할 수 있게 해주는 함수이다.일반적으로
RequestEvent객체는hooks.server.ts의handle함수나+server.ts의 핸들러, +page.server.ts의load및actions함수의 인자로 직접 전달된다. 하지만 때로는 이렇게 전달 받은event객체를 여러 함수를 거쳐 깊숙한 곳에 있는 유틸리티 함수나 모듈까지 전달해야 하는prop drilling문제가 발생하는에 이러한 문제를 해결하기 위해getRequestEvnet가 사용된다.제약 사항
- 서버 전용 : 서버측 코드에서만 사용할 수 있다.
- 요청 생명 주기 내에서만 유효 : 요청 처리의 동기적인 실행 흐름 내에서만 호출되어야 하며
setTimeout이나setInterval의 콜백 함수 처럼 요청 컨텍스트가 사라진 비동기적인 환경에서는 null이 반환된다.
// src/lib/server/auth.ts
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(307, `/login?${params}`);
}
return locals.user;
}// +page.server.ts
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}!`
};
}