똑같은 삽질은 2번 하지 말자

Nextjs + Firebase를 이용한 로그인과 게시판 만들어보기 본문

책을 읽자

Nextjs + Firebase를 이용한 로그인과 게시판 만들어보기

곽빵 2024. 8. 17. 19:00

개요

어떤 기업의 코딩테스트로 Nextjs + Firebase로 인증시스템과 게시판을 간단히 만들어보라는 코딩테스트?가 있어서 만들었던 과정을 기록해보고자 한다.

 

완성된 코드는 밑의 리포지토리 참조

https://github.com/gmldnjs26/playground-nextjs-firebase

 

GitHub - gmldnjs26/playground-nextjs-firebase

Contribute to gmldnjs26/playground-nextjs-firebase development by creating an account on GitHub.

github.com

 

배포한곳

https://playground-nextjs-firebase-8f65.vercel.app/

 

Firebase의 설정

https://console.firebase.google.com/

 

로그인 - Google 계정

이메일 또는 휴대전화

accounts.google.com

위의 사이트에 접근하면 firebase의 프로젝트를 만들수 있는 콘솔창이 표시될텐데 프로젝트를 하나 생성한다.

 

GA의 사용설정에 대해서 묻는데 일단 지금은 필요없으므로 비활성화 하고 프로젝트를 만든다.

 

 

내가 Firebase내부에서 사용할 서비스 3개(Authentication, Firestore Database, Storage)를 활성화

 

프로젝트의 개요의 부분을 클릭해서 앱추가 버튼을 누르면 밑과 같이 어떤 앱을 추가할건지 묻는데 나를 웹앱이므로 웹을 클릭해준다.

 

그러면 밑과 같이 SDK를 설정할 때 필요한 키나 버켓의 주소가 담긴 값을 보여준다.

(참고로 밑의 앱은 삭제했으므로 api key를 도용해도 안될꺼에요~)

 

이걸로 대충 Firebase의 설정은 끝났다.

 

Nextjs에서 Fireabase의 설정

우선 firebase sdk를 다운로드

npm install firebase

 

plugins/firebase.ts

import { FirebaseOptions, initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";

const firebaseConfig: FirebaseOptions = {
  apiKey: process.env.NEXT_PUBLIC_API_KEY || "",
  authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN || "",
  projectId: process.env.NEXT_PUBLIC_PROJECT_ID || "",
  storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET || "",
  messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID || "",
  appId: process.env.NEXT_PUBLIC_APP_ID || "",
};

const app = initializeApp(firebaseConfig);

const auth = getAuth(app);
const firestore = getFirestore(app);
const storage = getStorage(app);

export { auth, firestore, storage };

 

  • firebaseConfig에는 Firebase 콘솔에서 받았던 키들을 설정해준다.
  • auth, firestore, storage등은 내가 콘솔에서 활성화 시켰던 서비스들 이용하기 위해 export해준다.

app/(beforeLogin)/auth/sign-in

"use client";

import { signInWithEmailAndPassword } from "firebase/auth";
... 생략

export default function SignInPage() {
  const router = useRouter();

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = async () => {
    try {
      const res = await signInWithEmailAndPassword(auth, email, password);
      if (res.user) {
        router.push("/posts");
      }
    } catch (error) {
       console.error(error);
    }
  };

  return (
    <div className={styles.signInPage}>
      <h1>ログイン</h1>
      <Input
        type="email"
        label="メールアドレス"
        placeholder="test@example.com"
        width="300px"
        value={email}
        onChange={(value) => setEmail(value)}
      />
      <Input
        type="password"
        label="パスワード"
        placeholder="パスワードを入力してください"
        width="300px"
        value={password}
        onChange={(value) => setPassword(value)}
      />
      <Button width="200px" onClick={handleLogin}>
        ログイン
      </Button>
      <Link className={styles.linkButton} href="/auth/sign-up">
        アカウントをお持ちではない方はこちら
      </Link>
    </div>
  );
}
  • signInWithEmailAndPassword라는 함수를 firebase/auth로 부터 import해서 로그인할때 이용한다.
  • 로그인에 성공하면 브라우저의 IndexDB에 인증정보가 저장되더라

app/(beforeLogin)/auth/sign-up

(글이 길어지면 가독성이 떨어지므로 UI의 부분은 생략하고 로직부분을 중심으로 기록한다.)

"use client";

import { FirebaseError } from "firebase/app";
import { createUserWithEmailAndPassword } from "firebase/auth";
import { doc, setDoc } from "firebase/firestore";
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";

// ... 생략

export default function SignUpPage() {
  const router = useRouter();

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [profileIcon, setProfileIcon] = useState<File>();
  const [gender, setGender] = useState("male");
  const [birthday, setBirthday] = useState<IDate>();
  const [isAgreed, setIsAgreed] = useState(false);

  const handleRegister = async () => {
    try {
      const res = await createUserWithEmailAndPassword(auth, email, password);

      let profileIconUrl = "";
      if (profileIcon) {
        const storageRef = ref(storage, `users/${res.user.uid}/profile-icon`);
        const uploadSnapshot = await uploadBytes(storageRef, profileIcon);
        profileIconUrl = await getDownloadURL(uploadSnapshot.ref);
      }

      const userDoc = doc(firestore, "users", res.user.uid);
      await setDoc(userDoc, {
        name: name,
        email: email,
        profileIconUrl: profileIconUrl,
        gender: gender,
        birthday: birthday,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      });

      alert("登録が完了しました");
      router.push("/auth/sign-in");
    } catch (error) {
      if (error instanceof FirebaseError) {
        if (error.code === firebaseErrorCodes.emailAlreadyInUse) {
          alert("既に登録されているメアドです");
        }
      } else {
        alert("登録する際にエラーが発生しました");
      }
    }
  };
  
  return (
    // ... 생략
  )
}
  • 회원가입에 필요한 요소는 이름, 이메일, 패스워드, 프로필 아이콘, 성별, 생일, 약관동의여부등 7가지가 있다.
  • 여기서 이름, 이메일, 프로필 아이콘, 성별, 생일 약관동의여부등은 firestore에 저장을 하고 프로필 아이콘에 대해서는 url만 저장하고 실제 이미지는 storage에 저장을 한다.
  • createUserWithEmailAndPassword로 email과 password를 이용해 회원가입을 한다.
  • createUserWithEmailAndPassword의 res의 안에 있는 유저의 uid를 storage에 저장할 이미지 이름을 정하고 해당 이미지 이림의 참조 인스턴스를 ref(storage, `users/${res.user.uid}/profile-icon`)로 얻어낸다.
  • 얻어낸 참조인스턴스로 updateBytes로 업로드
  • getDownloadURL로 업로드한 이미지의 URL을 얻어낸다.
  • createUserWithEmailAndPassword의 res로 온 uid로 firestore의 doc함수를 이용해 users라는 document에 유저의 정보를 등록하기 위해 참조 인스턴스를 얻어낸다.
  • setDoc으로 유저의 정보를 등록

app/auth-provider

로그인여부와 현재 로그인중인 유저의 정보를 세션으로부터 얻어내 전역적으로 공유하기 위한 provider

"use client";

import { onAuthStateChanged } from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
import { usePathname, useRouter } from "next/navigation";
import {
  createContext,
  ReactNode,
  useContext,
  useEffect,
  useState,
} from "react";

import { auth, firestore } from "@/plugins/firebase";
import { IUser } from "@/types/user";

type AuthContext = {
  user: IUser | null;
  loading: boolean;
};

const AuthContext = createContext<AuthContext>({
  user: null,
  loading: true,
});

const noRedirectPaths = ["/auth/sign-in", "/auth/sign-up"];

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const router = useRouter();
  const pathName = usePathname();

  const [user, setUser] = useState<IUser | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const authorize = onAuthStateChanged(auth, async (fbUser) => {
      // 유저의 세션정보가 없을시 로그인화면으로 전환
      if (!fbUser && !noRedirectPaths.includes(pathName)) {
        setUser(null);
        router.push("/auth/sign-in");
      }

      // 유저의 세션이 있으나, 유저의 정보가 컨텍스트에 없을시 취득하고 저장한다.
      if (fbUser && !user) {
        const userDoc = doc(firestore, "users", fbUser.uid);
        const userDocSnap = await getDoc(userDoc);
        if (userDocSnap.exists()) {
          setUser({ uid: fbUser.uid, ...userDocSnap.data() } as IUser);
        }
      }

      setLoading(false);
    });
    return () => authorize();
  });

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

 

  • onAuthStateChanged라는 함수의 콜백을 등록한다. 콜백을 등록해 두면 로그인이나 로그아웃이 일어났을때를 트리거로써 콜백함수가 실행된다.
  • clear함수로써 authorize를 다시 한번 실행하는데 브라우저의 리로드로 인한 context의 데이터가 없어져도 다시 한번 호출함으로써 데이터를 유지할 수 있다.

 

게시판에 관해서는 지금까지 나왔던 개념들로도 충분히 작성할 수 있으니 완성된 코드를 참조!

'책을 읽자' 카테고리의 다른 글

미움받을 용기  (0) 2024.06.14
Comments