파이어베이스 Auth 세팅

우선 파이어베이스에서 로그인 제공업체를 선택합니다. github으로 진행해보겠습니다.

github에 접속하여, Settings / Developer Settings / OAuth Apps에서 New OAuth App을 만들고 Client ID와 Client secrets를 복사해둡니다.

복사해둔 ID와 secret을 입력하고, callback URL도 복붙해 깃허브에 입력해줍니다.
Next.js 설정
패키지 관리자로 firebase와 firebase-admin을 설치해줍니다.
pnpm add firebase firebase-admin
다음과 같이 .env를 구성합니다. 서버 측 코드(route.ts 등)에서는 firebase-admin SDK를 이용해서 접근해야 합니다.
NEXT_PUBLIC_FIREBASE_API_KEY= NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= NEXT_PUBLIC_FIREBASE_PROJECT_ID= NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= NEXT_PUBLIC_FIREBASE_APP_ID= FIREBASE_CLIENT_EMAIL= FIREBASE_PRIVATE_KEY= JWT_SECRET=
파이어베이스 프로젝트 설정의 일반 탭에서 키들을 복사해 넣어줍니다. client email과 private key는 파이어베이스의 프로젝트 설정에서 서비스 계정에 들어가면 생성할 수 있습니다.
lib/firebase/client.ts는 다음과 같이 구성하였습니다.
import { getApp, getApps, initializeApp } from "firebase/app"; import { GithubAuthProvider, getAuth, signInWithPopup, } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; export const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, }; export const app = !getApps().length ? initializeApp(firebaseConfig) : getApp(); export const db = getFirestore(app); export const auth = getAuth(app); const providers = { github: new GithubAuthProvider(), }; export const signIn = async (provider: "github") => { const rawUserData = (await signInWithPopup(auth, providers[provider])).user; if (!rawUserData) { throw new Error("No user data returned"); } const user = await ( await fetch(`/api/auth/sign-in`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(rawUserData), cache: "no-store", }) ).json(); return user; }; export async function signOut() { try { return auth.signOut(); } catch (error) { console.error("Error signing out", error); } }
lib/firebase/admin.ts는 아래와 같습니다.
import "server-only"; import * as admin from "firebase-admin"; import { cert } from "firebase-admin/app"; if (!admin.apps.length) { admin.initializeApp({ credential: cert({ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY, }), databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, }); } export const db = admin.firestore(); export const adminAuth = admin.auth();
signIn의 경우, 각 프로바이더를 이용해 인증을 한 후 파이어스토어에 유저 정보를 만들어주기 위해 /api/auth/sign-in에 POST를 날리도록 했습니다.
/api/auth/sign-in은 아래처럼 구성하였는데, cookie에 user의 uid를 저장해두고 차후 요청 시 사용할 계획입니다. db에 해당 유저의 정보가 있으면 그 유저의 정보를 반환하고, 없으면 만들어줍니다.
import { db } from "@/app/lib/firebase/admin"; import { UserType } from "@/app/types/user"; import { convertUserType } from "@/app/utils/convert-user-type"; import { User } from "firebase/auth"; import { cookies } from "next/headers"; export async function POST(request: Request) { const rawUserData: User = await request.json(); cookies().set("user", rawUserData.uid); const docRef = db.doc(`user/${rawUserData.uid}`); const doc = await docRef.get(); if (doc.exists) { return Response.json(doc.data()); } const user: UserType = convertUserType(rawUserData); docRef.set(user); return Response.json(user); }
convertUserType은 파이어베이스 인증 시 받아오는 유저 정보를 사용할 프로젝트에 맞게 변환해주는 함수입니다.
import { User } from "firebase/auth"; export const convertUserType = (rawUserData: User) => { return { id: rawUserData.uid, email: rawUserData.email, username: rawUserData.displayName, image: rawUserData.photoURL, provider: rawUserData.providerData[0].providerId, role: "user", }; };
이렇게 생겼습니다.
로컬에서 user 정보 관리는 일단 zustand로 해줄 겁니다. 이 과정은 생략하겠습니다.
import { IoLogoGithub, IoLogoGoogle } from "react-icons/io5"; import { signIn } from "../lib/firebase/client"; import { useCallback, useState } from "react"; import { useUserStore } from "../store/user-store"; export default function SignIn({ close }: { close: () => void }) { const [error, setError] = useState<string | null>(null); const { setUser } = useUserStore(); const handleSignIn = useCallback( async (provider: "github" | "google") => { const user = await signIn(provider); if (user) { setUser(user); setError(null); close(); return; } setError("Failed to sign in"); }, [close, setError, setUser] ); return ( <div className="bg-base-100 p-12 flex flex-col gap-4"> {error && <div className="text-red-500">{error}</div>} <button className="btn bg-[#333] text-white justify-between" onClick={() => { handleSignIn("github"); }} > <IoLogoGithub /> Sign in with GitHub </button> </div> ); }
이런 식으로 로그인을 하면 user를 로컬값으로 저장해두고 씁니다.
그리고 새로고침이나 재접속을 해도 user 값을 유지해주기 위해, 어딘가에 아래와 같은 코드를 넣어줍니다. (저의 경우 헤더 컴포넌트에 넣었습니다.)
useEffect(() => { const unsubscribe = auth.onAuthStateChanged((rawUserData) => { if (rawUserData) { setUser(convertUserType(rawUserData)); } else { setUser(null); } }); return () => unsubscribe(); }, []);
로그아웃 버튼은 아래처럼 만들어주는데, 쿠키를 삭제하기 위해 서버 함수를 사용했습니다.
import { signOut } from "@/app/lib/firebase/client"; ... const handleSignOut = async () => { deleteCookie(); await signOut(); }; ...
"use server"; import { cookies } from "next/headers"; export const deleteCookie = async () => { cookies().delete("user"); };
이제 signIn, signOut 함수를 구현했습니다.
article을 로그인한 유저만 작성할 수 있도록 firebase rule을 아래와 같이 업데이트 해줍니다.
service cloud.firestore { match /databases/{database}/documents { match /article/{document=**} { allow read: if true; allow write: if request.auth != null; } } }
클라이언트 컴포넌트에서 /api/articles에 POST 메소드로 보낼 때는 jwt 토큰을 생성해서 헤더에 넣어 사용합니다.
const result = await fetch(`/api/articles`, { method: "POST", headers: { authorization: `Bearer ${createToken(user!.id)}`, "Content-Type": "application/json", }, body: JSON.stringify(article), cache: "no-store", }); const json = await result.json(); if (json.error) throw new Error(json.error);
/api/articles/route.ts는 아래처럼 작성합니다. 여기서 db는 admin에서 정의한 db입니다.
export async function POST(request: Request) { const article: ArticleType = await request.json(); const userId = decodeToken( (request.headers.get("authorization") as string).split(" ")[1] )?.userId; const userData = userId ? (await db.doc(`user/${userId}`).get()).data() : null; if (!userData) { return Response.json({ error: "User not found" }); } const newDoc = await db.collection("article").add({ ...article, createdAt: new Date(), }); return Response.json({ ...article, id: newDoc.id }); }
유저가 존재하지 않으면 에러를 반환합니다.
서버 쪽에서 보낼 때는 쿠키에 저장해둔 uid값을 사용해 똑같이 요청을 보냅니다. 코드는 아래와 같습니다.
const getData = async (id: string) => { try { const userId = cookies().get("user")?.value || ""; const token = createToken(userId); const result = await fetch(`${process.env.APP_URL}/api/articles/${id}`, { headers: { credentials: "include", authorization: `Bearer ${token}`, }, cache: "no-cache", }).then((res) => res.json()); if (!result.article) return null; return result.article as ArticleType; } catch (e) { console.log(e); return null; } };
'Web > Frontend' 카테고리의 다른 글
[라이브러리] 뚝딱뚝딱 UI 라이브러리 만드는 일기 (4) | 2024.06.04 |
---|---|
[Next.js] 1시간만에 OpenAI로 영어 회화 도우미 만들기 (3) | 2024.04.07 |
[Web] 초보 웹 프론트엔드 개발자가 백엔드와 통신할 때 오류가 난다면? (3) | 2023.11.22 |
[Next.js] AWS Polly API를 사용해 TTS 서비스 만들기 (2) | 2023.11.15 |
[Next.js] Naver Map API 이용해서 현재 위치 지도 띄우기 (1) | 2023.11.15 |
댓글