FE
[SvelteKit] Firebase Authentication 로그인 후 세션 유지(Session Persistence)
| 서론
안녕하세요 팡일입니다.
지난 포스팅에서는 로그인과 회원가입 기능을 구현했는데요?
단순한 로그인 기능을 넘어서, 이제는 세션을 유지할 수 있는 단계까지 구현하고 싶었습니다.
어떻게 하면 세션을 유지할 수 있을지에 대한 과정을 공유해보려고 합니다.
| 시작 전에 알고 가면 좋을 내용
1) Firebase Auth가 기본적으로 하는일
먼저, Firebase Authentication은 자동으로 세션(토큰)을 관리합니다.
즉, 로그인을 성고하면 내부적으로 다음 세 가지를 처리합니다.
(1) idToken : 서버 요청 인증용 JWT(1시간 유효)
(2) refreshToken : 만료된 idToken을 자동 갱신 (브라우저에 저장)
(3) persistence : 세션 유지 방식(local, session, none) 제어 가능
2) 세션 유지 모드 설정 (setPersistence)
Firebase는 로그인 유지 방식을 직접 지정할 수 있습니다.
import { setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth';
import { auth } from '$lib/firebase';
// 브라우저 로컬에 로그인 정보 저장 → 새로고침/재접속 후에도 유지됨
await setPersistence(auth, browserLocalPersistence);browserLocalPersistence 로컬 스토리지에 저장. 브라우저를 닫아도 유지 (기본 추천)
browserSessionPersistence 세션 스토리지에 저장. 탭 닫으면 사라짐
inMemoryPersistence 메모리에만 저장. 새로고침하면 사라짐
저는 이 중에서 browserLocalPersistence를 설정하여, 로컬 스토리지에 저장할 수 있게끔 진행하였습니다.
| 로그인 후 세션 유지 적용
로그인을 진행하는 함수에 await setPersistence를 추가합니다.
import {
signInWithPopup,
signInWithEmailAndPassword,
setPersistence,
browserLocalPersistence
} from 'firebase/auth';
import { auth, provider } from '$lib/firebase';
export async function loginWithEmail(email: string, password: string) {
try {
// ✅ 세션 유지 설정 (localStorage 기반)
await setPersistence(auth, browserLocalPersistence);
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return { success: true, user: userCredential.user };
} catch (error) {
console.error('로그인 오류:', error);
return { success: false, error };
}
}
export async function loginWithGoogle() {
try {
// ✅ 동일하게 세션 유지 설정
await setPersistence(auth, browserLocalPersistence);
const result = await signInWithPopup(auth, provider);
const user = result.user;
return { success: true, user };
} catch (error) {
console.error('구글 로그인 오류:', error);
return { success: false, error };
}
}Firebase Auth의 setPersistence()는 '앞으로 생성될 로그인 세션이 어디에 저장될지를 미리 저장하는 함수'입니다.
즉, Persistence(저장 방식)는 로그인 이전에 설정해야,
signInWithEmailAndPassword()가 그 방식에 맞게 세션 토큰을 지정할 수 있습니다.
이를 쉽게 비유해보면,
“지금 로그인 세션을 어디에 저장할지 먼저 결정(setPersistence)한 다음,로그인(signIn)해야 Firebase가 그 장소(localStorage, sessionStorage 등)에 토큰을 저장할 수 있다.”
| 트러블슈팅 : 비동기로 인한 redirect로 세션 유지 실패
처음 구현했을 때, 로그인 후 새로고침하면 세션이 유지되지 않고 /login으로 리다이렉트되는 문제가 있었습니다.
원인을 확인해보니, Firebase가 로그인 정보를 localStorage에 저장하긴 하지만,
세션 복원(onAuthStateChanged) 과정이 비동기적으로 처리되기 때문에 문제가 발생했습니다.
SvelteKit에서는 새로고침 시 SSR(서버 렌더링) 이 먼저 실행되는데,
이 시점에는 아직 Firebase Auth가 초기화되지 않아 auth.currentUser 값이 null로 남습니다.
그 결과, +layout.ts의 가드가 “아직 로그인 안 됐네?”라고 판단하고 /login으로 리다이렉트하게 되는 상황이었습니다.
그래서 +layout.ts에서 Firebase 세션 복원이 완료될 때까지 기다리도록 수정했습니다.
// src/routes/(private)/+layout.ts
import { redirect } from '@sveltejs/kit';
import { auth } from '$lib/firebase';
import { onAuthStateChanged } from 'firebase/auth';
export const load = async () => {
if (typeof window === 'undefined') {
return {};
}
const user = await new Promise((resolve) => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
unsubscribe();
resolve(user);
});
});
if (!user) {
throw redirect(307, '/login');
}
return {};
};이렇게 하면 새로고침 후에도 Firebase의 세션 복원(onAuthStateChanged)이 끝난 다음에만 redirect를 판단하기 때문에, 정상적으로 로그인 상태가 유지됩니다.
| 트러블슈팅 : guard가 있는데, 로그인 없이 /list 접근 가능
처음에는 guard를 걸어두었음에도, 주소창에 /list를 직접 입력하면 로그인 없이 접근이 가능했습니다.
원인을 파악해보니, SvelteKit은 모든 라우트를 동일한 구조로 관리하기 때문에, “공개 페이지”와 “로그인 보호 페이지”를 명확히 분리하지 않으면 라우팅 단위에서 guard가 제대로 동작하지 않았습니다.
즉, 모든 페이지가 하나의 +layout.svelte 아래에 있었기 때문에 “로그인이 필요한 페이지”임을 구분할 수 없었습니다.
그래서 라우트 구조를 (public) / (private) 두 개로 분리했습니다.
src/routes/
├── (public)/
│ ├── +layout.svelte # 로그인 불필요
│ ├── +page.svelte # 홈
│ ├── login/+page.svelte
│ └── register/+page.svelte
└── (private)/
├── +layout.svelte # 로그인 필요 (헤더 + 로그아웃 버튼)
├── +layout.ts # Firebase Auth guard
├── list/+page.svelte
├── detail/[id]/+page.svelte
└── write/+page.svelte이제 /list, /write, /detail/... 등은
자동으로 (private) 그룹 아래에 포함되기 때문에 Firebase 인증이 없으면 guard에서 /login으로 리다이렉트됩니다
| 결론
이번 과정을 통해 Firebase Authentication의 세션 관리 구조를 명확히 이해할 수 있었습니다.
세션을 유지하려면 setPersistence()를 로그인 이전에 설정해야 하고,
새로고침 시에는 Firebase의 세션 복원이 끝날 때까지 기다려야 하며,
공개 페이지와 보호 페이지는 라우트 그룹을 분리해 관리해야 한다는 점을 배웠습니다.
지금은 로그인, 회원가입, 세션 유지까지 안정적으로 작동합니다.
다음 포스팅에서는 “로그아웃” 및 “프로필 표시” 기능을 추가하면서 사용자 경험을 더 다듬어보겠습니다.