파이어베이스 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 |
댓글