이제 앱에 로그인과 권한 제어를 붙여 봅니다.
시작 전에 정리하기
- 무엇을 만든다: 세션 쿠키를 읽어
locals.user에 심고, 보호된(app)레이아웃에서 인증 여부를 검사하는 흐름. - 왜 중요한가: API 통합 이후부터는 사용자별 데이터를 다루게 되므로, 인증 상태를 중앙에서 공유해야 실수가 줄어듭니다.
- 주의할 점:
handle훅에서 비동기 작업이 실패하면 전체 요청이 막히므로, 토큰 검증 오류를 예외 대신null처리로 흡수해야 합니다.
이번 글에서 새로 나오는 용어
- 세션: 서버가 발급하는 로그인 토큰으로, 쿠키에 저장해 두면 새로고침 후에도 사용자 정보를 기억할 수 있습니다.
- 쿠키: 브라우저가 보관하는 작은 데이터 조각으로,
cookies.set으로 세션 토큰을 남길 수 있습니다. - 라우트 가드: 로그인 여부나 권한을 확인해 특정 페이지나 레이아웃 접근을 막는 규칙입니다.
- event.locals: 서버 훅과 로드 함수 사이에서 공유되는 객체로, 여기에
user정보를 넣어 전역으로 전달합니다.
개념
세션(session)은 서버가 사용자 상태를 기억하기 위해 발급하는 토큰입니다. 쿠키에 세션 토큰을 저장하면 페이지를 새로고침해도 인증 상태가 유지됩니다. 라우트 가드(route guard, 경로 보호 규칙)는 인증 여부를 확인해 허용된 사용자만 특정 레이아웃이나 페이지를 보게 만드는 장치입니다. event.locals는 SvelteKit이 서버 훅에서 저장한 데이터를 모든 로드 함수에서 공유하게 해 줍니다. 세션 정보를 전달하기에 알맞은 자리입니다.
D2로 보는 인증 흐름
👉 미니 요약: 모든 요청은 먼저 hooks.server.ts를 거쳐 쿠키에서 사용자를 찾습니다. 찾은 결과는 event.locals.user로 넘어가고, 보호된 레이아웃이 없으면 로그인 화면으로 돌려보냅니다. 로그인 액션은 쿠키를 새로 써 준 뒤 원래 가려던 페이지로 다시 이동시킵니다.
코드
서버 훅, 레이아웃 가드, 로그인 액션 순서로 살펴봅니다.
// src/hooks.server.ts
const attachUser = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (!token) {
event.locals.user = null;
return resolve(event);
}
try {
event.locals.user = await getUserByToken(token);
} catch (error) {
console.error('토큰 검증 실패', error);
event.locals.user = null;
event.cookies.delete('session', { path: '/' });
}
return resolve(event);
};
export const handle = sequence(attachUser);
// src/routes/(app)/+layout.server.ts
export const load = async ({ locals, url }) => {
if (!locals.user) {
throw redirect(303, `/login?redirectTo=${encodeURIComponent(url.pathname)}`);
}
return { user: locals.user };
};
<!-- src/routes/(app)/+layout.svelte -->
<script lang="ts">
export let data;
</script>
<AppShell user={data.user}>
<slot />
</AppShell>
// src/routes/login/+page.server.ts
export const load = ({ locals }) => {
if (locals.user) {
throw redirect(303, '/dashboard');
}
return {};
};
export const actions = {
default: async ({ request, cookies, url }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
const token = await authenticate(String(email), String(password));
if (!token) {
return fail(400, { message: '이메일 또는 비밀번호를 확인하세요.' });
}
cookies.set('session', token, { path: '/', httpOnly: true, maxAge: 60 * 60 * 24 * 7 });
const redirectTo = url.searchParams.get('redirectTo') ?? '/dashboard';
throw redirect(303, redirectTo);
}
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
const errors = derived(page, ($page) => $page.form?.message);
</script>
<form method="POST" use:enhance>
<label>
이메일
<input name="email" type="email" autocomplete="email" required />
</label>
<label>
비밀번호
<input name="password" type="password" autocomplete="current-password" required />
</label>
{#if $errors}
<p role="alert">{$errors}</p>
{/if}
<button type="submit">로그인</button>
</form>
<form method="POST" action="?/logout" class="logout">
<button type="submit">로그아웃</button>
</form>
// src/routes/(app)/+layout.server.ts (logout action 추가)
export const actions = {
logout: async ({ cookies }) => {
cookies.delete('session', { path: '/' });
return { success: true };
}
};
💬 댓글
이 글에 대한 의견을 남겨주세요