[Firebase] Authentication 구현 과정 정리

예구·2023년 11월 16일
0

Firebase

목록 보기
1/3
post-thumbnail

X(트위터)를 클론코딩하면서 Firebase로 Authentication 구현하는 과정을 정리해 보려고 한다.


1. Setup

주로 사용한 건 React, TypeScript, styled-components이고, 사용한 버전은 다음과 같다.

현재까지의 폴더 구조는 아래와 같다.

📦src
 ┣ 📂components
 ┃ ┣ 📜auth-components.ts
 ┃ ┣ 📜github-btn.tsx
 ┃ ┣ 📜layout.tsx
 ┃ ┣ 📜loading-screen.tsx
 ┃ ┗ 📜protected-route.tsx
 ┣ 📂routes
 ┃ ┣ 📜create-account.tsx
 ┃ ┣ 📜home.tsx
 ┃ ┣ 📜login.tsx
 ┃ ┗ 📜profile.tsx
 ┣ 📜App.tsx
 ┣ 📜firebase.ts
 ┣ 📜main.tsx
 ┗ 📜vite-env.d.ts

우선 Firebase에서 프로젝트를 생성한 후, 앱을 추가한다. 내 경우, Web이므로 Web을 선택했다.

앱 등록이 끝나면 위와 같은 화면이 나오고, Firebase SDK 추가에 대해 설명해준다. Firebase에서 하라는대로 하면 된다.

npm install firebase

Firebase를 설치한 후 명시되어 있는 코드를 복사 붙여넣기만 하면 된다.

// firebase.ts

import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: "AIzaSyCHW8rdTIs_La9p0JGd3va2eyZYP2X6wfQ",
  authDomain: "nwitter-reloaded-d1408.firebaseapp.com",
  projectId: "nwitter-reloaded-d1408",
  storageBucket: "nwitter-reloaded-d1408.appspot.com",
  messagingSenderId: "395719636241",
  appId: "1:395719636241:web:84f98d6232e2710447c8b0",
};

const app = initializeApp(firebaseConfig);

// app에 대한 인증서비스를 이용하겠다.
export const auth = getAuth(app);

이렇게 하면 Firebase의 초기 설정은 완료됐다.



2. Authentication 활성화

Authentication 활성화는 Firebase 콘솔에서 먼저 활성화한 다음, 코드에서 초기화하는 순서로 하면 된다.

2.1. Firebase 콘솔 활성화

위의 화면에서 Authentication을 클릭하면 Firebase 콘솔에서 활성화가 된다.

그 다음 로그인 provider를 선택하면 되는데 내 경우, 이메일/비밀번호GitHub를 활성화했다.


2.2. 코드 초기화

// App.tsx

// ...
function App() {
  const [isLoading, setIsLoading] = useState(true);
  const init = async () => {
    // 최초 인증 상태가 완료될 때 실행되는 Promise를 return
    // Firebase가 쿠키와 토큰을 읽고 백엔드와 소통해서 로그인 여부를 확인하는 동안 기다림
    await auth.authStateReady();
    setIsLoading(false);
  };
  
  useEffect(() => {
    init();
  }, []);

  // ...
}

export default App;

위의 코드는 앱이 로딩되는 동안 Firebase의 초기 인증 상태를 확인하고, 이 상태가 resolve되면 애플리케이션을 렌더링한다.

authStateReady()를 사용하는 이유

authStateReady()는 Firebase에서 제공하는 메서드로, Firebase의 초기 인증 상태를 확인하는 데 사용된다. Firebase가 쿠키와 토큰을 읽고 백엔드와 소통하여 로그인 여부를 확인하는 동안 기다리는 Promise를 반환한다. 이 Promise는 초기 인증 상태가 해결될 때 즉시 resolve된다.

즉, authStateReady()를 사용한 이유는 애플리케이션이 시작될 때 Firebase의 초기 인증 상태를 기다리고, 해당 상태가 resolve된 후에 애플리케이션을 렌더링하기 위해서이다.



3. 계정 생성 구현하기

3.1. 사용자 입력 폼 구성

기능을 구현하기 전에 css를 먼저 구현했다.

사용자가 자신의 이름과 이메일, 비밀번호를 입력하는 폼을 만들었다. 이름과 이메일, 비밀번호는 모두 required로 설정했기 때문에 하나라도 작성하지 않으면 계정을 생성할 수 없다.

// create-account.tsx

// ...

export default function CreateAccount() {
  const [isLoading, setIsLoading] = useState(false);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const {
      target: { name, value },
    } = e;
    if (name === "name") {
      setName(value);
    } else if (name === "email") {
      setEmail(value);
    } else if (name === "password") {
      setPassword(value);
    }
  };

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // 화면 새로고침 방지
    try {
     // create an account
     // set the name of the user
     // redirect to the home page
    } catch (e) {
     // setError
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <Wrapper>
      <Title>Join 𝕏</Title>
      <Form onSubmit={onSubmit}>
        <Input
          onChange={onChange}
          name="name"
          value={name}
          placeholder="Name"
          type="text"
          required
        />
        <Input
          onChange={onChange}
          name="email"
          value={email}
          placeholder="Email"
          type="email"
          required
        />
        <Input
          onChange={onChange}
          name="password"
          value={password}
          placeholder="Password"
          type="password"
          required
        />
        <Input
          type="submit"
          value={isLoading ? "Loading..." : "Create account"}
        />
      </Form>
      {error !== "" ? <Error>{error}</Error> : null}
    </Wrapper>
  );
}

join 페이지

e.preventDefault() 사용하는 이유

이 코드에서 e.preventDefault()는 폼이 실제로 서버로 데이터를 보내지 않도록 하고, 대신에 onSubmit 함수에서 정의된 로직을 실행한다.

e.preventDefault()는 폼이 제출될 때 브라우저의 기본 동작을 중단시키는 역할을 한다. 기본적으로 HTML 폼이 제출되면 페이지가 새로고침되며, 이는 일반적으로 서버로 데이터를 전송하고 페이지를 다시 로드하는 동작을 수반한다. 하지만 React에서는 SPA의 일환으로, 페이지 전체를 다시 로드하지 않고도 애플리케이션의 상태를 업데이트하고 필요한 작업을 수행할 수 있다.


3.2. join 구현하기

try 안에 위에서 주석으로 작성해 둔 부분을 구현했다.

3.2.1. create an account

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    // ...
    // 로딩중이거나 하나라도 작성하지 않았다면 함수 종료
    if (isLoading || name === "" || email === "" || password === "") return;
    try {
      // Firebase Authentication을 사용하여 사용자 계정 생성
      const credentials = await createUserWithEmailAndPassword(
        auth,
        email,
        password
      );
      // 계정 생성에 성공하면 사용자의 자격 증명을 받게 되고, 
      // 계정이 이미 존재하거나 패스워드가 유요하지 않은 경우에 계정 생성에 실패함
    }
    // ...
  };

createUserWithEmailAndPassword()

Firebase Authentication에서 제공하는 함수로, 이메일과 비밀번호를 사용하여 사용자 계정을 생성하는 데 사용된다. 계정 생성에 성공하면 사용자의 자격 증명을 받게 되고, 계정이 이미 존재하거나 패스워드가 유효하지 않은 경우에 계정 생성에 실패한다.

이메일과 비밀번호를 매개변수로 받기 때문에, 로딩 중이거나 name, email, password 중 하나라도 비어있는 경우 함수를 종료하도록 조건문을 작성했다.


3.2.2. set the name of the user

사용자의 계정이 성공적으로 생성되면, 해당 사용자의 프로필을 업데이트하는 작업을 수행한다. 이 부분은 updateProfile() 메서드를 사용해서 구현했다.

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    // ...
    try {
      // ...
      // Firebase Authentication을 사용하여 사용자 프로필 업데이트
      await updateProfile(credentials.user, {
        displayName: name,
      });
      // Firebase의 사용자는 이름을 포함해서 작은 아바타 이미지의 URL을 가지는 미니 프로필을 갖게 됨
    }
    // ...
  };

updateProfile()

Firebase Authentication에서 제공하는 함수로, 사용자의 프로필 정보를 업데이트한다. 위의 코드에서는 displayName을 사용해서 사용자의 이름을 설정했다.


3.2.3. redirect to the home page

마지막으로, 사용자 계정 생성과 프로필 업데이트가 성공적으로 이뤄진 후에는 사용자를 애플리케이션의 홈 화면으로 리다이렉트해야 한다. 이를 위해 navigate("/")를 사용해서 화면을 전환헀다.

export default function CreateAccount() {
  const navigate = useNavigate();
  // ...

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    try {
      // ...
      // 사용자가 계정 생성 및 프로필 업데이트 후에는 애플리케이션의 홈 화면으로 리다이렉트
      navigate("/");
    }
    // ...
  };

navigate("/")는 React Router의 useNavigate 훅을 사용해서 지정된 경로로 이동시키는 역할을 한다. 따라서 사용자는 계정 생성이 성공하면 즉시 애플리케이션의 홈 화면으로 이동하게 된다.


추가된 코드는 다음과 같다.

// create-app.tsx

// ...

export default function CreateAccount() {
  const navigate = useNavigate();
  // ...

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (isLoading || name === "" || email === "" || password === "") return;
    
    try {
      setIsLoading(true);
      const credentials = await createUserWithEmailAndPassword(
        auth,
        email,
        password
      );

      await updateProfile(credentials.user, {
        displayName: name,
      });
      
      navigate("/");
    } catch (e) {
     // setError
    } finally {
      setIsLoading(false);
    }
  };

  return (
    // ...
  );
}



4. Protected Routes

현재 클론코딩 중인 앱은 사용자가 로그인한 상태여야만 HomeProfile 페이지로 접근할 수 있도록 설계되어 있다. 이를 구현하기 위해 ProtectedRoutes 컴포넌트가 필요하다. 이 컴포넌트는 사용자의 로그인 상태를 확인하고, 로그인되어 있지 않다면 로그인 페이지로 이동시킨다. 반대로 로그인되어 있다면, 해당 페이지로 이동을 허용한다.

4.1. ProtectedRoute 컴포넌트

// protected-route.tsx

import { Navigate } from "react-router-dom";
import { auth } from "../firebase";

export default function ProtectedRoute({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = auth.currentUser;
  
  // 사용자가 로그인되어 있지 않은 경우, 로그인 페이지로 이동
  if (user === null) {
    return <Navigate to="/login" />;
  }

  // 사용자가 로그인되어 있는 경우, 요청된 페이지로 이동
  return children;
}

auth.currentUser

Firebase Authentication에서 현재 로그인한 사용자의 정보를 제공하는 속성이다. 이를 통해 현재 애플리케이션 상에서 로그인한 사용자의 정보를 가져올 수 있다. ProtectedRoute에서는 이 정보를 활용하여 사용자가 로그인되어 있는지 여부를 확인하고, 그에 따라 페이지 이동을 제어한다.


4.2. ProtectedRoute 적용

ProtectedRouteApp.tsx에서 루트 경로(path: "/")에 대한 보호된 레이아웃을 정의하고, 해당 레이아웃 내에서 HomeProfile 페이지에 접근할 때 사용된다.

// App.tsx

// ...
const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <ProtectedRoute>
        <Layout />
      </ProtectedRoute>
    ),
    children: [
      {
        path: "",
        element: <Home />,
      },
      {
        path: "profile",
        element: <Profile />,
      },
    ],
  },
  {
    path: "/login",
    element: <Login />,
  },
  {
    path: "/create-account",
    element: <CreateAccount />,
  },
]);

// ...



5. Log Out

로그인을 구현하기 전에 임시로 Home 컴포넌트에 로그아웃을 구현헀다.

// Home.tsx

import { auth } from "../firebase";

export default function Home() {
  const logOut = () => {
    auth.signOut();
  };

  return (
    <h1>
      <button onClick={logOut}>Log Out</button>
    </h1>
  );
}

auth.signOut()

현재 로그인한 사용자를 로그아웃하는 역할을 한다. 위의 코드에서 logout 함수는 이 메서드를 호출해서 사용자를 로그아웃시킨다.



6. 오류 처리

Firebase를 이용해서 이메일과 비밀번호로 사용자를 생성하는 작업을 try할 때, 다양한 이유로 인해 작업이 실패할 수 있다. 사용자가 이미 존재하는 경우나 비밀번호가 규정에 맞지 않는 경우와 같은 상황에서 예외 처리를 통해 이러한 오류를 감지하고 처리할 수 있다. 이를 위해 try-catch 문을 사용하고, 이때 catch 블록에서 오류를 적절히 처리할 수 있다.

// create-account.tsx

// ...
export default function CreateAccount() {
  // ...
  const [error, setError] = useState("");

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    setError(""); // 기존 에러 값 지우기
    
    try {
      // ...
    } catch (e) {
      // 만약 오류가 FirebaseError의 인스턴스인 경우에만 처리
      if (e instanceof FirebaseError) {
        // setError 함수를 사용하여 상태 업데이트
        setError(e.message);
      }
    }
    // ...
  };

  return (
    <Wrapper>
      // ...
      {error !== "" ? <Error>{error}</Error> : null}
      // ...
    </Wrapper>
  );
}

e instanceof FirebaseError

JavaScript에서 instanceof 연산자는 객체가 특정 클래스나 생성자의 인스턴스인지 여부를 확인하는 데 사용된다. 여기에선 eFirebase 클래스의 인스턴스인지를 확인하여 해당하는 경우에 오류를 처리한다.

e.message

FirebaseError가 발생한 경우, 해당 오류에 대한 메시지가 e.message에 담겨져 있다.


이미 존재하는 이메일로 계정 생성을 시도하면 아래와 같이 에러가 표시된다.

catch error



7. Log In

로그인 페이지의 코드는 대부분 create-account.tsx와 유사하게 구성되어 있다. 대신, 로그인 페이지에서는 사용자의 이름(name)에 대한 코드가 필요하지 않기 때문에 해당 부분이 생략되었다.

// login.tsx

// ...
export default function Login() {
  const navigate = useNavigate();
  const [isLoading, setIsLoading] = useState(false);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const {
      target: { name, value },
    } = e;
    if (name === "email") {
      setEmail(value);
    } else if (name === "password") {
      setPassword(value);
    }
  };

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setError("");

    if (isLoading || email === "" || password === "") return;

    try {
      setIsLoading(true);
      // UserCredential 반환 -> 로그인한 사용자가 누군지 알려줌
      await signInWithEmailAndPassword(auth, email, password);
      navigate("/");
    } catch (e) {
      if (e instanceof FirebaseError) {
        setError(e.message);
      }
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <Wrapper>
      <Title>Log into 𝕏</Title>
      <Form onSubmit={onSubmit}>
        <Input
          onChange={onChange}
          name="email"
          value={email}
          placeholder="Email"
          type="email"
          required
        />
        <Input
          onChange={onChange}
          name="password"
          value={password}
          placeholder="Password"
          type="password"
          required
        />
        <Input type="submit" value={isLoading ? "Loading..." : "Log in"} />
      </Form>
      {error !== "" ? <Error>{error}</Error> : null}
    </Wrapper>
  );
}

signInWithEmailAndPassword()

Firebase에서 제공하는 메서드로, 이메일과 비밀번호를 사용하여 사용자를 인증하는 데 사용된다. 이 메서드는 auth 객체와 사용자의 이메일과 비밀번호를 매개변수로 받아와서, 해당 정보로 사용자를 로그인시킨다.

이 메서드가 성공하면 UserCredential라는 객체를 반환하는데, 이 객체를 통해 로그인한 사용자에 대한 정보를 얻을 수 있다.

Login 구현


추가로, 로그인 페이지에서 계정이 없는 경우에 회원가입 페이지로 이동할 수 있고, 회원가입 페이지에서 계정이 있는 경우에 로그인 페이지로 이동할 수 있는 Switcher 컴포넌트를 추가했다.

// create-account.tsx

// ...
export default function CreateAccount() {
  // ...

  return (
    <Wrapper>
      <Switcher>
        Already have an account? <Link to="/login">Log in &rarr;</Link>
      </Switcher>
    </Wrapper>
  );
}

회원가입페이지

// login.tsx

// ...
export default function Login() {
  // ...

  return (
    <Wrapper>
      // ...
      <Switcher>
        Don't have an account?{" "}
        <Link to="/create-account">Create one &rarr;</Link>
      </Switcher>
    </Wrapper>
  );
}

로그인페이지



8. 마무리

글이 너무 길어지는 것 같아서 이번 포스팅은 여기서 마무리를 지으려고 한다. 추가로 GitHub 로그인과 이메일로 회원가입 시 인증 메일을 보내는 기능까지 구현해 보고 추가로 포스팅을 이어나가려고 한다. 그럼 이번 포스팅은 여기서 끄-ㅌ!

profile
우당탕탕 FE 성장기

0개의 댓글