카테고리 없음

React + Firebase에서 로그인, Google OAuth 인증 구현하기

민이(MInE) 2025. 4. 18. 01:09
반응형

 

안녕하세요! 오늘은 React 애플리케이션에서 Firebase를 활용하여 Google OAuth 로그인을 구현하는 방법을 단계별로 알아보겠습니다. 로컬 개발 환경에서 Firebase 에뮬레이터를 사용하여 실제 프로덕션 환경에 영향을 주지 않고도 테스트할 수 있는 방법까지 자세히 설명하겠습니다.

시작하기 전에

이 튜토리얼은 다음 환경을 기준으로 작성되었습니다:

  • Node.js (v14 이상)
  • npm 또는 yarn 패키지 매니저
  • 기본적인 React와 JavaScript 지식

1. React 프로젝트 생성하기

우선 Create React App을 사용하여 새 프로젝트를 생성합니다:

npx create-react-app rebrain-app
cd rebrain-app

2. Firebase 및 필요한 패키지 설치하기

프로젝트에 Firebase를 추가하고 Firebase CLI를 설치합니다:

# Firebase 라이브러리 설치
npm install firebase

# Firebase CLI 설치 (전역)
npm install -g firebase-tools

3. Firebase 프로젝트 설정하기

3.1 Firebase 계정 로그인

터미널에서 다음 명령어로 Firebase 계정에 로그인합니다:

firebase login

브라우저가 열리면 Google 계정으로 인증을 완료합니다.

3.2 Firebase 프로젝트 초기화

프로젝트 폴더에서 Firebase를 초기화합니다:

firebase init

초기화 과정에서 다음 항목을 선택합니다:

  • Firestore: 앱의 데이터베이스로 사용
  • Authentication: 인증 기능 사용
  • Storage: 파일 저장 (나중에 PDF/Word 임포트 기능을 위해)
  • Emulators: 로컬 개발 환경

각 서비스에 대해 기본 설정을 사용하고, 에뮬레이터 UI를 활성화합니다.

4. Firebase 구성 파일 생성하기

React 프로젝트에 Firebase 구성 파일을 추가합니다:

mkdir -p src/firebase
touch src/firebase/firebase.js

firebase.js 파일에 다음 코드를 추가합니다:

// src/firebase/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
import { getStorage, connectStorageEmulator } from "firebase/storage";

// Firebase 설정 정보 (Firebase 콘솔에서 확인)
const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID"
};

// Firebase 초기화
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const storage = getStorage(app);

// 개발 환경에서만 에뮬레이터 사용
if (process.env.NODE_ENV === 'development') {
  // Auth 에뮬레이터 연결
  connectAuthEmulator(auth, "http://localhost:9099");
  
  // Firestore 에뮬레이터 연결
  connectFirestoreEmulator(db, 'localhost', 8080);
  
  // Storage 에뮬레이터 연결
  connectStorageEmulator(storage, "localhost", 9199);
  
  console.log("Using Firebase Emulators");
}

export { auth, db, storage };

Firebase 콘솔에서 가져온 프로젝트 설정 정보를 firebaseConfig 객체에 넣어주세요.

5. 인증 로직을 위한 커스텀 훅 만들기

사용자 인증 로직을 관리할 커스텀 훅을 생성합니다:

touch src/firebase/useAuth.js

useAuth.js 파일에 다음 코드를 추가합니다:

// src/firebase/useAuth.js
import { useState, useEffect } from 'react';
import { 
  createUserWithEmailAndPassword, 
  signInWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  onAuthStateChanged,
  signOut
} from 'firebase/auth';
import { doc, setDoc } from 'firebase/firestore';
import { auth, db } from './firebase';

export function useAuth() {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 인증 상태 변경 감지
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setCurrentUser(user);
      setLoading(false);
    });

    return unsubscribe;
  }, []);

  // 이메일/비밀번호 회원가입
  const signup = async (email, password) => {
    setError(null);
    try {
      const userCredential = await createUserWithEmailAndPassword(auth, email, password);
      // 회원가입 후 사용자 정보 Firestore에 저장
      await saveUserToFirestore(userCredential.user);
      return userCredential.user;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  // 이메일/비밀번호 로그인
  const login = async (email, password) => {
    setError(null);
    try {
      const userCredential = await signInWithEmailAndPassword(auth, email, password);
      return userCredential.user;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  // Google 로그인
  const loginWithGoogle = async () => {
    setError(null);
    try {
      const provider = new GoogleAuthProvider();
      // 필요한 추가 스코프 설정
      provider.addScope('profile');
      provider.addScope('email');
      
      const result = await signInWithPopup(auth, provider);
      
      // Google Access Token
      const credential = GoogleAuthProvider.credentialFromResult(result);
      const token = credential.accessToken;
      
      // 사용자 정보 Firestore에 저장
      await saveUserToFirestore(result.user);
      
      return result.user;
    } catch (err) {
      setError(err.message);
      console.error('Google 로그인 에러:', err);
      throw err;
    }
  };

  // Firestore에 사용자 정보 저장
  const saveUserToFirestore = async (user) => {
    try {
      const userRef = doc(db, 'users', user.uid);
      await setDoc(userRef, {
        displayName: user.displayName || 'User',
        email: user.email,
        photoURL: user.photoURL,
        providerId: user.providerData[0]?.providerId,
        lastLogin: new Date(),
        createdAt: new Date(),
        // 사용자 설정 기본값
        settings: {
          reviewCycle: 5,
          notifications: true,
          notificationTime: '18:00'
        }
      }, { merge: true }); // merge: true로 기존 데이터 유지
    } catch (error) {
      console.error('사용자 정보 저장 에러:', error);
    }
  };

  // 로그아웃
  const logout = async () => {
    setError(null);
    try {
      await signOut(auth);
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  return {
    currentUser,
    loading,
    error,
    signup,
    login,
    loginWithGoogle,
    logout
  };
}

6. Google 로그인 버튼 컴포넌트 만들기

Google 로그인 버튼을 위한 컴포넌트를 만듭니다:

touch src/components/GoogleLoginButton.js
touch src/components/GoogleLoginButton.css

GoogleLoginButton.js 파일에 다음 코드를 작성합니다:

Google 로그인 버튼을 위한 컴포넌트를 만듭니다:
bashtouch src/components/GoogleLoginButton.js
touch src/components/GoogleLoginButton.css
GoogleLoginButton.js 파일에 다음 코드를 작성합니다:
jsx// src/components/GoogleLoginButton.js
import React from 'react';
import './GoogleLoginButton.css';
import { useAuth } from '../firebase/useAuth';

function GoogleLoginButton() {
  const { loginWithGoogle, error } = useAuth();

  const handleGoogleLogin = async () => {
    try {
      await loginWithGoogle();
    } catch (err) {
      // 에러는 useAuth에서 처리됨
      console.error('Google 로그인 처리 중 에러:', err);
    }
  };

  return (
    <div className="google-login-container">
      <button 
        className="google-login-button"
        onClick={handleGoogleLogin}
      >
        <svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
          <g transform="matrix(1, 0, 0, 1, 0, 0)">
            <path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032 s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2 C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" fill="#FFF"></path>
          </g>
        </svg>
        Google로 로그인
      </button>
      
      {error && <p className="google-login-error">{error}</p>}
    </div>
  );
}

export default GoogleLoginButton;

GoogleLoginButton.css 파일에 다음 스타일을 추가합니다:

/* src/components/GoogleLoginButton.css */
.google-login-container {
  margin: 15px 0;
}

.google-login-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 12px;
  background-color: #4285F4;
  color: white;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  cursor: pointer;
  transition: background-color 0.3s;
}

.google-login-button:hover {
  background-color: #357ae8;
}

.google-login-button svg {
  width: 20px;
  height: 20px;
}

.google-login-error {
  color: #dc3545;
  margin-top: 10px;
  text-align: center;
}

7. 로그인 페이지 컴포넌트 만들기

로그인 및 회원가입 기능을 제공하는 페이지 컴포넌트를 만듭니다:

touch src/components/Login.js
touch src/components/Login.css

Login.js 파일에 다음 코드를 작성합니다:

// src/components/Login.js
import React, { useState } from 'react';
import './Login.css';
import { useAuth } from '../firebase/useAuth';
import GoogleLoginButton from './GoogleLoginButton';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSignup, setIsSignup] = useState(false);
  const { currentUser, loading, error, signup, login, logout } = useAuth();

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      if (isSignup) {
        await signup(email, password);
      } else {
        await login(email, password);
      }
      // 성공 시 폼 초기화
      setEmail('');
      setPassword('');
    } catch (err) {
      console.error('인증 에러:', err);
      // 에러는 useAuth에서 처리됨
    }
  };

  const handleLogout = async () => {
    try {
      await logout();
    } catch (err) {
      console.error('로그아웃 에러:', err);
    }
  };

  if (loading) {
    return <div className="loading">로딩 중...</div>;
  }

  return (
    <div className="login-container">
      <h2>{isSignup ? '회원가입' : '로그인'}</h2>
      
      {currentUser ? (
        <div className="profile-container">
          <h3>로그인 상태</h3>
          <div className="profile-info">
            {currentUser.photoURL && (
              <img 
                src={currentUser.photoURL} 
                alt="프로필 이미지" 
                className="profile-image" 
              />
            )}
            <div className="profile-details">
              <p><strong>이름:</strong> {currentUser.displayName}</p>
              <p><strong>이메일:</strong> {currentUser.email}</p>
              <p><strong>UID:</strong> {currentUser.uid}</p>
              <p><strong>인증 제공업체:</strong> {currentUser.providerData[0]?.providerId}</p>
            </div>
          </div>
          <button onClick={handleLogout} className="logout-btn">로그아웃</button>
        </div>
      ) : (
        <>
          <form onSubmit={handleSubmit} className="auth-form">
            <div className="form-group">
              <label htmlFor="email">이메일:</label>
              <input
                id="email"
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                required
              />
            </div>
            <div className="form-group">
              <label htmlFor="password">비밀번호:</label>
              <input
                id="password"
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                required
              />
            </div>
            <button type="submit" className="submit-btn">
              {isSignup ? '회원가입' : '로그인'}
            </button>
          </form>
          
          <div className="divider">
            <span>또는</span>
          </div>
          
          <GoogleLoginButton />
          
          <p className="toggle-auth">
            {isSignup ? '이미 계정이 있으신가요?' : '계정이 없으신가요?'} 
            <button onClick={() => setIsSignup(!isSignup)} className="toggle-btn">
              {isSignup ? '로그인' : '회원가입'}
            </button>
          </p>
        </>
      )}
      
      {error && <p className="error-message">{error}</p>}
    </div>
  );
}

export default Login;

Login.css 파일에 다음 스타일을 추가합니다:

/* src/components/Login.css */
.login-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  background-color: white;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 15px;
  margin-bottom: 20px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.form-group label {
  font-weight: bold;
}

.form-group input {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 16px;
}

.submit-btn {
  padding: 12px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  cursor: pointer;
  font-size: 16px;
}

.submit-btn:hover {
  background-color: #0069d9;
}

.divider {
  display: flex;
  align-items: center;
  text-align: center;
  margin: 20px 0;
}

.divider::before,
.divider::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid #ccc;
}

.divider span {
  padding: 0 10px;
  color: #777;
  font-size: 14px;
}

.toggle-auth {
  margin-top: 15px;
  text-align: center;
}

.toggle-btn {
  background: none;
  border: none;
  color: #007bff;
  padding: 0 5px;
  font-weight: bold;
  cursor: pointer;
}

.error-message {
  color: #dc3545;
  margin-top: 15px;
}

.profile-container {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
}

.profile-info {
  display: flex;
  gap: 15px;
  margin-bottom: 20px;
}

.profile-image {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
}

.profile-details {
  flex: 1;
}

.profile-details p {
  margin: 8px 0;
}

.logout-btn {
  width: 100%;
  padding: 10px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  cursor: pointer;
}

.logout-btn:hover {
  background-color: #c82333;
}

.loading {
  text-align: center;
  padding: 20px;
  font-size: 18px;
}

8. App 컴포넌트 수정하기

이제 App 컴포넌트를 수정하여 로그인 페이지를 렌더링합니다:

// src/App.js
import React from 'react';
import './App.css';
import Login from './components/Login';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Rebrain App</h1>
        <p>복습을 통한 효율적인 학습 시스템</p>
      </header>
      <main>
        <Login />
      </main>
      <footer>
        <p>© 2025 Rebrain</p>
      </footer>
    </div>
  );
}

export default App;

App.css에 기본 스타일을 추가합니다:

/* src/App.css */
.App {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.App-header {
  text-align: center;
  margin-bottom: 30px;
}

main {
  margin-bottom: 50px;
}

footer {
  text-align: center;
  margin-top: 50px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  color: #777;
}

9. Firebase 에뮬레이터 실행하기

이제 Firebase 에뮬레이터를 실행하여 로그인 기능을 테스트할 준비가 되었습니다. 새 터미널 창을 열고 다음 명령어를 실행합니다:

firebase emulators:start

에뮬레이터가 성공적으로 시작되면 다음과 같은 출력이 표시됩니다:

✔  hub: emulator hub started at http://localhost:4400
✔  ui: Emulator UI started at http://localhost:4000
✔  auth: started on port 9099
✔  firestore: started on port 8080
✔  storage: started on port 9199

 

10. React 앱 실행하기

다른 터미널 창을 열고 React 앱을 실행합니다:

npm start

이제 브라우저에서 http://localhost:3000으로 접속하면 로그인 페이지가 표시됩니다.

11. 테스트 및 디버깅

에뮬레이터 UI 활용하기

Firebase 에뮬레이터 UI(http://localhost:4000)를 사용하면 인증, 데이터베이스, 스토리지 상태를 모니터링하고 관리할 수 있습니다. Authentication 탭에서 사용자 계정을 확인하고, Firestore 탭에서 저장된 사용자 데이터를 확인할 수 있습니다.

 

Google 로그인 테스트하기

Google 로그인 버튼을 클릭하면 에뮬레이터는 실제 Google 서버에 연결하지 않고 가상의 Google 로그인 화면을 표시합니다. 여기서 테스트 계정을 선택하거나 새 계정을 만들 수 있습니다.

 

사용자 데이터 확인하기

로그인 후 Firestore 에뮬레이터에서 users 컬렉션을 확인하여 사용자 데이터가 올바르게 저장되었는지 확인합니다.

프로덕션 환경으로 전환하기

개발이 완료되면 에뮬레이터 연결 코드를 조건부로 만들거나 제거하여 실제 Firebase 서비스에 연결할 수 있습니다.

firebase.js 파일에서 다음 부분을 수정합니다:

// 개발 환경에서만 에뮬레이터 사용
// process.env.NODE_ENV === 'development' 조건을 
// process.env.REACT_APP_USE_EMULATOR === 'true'로 변경
if (process.env.REACT_APP_USE_EMULATOR === 'true') {
  // 에뮬레이터 연결
  connectAuthEmulator(auth, "http://localhost:9099");
  connectFirestoreEmulator(db, 'localhost', 8080);
  connectStorageEmulator(storage, "localhost", 9199);
  console.log("Using Firebase Emulators");
}

그리고 .env.development 파일을 생성하여 환경 변수를 설정합니다:

REACT_APP_USE_EMULATOR=true

마무리

이제 React 애플리케이션에서 Firebase를 활용한 Google OAuth 로그인 기능이 성공적으로 구현되었습니다! 이 기본 구조 위에 Rebrain 앱의 나머지 기능을 추가할 수 있습니다.

 

Firebase와 React를 조합하면 복잡한 기능도 비교적 쉽게 구현할 수 있습니다. 특히 Firebase 에뮬레이터를 활용하면 개발 비용을 절약하면서 효율적으로 테스트할 수 있는 장점이 있습니다.

반응형