[Flutter + Firebase로 북마크 앱 만들기 #3] Firebase Authentication 연동 및 로그인 구현

도경원's avatar
Oct 08, 2025
[Flutter + Firebase로 북마크 앱 만들기 #3] Firebase Authentication 연동 및 로그인 구현

🎯 이번 글에서 다룰 내용

  • Firebase Authentication Service 구현
  • Riverpod으로 인증 상태 관리
  • 로그인/회원가입 화면 개발
  • go_router로 인증 기반 라우팅 구현
  • 자동 리다이렉트 및 세션 관리

🔐 Firebase Authentication이란?

Firebase Authentication은 사용자 인증을 쉽게 구현할 수 있는 백엔드 서비스입니다.
주요 기능:
  • 이메일/비밀번호 인증
  • 소셜 로그인 (Google, Facebook 등)
  • 전화번호 인증
  • 익명 로그인
장점:
  • 서버 구축 불필요
  • 보안 처리 자동화
  • 세션 관리 자동화
  • 무료 tier로 충분한 사용량

📁 구현할 기능 구조

Authentication Flow ├── Service Layer (auth_service.dart) │ └── Firebase와 직접 통신 ├── State Management (auth_provider.dart) │ └── Riverpod Provider로 상태 관리 ├── UI Layer │ ├── LoginScreen (로그인 화면) │ └── SignupScreen (회원가입 화면) └── Routing (app_router.dart) └── 인증 상태에 따른 자동 리다이렉트

1️⃣ Firebase Auth Service 구현

Service Layer의 역할

위치: lib/features/auth/services/auth_service.dart
Service Layer는 Firebase와 직접 통신하는 계층입니다. 비즈니스 로직을 담당하며, UI와 Firebase를 분리합니다.
import 'package:firebase_auth/firebase_auth.dart'; import '../models/user_model.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; // 현재 사용자 가져오기 User? get currentUser => _auth.currentUser; // 사용자 스트림 (실시간 인증 상태) Stream<User?> get authStateChanges => _auth.authStateChanges(); // 이메일/비밀번호 회원가입 Future<UserModel?> signUpWithEmail({ required String email, required String password, }) async { try { final UserCredential userCredential = await _auth.createUserWithEmailAndPassword( email: email, password: password, ); if (userCredential.user != null) { return UserModel.fromFirebaseUser(userCredential.user!); } return null; } on FirebaseAuthException catch (e) { throw _handleAuthException(e); } catch (e) { throw '회원가입 중 오류가 발생했습니다: $e'; } } // 이메일/비밀번호 로그인 Future<UserModel?> signInWithEmail({ required String email, required String password, }) async { try { final UserCredential userCredential = await _auth.signInWithEmailAndPassword( email: email, password: password, ); if (userCredential.user != null) { return UserModel.fromFirebaseUser(userCredential.user!); } return null; } on FirebaseAuthException catch (e) { throw _handleAuthException(e); } catch (e) { throw '로그인 중 오류가 발생했습니다: $e'; } } // 로그아웃 Future<void> signOut() async { try { await _auth.signOut(); } catch (e) { throw '로그아웃 중 오류가 발생했습니다: $e'; } } // Firebase Auth 에러 처리 String _handleAuthException(FirebaseAuthException e) { switch (e.code) { case 'weak-password': return '비밀번호가 너무 약합니다.'; case 'email-already-in-use': return '이미 사용 중인 이메일입니다.'; case 'invalid-email': return '유효하지 않은 이메일입니다.'; case 'user-not-found': return '사용자를 찾을 수 없습니다.'; case 'wrong-password': return '잘못된 비밀번호입니다.'; case 'too-many-requests': return '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'; case 'user-disabled': return '비활성화된 계정입니다.'; default: return '인증 오류가 발생했습니다: ${e.message}'; } } }

설계 포인트

1. 단일 책임 원칙:
  • AuthService는 오직 인증 관련 로직만 처리
  • UI 로직과 완전히 분리
2. 에러 처리:
String _handleAuthException(FirebaseAuthException e) { switch (e.code) { case 'weak-password': return '비밀번호가 너무 약합니다.'; // ... 사용자 친화적 메시지로 변환 } }
  • Firebase의 영문 에러 코드를 한글 메시지로 변환
  • 사용자 경험 향상
3. 비동기 처리:
Future<UserModel?> signInWithEmail({...}) async { // Firebase 통신은 비동기 final UserCredential userCredential = await _auth.signInWithEmailAndPassword(...); }

2️⃣ Riverpod Provider로 상태 관리

Provider의 역할

위치: lib/features/auth/services/auth_provider.dart
Provider는 앱 전체에서 인증 상태를 공유하고 관리합니다.
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'auth_service.dart'; import '../models/user_model.dart'; // AuthService Provider final authServiceProvider = Provider<AuthService>((ref) { return AuthService(); }); // 인증 상태 스트림 Provider final authStateProvider = StreamProvider<User?>((ref) { final authService = ref.watch(authServiceProvider); return authService.authStateChanges; }); // 현재 사용자 Provider final currentUserProvider = Provider<User?>((ref) { final authService = ref.watch(authServiceProvider); return authService.currentUser; }); // 로그인 상태 확인 Provider final isLoggedInProvider = Provider<bool>((ref) { final authState = ref.watch(authStateProvider); return authState.value != null; });

Provider 종류 설명

1. Provider:
final authServiceProvider = Provider<AuthService>((ref) { return AuthService(); });
  • 불변 객체 제공
  • 싱글톤처럼 동작
  • AuthService 인스턴스 재사용
2. StreamProvider:
final authStateProvider = StreamProvider<User?>((ref) { final authService = ref.watch(authServiceProvider); return authService.authStateChanges; });
  • 실시간 데이터 스트림 제공
  • 로그인/로그아웃 시 자동 업데이트
  • UI가 자동으로 반응
3. 계산된 Provider:
final isLoggedInProvider = Provider<bool>((ref) { final authState = ref.watch(authStateProvider); return authState.value != null; });
  • 다른 Provider 값을 기반으로 계산
  • authStateProvider를 watch해서 boolean 반환

Riverpod 사용 장점

기존 방식
Riverpod
setState로 수동 관리
자동 상태 업데이트
BuildContext 필요
어디서든 접근 가능
런타임 에러 가능
컴파일 타임 체크
테스트 어려움
Mock 작성 쉬움

3️⃣ 로그인 화면 구현

UI 설계 원칙

위치: lib/features/auth/screens/login_screen.dart
디자인 고려사항:
  • 간결하고 직관적인 UI
  • Material Design 3 가이드라인 준수
  • 로딩 상태 표시
  • 유효성 검사
  • 사용자 친화적 에러 메시지

핵심 코드 분석

1. State 관리

class _LoginScreenState extends ConsumerState<LoginScreen> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; bool _obscurePassword = true; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } // ... }
포인트:
  • ConsumerState: Riverpod 사용을 위한 State
  • GlobalKey<FormState>: Form 유효성 검사용
  • TextEditingController: 입력값 관리
  • dispose(): 메모리 누수 방지

2. 로그인 로직

Future<void> _handleLogin() async { // 1. 유효성 검사 if (!_formKey.currentState!.validate()) return; // 2. 로딩 시작 setState(() => _isLoading = true); try { // 3. Firebase 로그인 final authService = ref.read(authServiceProvider); await authService.signInWithEmail( email: _emailController.text.trim(), password: _passwordController.text, ); // 4. 성공 메시지 if (mounted) { Fluttertoast.showToast( msg: AppStrings.loginSuccess, backgroundColor: AppColors.success, ); } } catch (e) { // 5. 에러 처리 if (mounted) { Fluttertoast.showToast( msg: e.toString(), backgroundColor: AppColors.error, ); } } finally { // 6. 로딩 종료 if (mounted) { setState(() => _isLoading = false); } } }
흐름 설명:
  1. Form 유효성 검사 (이메일, 비밀번호)
  1. 로딩 인디케이터 시작
  1. AuthService를 통해 Firebase 로그인
  1. 성공 시 토스트 메시지
  1. 실패 시 에러 메시지
  1. 로딩 종료
mounted 체크 이유:
if (mounted) { // UI 업데이트 }
  • 비동기 작업 중 화면이 닫힐 수 있음
  • 닫힌 화면에 setState 호출하면 에러 발생
  • mounted로 화면이 아직 존재하는지 확인

3. 이메일 입력 필드

TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: AppStrings.email, prefixIcon: const Icon(Icons.email), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), filled: true, fillColor: AppColors.surface, ), validator: (value) { if (value == null || value.isEmpty) { return AppStrings.fieldsRequired; } if (!value.contains('@')) { return AppStrings.invalidEmail; } return null; }, )
유효성 검사:
  • 빈 값 체크
  • 이메일 형식 체크 (@ 포함 여부)
  • 실시간 에러 메시지 표시

4. 비밀번호 입력 필드 (표시/숨김 기능)

TextFormField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( labelText: AppStrings.password, prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), // ... ), validator: (value) { if (value == null || value.isEmpty) { return AppStrings.fieldsRequired; } if (value.length < 6) { return AppStrings.passwordTooShort; } return null; }, )
기능:
  • obscureText: 비밀번호 숨김
  • suffixIcon: 표시/숨김 토글 버튼
  • 6자 이상 검증

5. 로그인 버튼 (로딩 상태)

ElevatedButton( onPressed: _isLoading ? null : _handleLogin, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( AppStrings.login, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), )
로딩 상태 처리:
  • _isLoading이 true면 버튼 비활성화
  • 로딩 중에는 CircularProgressIndicator 표시
  • 중복 클릭 방지

4️⃣ 회원가입 화면 구현

위치: lib/features/auth/screens/signup_screen.dart

로그인 화면과의 차이점

비밀번호 확인 필드 추가:
TextFormField( controller: _confirmPasswordController, obscureText: _obscureConfirmPassword, decoration: InputDecoration( labelText: AppStrings.confirmPassword, // ... ), validator: (value) { if (value == null || value.isEmpty) { return AppStrings.fieldsRequired; } if (value != _passwordController.text) { return AppStrings.passwordsDoNotMatch; } return null; }, )
비밀번호 일치 검증:
  • _passwordController.text와 비교
  • 불일치 시 에러 메시지
회원가입 후 처리:
Future<void> _handleSignup() async { // ... 회원가입 로직 if (mounted) { Fluttertoast.showToast( msg: AppStrings.signupSuccess, backgroundColor: AppColors.success, ); context.pop(); // 로그인 화면으로 돌아가기 } }

5️⃣ go_router 라우팅 설정

인증 기반 라우팅의 핵심

위치: lib/routes/app_router.dart
final goRouterProvider = Provider<GoRouter>((ref) { final authState = ref.watch(authStateProvider); return GoRouter( initialLocation: '/login', redirect: (context, state) { final isLoggedIn = authState.value != null; final isLoggingIn = state.matchedLocation == '/login' || state.matchedLocation == '/signup'; // 로그인 안 했는데 로그인 페이지가 아니면 -> 로그인 페이지로 if (!isLoggedIn && !isLoggingIn) { return '/login'; } // 로그인 했는데 로그인 페이지에 있으면 -> 홈으로 if (isLoggedIn && isLoggingIn) { return '/'; } return null; // 리다이렉트 없음 }, routes: [ GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/signup', builder: (context, state) => const SignupScreen(), ), GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), ], ); });

redirect 로직 상세 분석

1. 인증 상태 확인:
final authState = ref.watch(authStateProvider); final isLoggedIn = authState.value != null;
  • authStateProvider를 watch해서 실시간 감지
  • 로그인 상태가 변경되면 자동으로 재평가
2. 현재 위치 확인:
final isLoggingIn = state.matchedLocation == '/login' || state.matchedLocation == '/signup';
  • 현재 로그인/회원가입 페이지인지 확인
3. 리다이렉트 로직:
상태
현재 위치
동작
비로그인
로그인 페이지
그대로 유지
비로그인
다른 페이지
/login으로 이동
로그인
로그인 페이지
/(홈)으로 이동
로그인
다른 페이지
그대로 유지
4. 자동 리다이렉트 예시:
시나리오 1: 로그인 성공 1. 로그인 완료 2. authState 변경 감지 3. redirect 실행 4. 로그인 상태 + 로그인 페이지 = 홈으로 이동 시나리오 2: 로그아웃 1. 로그아웃 완료 2. authState 변경 감지 3. redirect 실행 4. 비로그인 상태 + 홈 = 로그인 페이지로 이동

go_router 네비게이션 방법

context.push() vs context.go():
// push: 스택에 추가 (뒤로가기 가능) context.push('/signup'); // go: 현재 화면 대체 (뒤로가기 불가) context.go('/home'); // pop: 뒤로가기 context.pop();
사용 예시:
// 로그인 → 회원가입 TextButton( onPressed: () => context.push('/signup'), // 뒤로가기로 로그인 화면 복귀 가능 ) // 회원가입 완료 후 context.pop(); // 로그인 화면으로

6️⃣ main.dart 라우터 적용

위치: lib/main.dart

MaterialApp.router 사용

class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(goRouterProvider); return MaterialApp.router( title: 'Bookmark Manager', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), routerConfig: router, debugShowCheckedModeBanner: false, ); } }
변경사항:
  • MaterialAppMaterialApp.router
  • routerConfig에 goRouter 연결
  • StatelessWidgetConsumerWidget (Riverpod 사용)

7️⃣ 홈 화면 (임시) 구현

위치: lib/features/bookmarks/screens/home_screen.dart

기본 구조

class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authService = ref.read(authServiceProvider); final currentUser = authService.currentUser; return Scaffold( appBar: AppBar( title: const Text(AppStrings.myBookmarks), backgroundColor: AppColors.primary, foregroundColor: Colors.white, actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: () async { await authService.signOut(); // go_router가 자동으로 로그인 화면으로 리다이렉트 }, ), ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('환영합니다!'), Text(currentUser?.email ?? ''), Text(AppStrings.noBookmarks), ], ), ), ); } }
기능:
  • 사용자 이메일 표시
  • 로그아웃 버튼
  • 북마크 추가 플로팅 버튼 (다음 단계 구현 예정)

🧪 테스트 시나리오

1. 회원가입 플로우

1. 앱 실행 ↓ 2. 로그인 화면 표시 (자동) ↓ 3. "계정이 없으신가요? 회원가입" 클릭 ↓ 4. 회원가입 화면 이동 ↓ 5. 이메일/비밀번호 입력 - test@example.com - password123 ↓ 6. "회원가입" 버튼 클릭 ↓ 7. ✅ "회원가입 성공!" 토스트 ↓ 8. 로그인 화면으로 자동 복귀

2. 로그인 플로우

1. 로그인 화면 ↓ 2. 이메일/비밀번호 입력 ↓ 3. "로그인" 버튼 클릭 ↓ 4. ✅ "로그인 성공!" 토스트 ↓ 5. 홈 화면으로 자동 이동 ↓ 6. 사용자 이메일 표시 확인

3. 로그아웃 플로우

1. 홈 화면에서 로그아웃 버튼 클릭 ↓ 2. Firebase 로그아웃 실행 ↓ 3. authState 변경 감지 ↓ 4. ✅ 로그인 화면으로 자동 리다이렉트

4. 유효성 검사 테스트

이메일 검증:
  • ❌ 빈 값: "모든 필드를 입력해주세요"
  • @ 없음: "유효하지 않은 이메일입니다"
  • test@example.com: 통과
비밀번호 검증:
  • ❌ 빈 값: "모든 필드를 입력해주세요"
  • ❌ 5자 이하: "비밀번호는 최소 6자 이상이어야 합니다"
  • password123: 통과
비밀번호 확인 (회원가입):
  • ❌ 불일치: "비밀번호가 일치하지 않습니다"
  • ✅ 일치: 통과

5. Firebase 에러 처리 테스트

이미 존재하는 이메일:
1. 이미 가입된 이메일로 회원가입 시도 ↓ 2. ❌ "이미 사용 중인 이메일입니다" 토스트
잘못된 비밀번호:
1. 올바른 이메일 + 잘못된 비밀번호 ↓ 2. ❌ "잘못된 비밀번호입니다" 토스트
존재하지 않는 사용자:
1. 가입하지 않은 이메일로 로그인 시도 ↓ 2. ❌ "사용자를 찾을 수 없습니다" 토스트

🔍 Firebase Console 확인

Authentication 탭에서 확인 가능한 정보

  1. Users 목록:
      • UID (고유 식별자)
      • 이메일
      • 생성일
      • 마지막 로그인
      • 로그인 제공업체
  1. 활동 로그:
      • 로그인 시도
      • 성공/실패 기록
      • IP 주소
  1. 보안 설정:
      • 비밀번호 강도 설정
      • 이메일 인증 활성화
      • 계정 복구 옵션

📊 최종 프로젝트 구조

lib/ ├── main.dart ✅ ├── firebase_options.dart ✅ │ ├── core/ │ └── constants/ │ ├── app_colors.dart ✅ │ └── app_strings.dart ✅ │ ├── features/ │ ├── auth/ │ │ ├── models/ │ │ │ └── user_model.dart ✅ │ │ ├── screens/ │ │ │ ├── login_screen.dart ✅ │ │ │ └── signup_screen.dart ✅ │ │ └── services/ │ │ ├── auth_service.dart ✅ │ │ └── auth_provider.dart ✅ │ │ │ └── bookmarks/ │ ├── models/ │ │ └── bookmark.dart ✅ │ └── screens/ │ └── home_screen.dart ✅ │ └── routes/ └── app_router.dart

💡 구현하면서 배운 점

1. Service Layer 분리의 중요성

Before: UI에 Firebase 로직 직접 작성
// ❌ 화면에 직접 작성 await FirebaseAuth.instance.signInWithEmailAndPassword(...);
After: Service로 분리
// ✅ Service 사용 final authService = ref.read(authServiceProvider); await authService.signInWithEmail(...);
장점:
  • UI와 비즈니스 로직 분리
  • 테스트 용이
  • 재사용성 증가
  • 에러 처리 일원화

2. Riverpod의 실시간 상태 관리

StreamProvider의 강력함:
final authStateProvider = StreamProvider<User?>((ref) { return authService.authStateChanges; });
로그인/로그아웃 시:
  1. Firebase 상태 변경
  1. Stream이 새 값 emit
  1. Provider가 자동 업데이트
  1. UI가 자동 리빌드
  1. go_router의 redirect가 자동 실행
직접 관리할 필요 없음 - 모든 것이 자동!

3. go_router의 선언적 라우팅

기존 Navigator 방식의 문제:
// ❌ 명령형, 복잡함 if (isLoggedIn) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => HomeScreen()), ); } else { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => LoginScreen()), ); }
go_router 방식:
// ✅ 선언적, 간단함 redirect: (context, state) { if (!isLoggedIn && !isLoggingIn) return '/login'; if (isLoggedIn && isLoggingIn) return '/'; return null; }
장점:
  • 라우팅 로직이 한 곳에 집중
  • 인증 상태 변경 시 자동 리다이렉트
  • URL 기반 라우팅 (웹 친화적)

4. Form 유효성 검사의 중요성

사용자 경험 개선:
validator: (value) { if (value == null || value.isEmpty) { return AppStrings.fieldsRequired; } if (!value.contains('@')) { return AppStrings.invalidEmail; } return null; }
효과:
  • Firebase 요청 전 클라이언트에서 검증
  • 불필요한 네트워크 요청 감소
  • 즉각적인 피드백
  • 사용자 친화적

5. 에러 처리의 세심함

mounted 체크의 중요성:
if (mounted) { setState(() => _isLoading = false); }
왜 필요한가?:
  • 비동기 작업 중 화면이 닫힐 수 있음
  • 닫힌 화면에 setState 호출 시 에러
  • 앱 크래시 방지
Firebase 에러 메시지 한글화:
String _handleAuthException(FirebaseAuthException e) { switch (e.code) { case 'weak-password': return '비밀번호가 너무 약합니다.'; // ... } }
효과:
  • 사용자가 이해하기 쉬운 메시지
  • 더 나은 사용자 경험

🐛 트러블슈팅

문제 1: Navigator.onGenerateRoute 에러

에러 메시지:
Navigator.onGenerateRoute was null, but the route named "/signup" was referenced.
원인:
// ❌ go_router 사용 중인데 Navigator 사용 Navigator.pushNamed(context, '/signup');
해결:
// ✅ go_router 방식 사용 context.push('/signup');
교훈: 라우팅 패키지를 사용할 때는 일관되게 해당 패키지의 API 사용

문제 2: 로그인 후 화면 전환 안 됨

원인: go_router의 redirect 로직 오류
해결:
// authStateProvider를 watch final authState = ref.watch(authStateProvider); // redirect 로직 개선 redirect: (context, state) { final isLoggedIn = authState.value != null; // ... }
포인트: Provider를 watch해야 상태 변경 감지

📈 성능 최적화 포인트

1. TextEditingController dispose

@override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); }
왜 중요한가?:
  • Controller는 리소스를 점유
  • dispose하지 않으면 메모리 누수
  • 앱 성능 저하 원인

2. const 생성자 사용

const Icon(Icons.email) // ✅ Icon(Icons.email) // ❌
효과:
  • 컴파일 타임에 객체 생성
  • 런타임 오버헤드 감소
  • 불필요한 재빌드 방지

3. Provider의 적절한 사용

// ❌ 매번 새로운 인스턴스 ref.read(authServiceProvider) // ✅ watch: 변경 감지 필요할 때 ref.watch(authStateProvider) // ✅ read: 한 번만 읽을 때 ref.read(authServiceProvider)

🎨 UI/UX 개선 포인트

1. 로딩 인디케이터

_isLoading ? const CircularProgressIndicator(...) : Text(AppStrings.login)
효과:
  • 사용자에게 진행 상황 표시
  • 중복 클릭 방지
  • 신뢰감 향상

2. 비밀번호 표시/숨김

suffixIcon: IconButton( icon: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility), onPressed: () { setState(() => _obscurePassword = !_obscurePassword); }, )
효과:
  • 사용자 편의성
  • 비밀번호 확인 용이

3. Toast 메시지

Fluttertoast.showToast( msg: AppStrings.loginSuccess, backgroundColor: AppColors.success, );
장점:
  • 방해하지 않는 피드백
  • 자동으로 사라짐
  • 색상으로 성공/실패 구분

4. 라운드 디자인

border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ),
효과:
  • 모던한 디자인
  • Material Design 3 스타일
  • 시각적 통일성

🔒 보안 고려사항

1. 비밀번호 최소 길이

if (value.length < 6) { return AppStrings.passwordTooShort; }
이유: Firebase의 최소 요구사항 준수

2. 이메일 trim 처리

email: _emailController.text.trim(),
이유: 공백으로 인한 로그인 실패 방지

3. 에러 메시지 노출 제한

// ❌ 보안 위험 return 'Database error: ${e.message}'; // ✅ 일반적 메시지 return '로그인 중 오류가 발생했습니다';
이유: 시스템 정보 노출 방지

📝 코드 품질 개선

1. 상수 사용

// ✅ 상수 사용 Text(AppStrings.login) // ❌ 하드코딩 Text('로그인')
장점:
  • 오타 방지
  • 일관성 유지
  • 다국어 대응 준비

2. 색상 관리

// ✅ 색상 상수 backgroundColor: AppColors.primary // ❌ 직접 지정 backgroundColor: Color(0xFF2196F3)
장점:
  • 디자인 통일성
  • 색상 변경 시 한 곳만 수정

3. 파일 구조화

features/ auth/ models/ # 데이터 screens/ # UI services/ # 로직
장점:
  • 찾기 쉬움
  • 유지보수 편함
  • 확장 용이

🎯 다음 단계 예고

#4편에서 다룰 내용

1. Firestore 연동:
  • Bookmark Service 구현
  • CRUD Provider 작성
2. 북마크 목록 화면:
  • 실시간 북마크 목록 표시
  • StreamProvider로 자동 업데이트
  • 카테고리별 필터링
3. 북마크 추가 화면:
  • URL 입력 및 검증
  • 메타데이터 자동 추출 (선택)
  • 카테고리 설정
4. 북마크 수정/삭제:
  • 상세 화면
  • 수정 다이얼로그
  • 삭제 확인
5. 검색 기능:
  • 북마크 검색
  • 실시간 필터링

📦 현재 패키지 목록

dependencies: flutter: sdk: flutter # Firebase firebase_core: ^4.1.1 firebase_auth: ^6.1.0 cloud_firestore: ^6.0.2 # State Management flutter_riverpod: ^3.0.3 # Routing go_router: ^16.2.4 # Utilities url_launcher: ^6.3.2 flutter_spinkit: ^5.2.2 fluttertoast: ^9.0.0

🎓 학습 정리

이번 글에서 배운 것

기술적 학습:
  • ✅ Firebase Authentication 연동
  • ✅ Riverpod 상태 관리 실전 적용
  • ✅ go_router 인증 기반 라우팅
  • ✅ Form 유효성 검사
  • ✅ 비동기 에러 처리
  • ✅ StreamProvider 활용
아키텍처 학습:
  • ✅ Service Layer 분리
  • ✅ Provider 계층 구조
  • ✅ 선언적 라우팅
  • ✅ 상태 중심 설계
실무 스킬:
  • ✅ 사용자 친화적 에러 메시지
  • ✅ 로딩 상태 관리
  • ✅ 메모리 누수 방지
  • ✅ 보안 고려사항

💭 회고

잘한 점

  • 체계적인 폴더 구조로 확장성 확보
  • Riverpod으로 상태 관리 자동화
  • 에러 처리를 세심하게 구현
  • 사용자 경험 고려한 UI

개선할 점

  • 테스트 코드 작성 (다음 단계에서 추가 예정)
  • 로딩 상태 전역 관리 고려
  • 에러 핸들링 더 세분화

느낀 점

Firebase와 Riverpod의 조합이 정말 강력했습니다. 특히 StreamProvider를 통한 실시간 상태 관리는 마법 같았어요. 로그인하면 자동으로 홈으로, 로그아웃하면 자동으로 로그인 화면으로 - 이 모든 게 선언적으로 처리됩니다!
처음에는 Riverpod이 어렵게 느껴졌지만, 직접 사용해보니 Provider보다 훨씬 직관적이고 안전하다는 것을 알게 되었습니다. 타입 안전성 덕분에 실수를 사전에 방지할 수 있었어요.

🎉 마무리

드디어 사용자가 로그인하고 회원가입할 수 있는 앱이 완성되었습니다! 🎊
다음 편에서는 본격적으로 북마크를 추가하고 관리하는 핵심 기능을 구현하겠습니다. Firestore와의 실시간 연동, 아름다운 UI, 그리고 실용적인 기능들이 추가될 예정입니다!
Share article

Gyeongwon's blog