0340

FE

[SvelteKit] Firebase의 Authentication으로 사용자 인증 관리하기

| 서론

안녕하세요! 팡일입니다.

대부분의 사이트에서는 '회원가입'과 '로그인' 과정을 거쳐 사용자를 인증하고 식별한 뒤 서비스를 이용할 수 있게 하는데요?

현재 제가 제작중인 re:log 서비스에서도 사용자 인증 과정이 필요하여, 해당 기능을 구현하고자 Firebase의 Authentication 서비스를 이용해보려고 합니다.

그래서 오늘은 Firebase의 Authentication으로 사용자 인증 관리하는 방법을 공유해보려고 합니다.

| 1단계 : 공급업체 설정하기

1) "이메일/비밀번호" 로그인 방법

(1) 좌측 메뉴 중에서 '빌드'를 선택하여, 'Authentication'을 클릭해줍니다.

(2) 클릭한 이우헤는 '시작하기' 버튼을 클릭합니다.

(3) 원하는 로그인 방법을 사용하여 Firebase 인증을 시작합니다.

: 현재 저는 '이메일/비밀번호' 로그인 방법을 먼저 진행해보려고 합니다.

(4) 이메일/비밀번호를 모두 사용할지, 비밀번호 없이 이메일만 사용할지 선택합니다.

: 현재 저는 이메일/비밀번호 모두 사용하기 위해 첫 번째를 설정했습니다.

(5) 설정 후 '저장' 버튼을 누릅니다.

(6) 이런식으로 성공적으로 이메일/비밀번호를 사용한 로그인 방법을 설정할 수 있습니다.

2) "구글로그인" 로그인 방법

(1) 우측의 '새 제공업체 추가' 버튼을 클릭합니다.

(2) Google 버튼을 클릭합니다.

(3) 프로젝트 지원 이메일을 선택합니다.

  • 해당 부분은 Firebase가 제공하는 Google 로그인 화면이나 비밀번호 재설정 메일에서,
    사용자에게 "문의사항이 있으면 이 이메일로 연락하세요" 라고 표시되는 부분입니다.

(4) '저장' 버튼을 클릭합니다.

(5) 성공적으로 설정이 된 것을 볼 수 있습니다.

| 2단계 : SvelteKit Project

성공적으로 Firebase Console에서 로그인 공급업체를 설정했다면,

이제 SvelteKit Project에서 반영할 차례입니다.

1) Firebase 초기 설정 (lib/firebase.ts)

TypeScript
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
	apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
	authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
	projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
	storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
	messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
	appId: import.meta.env.VITE_FIREBASE_APP_ID
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const provider = new GoogleAuthProvider();
export const db = getFirestore(app);
  • auth와 provider 부분을 추가합니다.

2) userService 생성 (/li/services/userService.ts)

TypeScript
// src/lib/services/userService.ts
import {
	createUserWithEmailAndPassword,
	signInWithEmailAndPassword,
	signInWithPopup,
	updateProfile
} from 'firebase/auth';
import { doc, setDoc, serverTimestamp, getDoc } from 'firebase/firestore';
import { auth, db, provider } from '$lib/firebase';

export async function registerWithEmail(name: string, email: string, password: string) {
	try {
		const userCredential = await createUserWithEmailAndPassword(auth, email, password);
		const user = userCredential.user;

		await updateProfile(user, { displayName: name });

		await setDoc(doc(db, 'users', user.uid), {
			uid: user.uid,
			displayName: name,
			email: user.email,
			createdAt: serverTimestamp()
		});

		return { success: true, user };
	} catch (error) {
		console.error('회원가입 오류:', error);
		return { success: false, error };
	}
}

export async function loginWithEmail(email: string, password: string) {
	try {
		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 {
		const result = await signInWithPopup(auth, provider);
		const user = result.user;

		const userRef = doc(db, 'users', user.uid);
		const docSnap = await getDoc(userRef);

		if (!docSnap.exists()) {
			await setDoc(userRef, {
				uid: user.uid,
				displayName: user.displayName,
				email: user.email,
				createdAt: serverTimestamp()
			});
		}

		return { success: true, user };
	} catch (error) {
		console.error('구글 로그인 오류:', error);
		return { success: false, error };
	}
}
  • registerWithEmail : 이메일과 비밀번호를 사용하여 회원가입을 진행합니다.

  • loginWithEmail : 이메일과 비밀번호를 사용하여 로그인을 진행합니다.

  • loginWithGoogle : 구글 로그인을 사용하여 회원가입과 로그인을 진행합니다.

3) 로그인 페이지 (routes/login/+page.svelte)

TypeScript
<script lang="ts">
	import { loginWithEmail, loginWithGoogle } from '$lib/services/userService';
	import { goto } from '$app/navigation';
	import { getErrorMessage } from '$lib/utils/firebaseError';

	let email = '';
	let password = '';
	let error: string | null = null;
	let loading = false;

	async function handleLogin() {
		if (!email || !password) {
			error = '이메일과 비밀번호를 입력해주세요.';
			return;
		}

		loading = true;
		const { success, error: err } = await loginWithEmail(email, password);
		loading = false;

		if (success) goto('/list');
		else error = getErrorMessage(err);
	}

	async function handleGoogleLogin() {
		const { success, error: err } = await loginWithGoogle();
		if (success) goto('/list');
		else error = getErrorMessage(err);
	}
</script>

<div class="auth-container">
	<h2>로그인</h2>

	{#if error}<p class="error">{error}</p>{/if}

	<input type="email" placeholder="이메일" bind:value={email} />
	<input type="password" placeholder="비밀번호" bind:value={password} />

	<button on:click={handleLogin} disabled={loading}>
		{loading ? '로그인 중...' : '로그인'}
	</button>

	<button on:click={handleGoogleLogin} class="google-btn">Google로 로그인</button>

	<p class="link">아직 회원이 아니신가요? <a href="/register">회원가입</a></p>
</div>

<style>
	.auth-container {
		max-width: 400px;
		margin: 4rem auto;
		display: flex;
		flex-direction: column;
		gap: 0.8rem;
	}
	input {
		padding: 0.8rem;
		border: 1px solid #ccc;
		border-radius: 8px;
	}
	button {
		padding: 0.8rem;
		border: none;
		border-radius: 8px;
		cursor: pointer;
		background-color: #4a6cf7;
		color: white;
	}
	.google-btn {
		background-color: #db4437;
	}
	.error {
		color: red;
		font-size: 0.9rem;
	}
	.link {
		margin-top: 1rem;
		text-align: center;
	}
</style>
  • getErrorMessage : 에러 코드 별 사용자 메세지 매핑을 진행합니다.

  • handleLogin() : 입력된 이메일과 비밀번호를 가지고 loginWithEmail을 요청하며, 로그인 후 /list로 이동합니다.

  • handleGoogleLogin() : 구글 로그인을 요청하며, 로그인 후 /list로 이동합니다.

4) 회원가입 페이지 (routes/register/+page.svelte)

TypeScript
<script lang="ts">
	import { registerWithEmail, loginWithGoogle } from '$lib/services/userService';
	import { goto } from '$app/navigation';
	import { getErrorMessage } from '$lib/utils/firebaseError';
	import { validatePassword } from '$lib/utils/validatePassword';

	let name = '';
	let email = '';
	let password = '';
	let confirmPassword = '';
	let error: string | null = null;
	let loading = false;

	async function handleRegister() {
		if (!name || !email || !password || !confirmPassword) {
			error = '모든 필드를 입력해주세요.';
			return;
		}

		const pwError = validatePassword(password);
		if (pwError) {
			error = pwError;
			return;
		}

		if (password !== confirmPassword) {
			error = '비밀번호가 일치하지 않습니다.';
			return;
		}

		loading = true;
		const { success, error: err } = await registerWithEmail(name, email, password);
		loading = false;

		if (success) goto('/list');
		else error = getErrorMessage(err);
	}

	async function handleGoogleRegister() {
		const { success, error: err } = await loginWithGoogle();
		if (success) goto('/list');
		else error = getErrorMessage(err);
	}
</script>

<div class="auth-container">
	<h2>회원가입</h2>

	{#if error}<p class="error">{error}</p>{/if}

	<input type="text" placeholder="이름" bind:value={name} />
	<input type="email" placeholder="이메일" bind:value={email} />
	<input type="password" placeholder="비밀번호" bind:value={password} />
	<input type="password" placeholder="비밀번호 확인" bind:value={confirmPassword} />

	<button on:click={handleRegister} disabled={loading}>
		{loading ? '가입 중...' : '회원가입'}
	</button>

	<button on:click={handleGoogleRegister} class="google-btn">Google로 회원가입</button>

	<p class="link">이미 계정이 있으신가요? <a href="/login">로그인</a></p>
</div>

<style>
	.auth-container {
		max-width: 400px;
		margin: 4rem auto;
		display: flex;
		flex-direction: column;
		gap: 0.8rem;
	}
	input {
		padding: 0.8rem;
		border: 1px solid #ccc;
		border-radius: 8px;
	}
	button {
		padding: 0.8rem;
		border: none;
		border-radius: 8px;
		cursor: pointer;
		background-color: #4a6cf7;
		color: white;
	}
	.google-btn {
		background-color: #db4437;
	}
	.error {
		color: red;
		font-size: 0.9rem;
	}
	.link {
		margin-top: 1rem;
		text-align: center;
	}
</style>
  • validatePassword : 비밀번호 유효성을 검증합니다.

  • handleRegiser() : 입력된 회원가입 정보를 검증합니다.

  • handleGoogleRegister : 구글 로그인으로 회원가입을 진행합니다.

| 트러블슈팅 공유

1) 비밀번호 정책 위반 시, 발생한 오류

회원가입 기능을 구현하는 과정 속에서 아래와 같은 오류를 발견했습니다.

메세지를 확인해보니, Firebase Authentication에서 비밀번호 정책을 위반했을 때 발생하는 대표적인 오류였습니다.

이는 클라이언트에서 createUserWithEmailAndPassword() 함수를 호출했을 때, Firebase가 자동으로 검증을 해주는 단계에서 나옵니다.

조금 더 구체적으로 보면 해당 오류는 이메일/비밀번호 회원가입 시, 입력한 비밀번호 길이가 6자 미만일 때 발생한 오류였습니다.

이 부분을 두고, 정책 위반에 대한 오류를 createUserWithEmailAndPassword() 함수를 호출하기 전에 처리할지, 아니면 호출한 뒤에 서버로부터 받은 메세지를 두고 처리할지 그 순서에 대해서 고민했습니다.

제가 내렸던 결론은 입력 검증 위치를 함수 호출하기 전으로 지정하여, 검증된 정보를 함수 호출의 인자값으로 넣는 방법으로 선택했습니다.

그래서 이를 위해 validate 함수를 추가하여 유효성 검사를 진행하였습니다.

TypeScript
// 비밀번호 유효성 검사 함수
export function validatePassword(pw: string) {
	if (pw.length < 6) {
		return '비밀번호는 최소 6자 이상이어야 합니다.';
	}
	if (!/[A-Za-z]/.test(pw) || !/[0-9]/.test(pw)) {
		return '비밀번호는 영문과 숫자를 모두 포함해야 합니다.';
	}
	if (/\s/.test(pw)) {
		return '비밀번호에는 공백을 포함할 수 없습니다.';
	}
	if (/^(?:123456|abcdef|password)$/i.test(pw)) {
		return '너무 단순한 비밀번호입니다. 다른 비밀번호를 사용해주세요.';
	}
	return null;
}
TypeScript
// registerWithEmail() 함수 호출 전, validatePassword()를 진행.
async function handleRegister() {
    if (!name || !email || !password || !confirmPassword) {
        error = '모든 필드를 입력해주세요.';
        return;
    }

    const pwError = validatePassword(password);
    if (pwError) {
        error = pwError;
        return;
    }

    if (password !== confirmPassword) {
        error = '비밀번호가 일치하지 않습니다.';
        return;
    }

    loading = true;
    const { success, error: err } = await registerWithEmail(name, email, password);
    loading = false;

    if (success) goto('/list');
    else error = getErrorMessage(err);
}

이외에도 Firebase Authentication의 주요 오류 코드 목록도 공유합니다.

상황

오류 코드

설명

회원가입 관련

auth/email-already-in-use

이미 등록된 이메일 주소

auth/invalid-email

이메일 형식이 올바르지 않음

auth/weak-password

비밀번호가 너무 짧거나 취약함 (6자 미만)

auth/operation-not-allowed

해당 인증 방식(예: 이메일/비밀번호)이 비활성화되어 있음

로그인 관련

auth/user-not-found

해당 이메일로 가입된 사용자가 없음

auth/wrong-password

비밀번호가 일치하지 않음

auth/invalid-credential

자격 증명(credential)이 잘못되었거나 만료됨

auth/too-many-requests

로그인 시도 횟수가 너무 많음 (Firebase가 일시 차단)

auth/user-disabled

관리자가 해당 계정을 비활성화함

Google 로그인 등 외부 인증

auth/popup-closed-by-user

로그인 팝업 창을 사용자가 닫음

auth/cancelled-popup-request

여러 로그인 팝업이 동시에 호출됨

auth/account-exists-with-different-credential

같은 이메일이 다른 로그인 방식으로 이미 존재

기타

auth/network-request-failed

네트워크 오류로 Firebase에 요청 실패

auth/internal-error

Firebase 내부 오류 (일시적인 문제)

| 서론

이번 과정을 통해 Firebase Authentication을 사용하면 복잡한 인증 로직 없이도,

이메일/비밀번호 로그인과 구글 로그인을 간단히 구현할 수 있음을 확인했습니다.

또한 Firestore와 연동해 사용자 정보를 함께 관리함으로써,
회원 데이터 구조를 통합적으로 다룰 수 있었습니다.

결국, Firebase Authentication은 서버리스 환경에서 보안성과 편의성을 모두 확보할 수 있는 효율적인 인증 도구라는 점을 느꼈습니다.