Flutter와 Firebase를 사용해 전화번호 인증 로그인 기능을 간단하게 구현해보았다.
Firebase Authentication을 통해 사용자에게 OTP(문자인증번호)를 전송하고 이를 확인하여 로그인 처리하는 기능을 구현하였다.
- 사용자가 전화번호를 입력하면 Firebase가 인증번호를 전송.
- 인증번호를 입력하면 로그인 성공 후 사용자 정보를 Firebase에 저장.
1. Firebase Authenticaiton 에서 전화번호
인증 방식 활성화 하기
Firebase Console에 접속 후 Authentication에 접속한다.
로그인 방법의 로그인 제공업자 에서 전화를 선택, 다음 버튼을 클릭한 후 사용 설정에 체크 한 후 저장을 누른다. 원한다면 테스트용 전화번호를 입력한다.
2. Android: SHA-1 키 및 SHA-256 키 생성.
Android Studio를 실행하여 Flutter Project를 연다.
터미널을 실행 후 cd android
를 입력해 android
폴더로 이동한다.
터미널에서 ./gredlew signingReport
명령어를 실행한다.
생성된 SHA-1 키와 SHA-246 를 메모장에 저장해둔다.
3. Firebase 안드로이드 프로젝트에 디지털 지문 추가
AuthRepository
는 Firebase Authentication을 활용해 전화번호 인증과 사용자 인증 관련 기능을 담당한다.
인터페이스와 구현체를 분리해 확장성을 고려하며, Firebase에 의존하지 않는 상위 계층의 설계를 목표로 한다.
주요목표 :
AuthRepository
는 전화번호 인증, SMS 코드 인증, 사용자 로그인 상태 확인 및 로그아웃과 같은 인증 관련 기능을 정의한다.
Firebase에 의존하지 않고 추상화된 기능만을 포함하여, 다른 인증 서비스로 쉽게 확장 가능하다.
코드는 다음과 같다.
/// Firebase Authentication을 통한 인증 관련 기능을 정의하는 AuthRepository 인터페이스
abstract class AuthRepository {
/// 전화번호로 인증 요청을 시작한다.
/// [phoneNumber]는 인증할 사용자의 전화번호다.
Future<void> requestPhoneVerification({required String phoneNumber});
/// SMS 코드로 사용자 인증을 수행한다.
/// [smsCode]는 사용자가 입력한 인증 코드다.
Future<String> verifySMSCode({required String smsCode});
/// Firebase 인증 상태 변화 감지 및 자동 로그인 여부를 확인한다.
/// 자동 로그인을 위해 앱 실행 시 호출되어 로그인 상태를 확인한다.
Future<bool> isUserLoggedIn();
/// Firebase 로그아웃 기능을 수행한다.
Future<void> logout();
/// 현재 인증된 사용자의 UID를 반환한다.
/// 로그인되지 않은 상태에서는 null을 반환한다.
String? getCurrentUserId();
}
AuthRepositoryImplement는 Firebase Authentication을 활용해 AuthRepository 인터페이스를 구현한다.
전화번호 인증, SMS 코드 인증, 로그아웃, 현재 사용자 확인과 같은 기능을 처리하며, Firebase의 메서드들을 캡슐화한다.
주요 목표 :
상태 관리 : 인증 ID(verificationId)와 재전송 토큰(forceResendToken)을 내부 상태로 관리.
재사용성 : Firebase Authentication의 복잡한 API를 단순화하여 서비스 계층에서 쉽게 호출 가능.
예외 처리 : Firebase 예외를 상위 계층으로 전달해 글로벌 예외 처리 로직과 통합.
코드는 다음과 같다.
/// Firebase Authentication을 통한 인증 관련 기능을 구현한 AuthRepositoryImplement
class AuthRepositoryImplement implements AuthRepository {
final FirebaseAuth _auth; // FirebaseAuth 인스턴스 주입
int? _forceResendToken; // 인증 코드 재전송을 위한 토큰
String? _verificationId; // 인증 ID 저장
AuthRepositoryImplement(this._auth);
/// 전화번호 인증 요청
Future<void> requestPhoneVerification({
required String phoneNumber,
}) async {
try {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) async {
try {
await _auth.signInWithCredential(credential); // 자동 인증 처리
} catch (e) {
rethrow; // 예외를 상위로 전달
}
},
verificationFailed: (FirebaseAuthException error) {
throw error; // 인증 실패 예외 전달
},
codeSent: (String verificationId, int? resendToken) {
_verificationId = verificationId; // 인증 ID 저장
_forceResendToken = resendToken; // 재전송 토큰 저장
},
codeAutoRetrievalTimeout: (String verificationId) {
_verificationId = verificationId; // 타임아웃 시 인증 ID 저장
},
timeout: const Duration(seconds: 60),
forceResendingToken: _forceResendToken,
);
} catch (e) {
rethrow; // 예외를 상위 레이어로 전달
}
}
/// SMS 코드로 사용자 인증
Future<String> verifySMSCode({
required String smsCode,
}) async {
try {
final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: smsCode,
);
final userCredential = await _auth.signInWithCredential(credential);
return userCredential.user?.uid ?? ''; // UID 반환
} catch (e) {
rethrow; // 예외를 상위로 전달
}
}
/// 현재 로그인 여부 확인
Future<bool> isUserLoggedIn() async {
return _auth.currentUser != null;
}
/// 로그아웃
Future<void> logout() async {
await _auth.signOut();
}
/// 현재 사용자 UID 반환
String? getCurrentUserId() {
return _auth.currentUser?.uid;
}
}
전화번호 인증을 구현하기 위해 다음과 같은 단계로 화면을 구성하였다.
1. 사용자가 전화번호 입력 화면에서 국가를 선택하고 전화번호를 입력.
2. 입력된 전화번호로 Firebase 인증 요청.
3. 인증번호 입력 화면으로 이동해 OTP를 확인하고 사용자 인증 처리.
설계 개요
1. 전화번호 입력:
- 전화번호는 숫자만 입력 가능하며, 유효성 검사가 포함된다.
2. 다음 버튼:
- 사용자가 전화번호 입력 후 "다음" 버튼을 누르면 Firebase로 인증 요청이 전송된다.
전화번호 입력 화면 코드 구현은 다음과 같다.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PhoneNumberInputScreen extends StatefulWidget {
const PhoneNumberInputScreen({super.key});
State<PhoneNumberInputScreen> createState() => _PhoneNumberInputScreenState();
}
class _PhoneNumberInputScreenState extends State<PhoneNumberInputScreen> {
final phoneNumberController = TextEditingController();
final globalKey = GlobalKey<FormState>();
final String phoneCode = '82'; // 한국 국가 코드
void dispose() {
phoneNumberController.dispose();
super.dispose();
}
Future<void> sendOTP() async {
final phoneNumber = phoneNumberController.text;
final fullPhoneNumber = '+$phoneCode$phoneNumber';
// Firebase 인증 요청 (테스트용 로그)
print('Sending OTP to: $fullPhoneNumber');
// 실제 요청 코드 추가 가능
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: const Text('전화번호를 입력해주세요'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Obri에서 당신의 계정을 인증합니다.',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 20),
SizedBox(
width: 250,
child: Row(
children: [
Expanded(
flex: 1,
child: TextFormField(
initialValue: '+$phoneCode', // 한국 국가 코드 자동 설정
readOnly: true,
decoration: const InputDecoration(
isDense: true,
prefixIconConstraints: BoxConstraints(
minWidth: 0,
minHeight: 0,
),
),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Form(
key: globalKey,
child: TextFormField(
controller: phoneNumberController,
decoration: const InputDecoration(
labelText: '전화번호 입력',
isDense: true,
),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '전화번호를 입력해주세요';
}
if (value.length < 9 || value.length > 11) {
return '유효한 전화번호를 입력해주세요';
}
return null;
},
),
),
),
],
),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {
FocusScope.of(context).unfocus();
final form = globalKey.currentState;
if (form == null || !form.validate()) {
return;
}
sendOTP();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('OTP 요청이 전송되었습니다.')),
);
},
child: const Text('다음'),
),
],
),
),
),
);
}
}
주요 구현 설명
1. 전화번호 입력
Form( key: globalKey, child: TextFormField( controller: phoneNumberController, keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], validator: (value) { if (value == null || value.trim().isEmpty) { return '전화번호를 입력해주세요'; } return null; }, ), );
2. Firebase로 인증 요청
authProvider
를 통해 Firebase로 인증 요청을 보낸다.await ref.read(authProvider.notifier).requestPhoneVerification( phoneNumber: '+$phoneCode$phoneNumber', );
설계 개요
1. OTP 입력:
- 사용자가 Firebase로부터 받은 인증번호(OTP)를 입력.
2. 인증 처리:
- 입력된 OTP를 Firebase로 전송해 사용자 인증을 완료.
3. 에러 처리:
- 인증 실패 시 사용자에게 오류 메시지를 표시.
인증번호 입력 화면 구현 코드는 다음과 같다.
class OPTScreen extends ConsumerWidget {
static const String routeName = '/otp-screen';
const OPTScreen({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: const Text('전화번호 인증 중'),
),
body: Center(
child: Column(
children: [
const Text('인증 번호를 전송했습니다'),
Container(
width: 240,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.green),
)),
child: OtpTextField(
numberOfFields: 6,
fieldWidth: 35,
hasCustomInputDecoration: true,
decoration: const InputDecoration(
hintText: '-',
counterText: '',
border: InputBorder.none,
hintStyle: TextStyle(fontWeight: FontWeight.bold),
),
onSubmit: (value) async {
await ref
.read(authProvider.notifier)
.verifySmsCode(smsCode: value);
},
),
),
],
),
),
),
);
}
}
주요 구현 설명
1. OTP 입력 필드
2. 인증 결과 처리
flutter run
명령어를 통해 flutter 앱을 실행한다.> flutter run
실행된 앱 화면에서 테스트용 전화번호나 실제로 인증번호를 받을 전화번호를 입력한다.
다음 버튼을 클릭하여 reCAPCHA를 진행한다.
실제 번호로 테스트를 진행한다면 다음화면과 같은 문자가 온다.
인즌번호 입력 화면에서 인증번호를 입력한다.
Firebase Authentication Console 에서 사용자가 추가된걸 확인할 수 있다.