React + Firebase에서 로그인, Google OAuth 인증 구현하기
안녕하세요! 오늘은 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 에뮬레이터를 활용하면 개발 비용을 절약하면서 효율적으로 테스트할 수 있는 장점이 있습니다.