Flutter MVP에 5분 만에 휴대폰 인증 구현하기 (서류 제출 없이)
2026-03-20T01:04:06.320Z
![]()
Flutter MVP에 5분 만에 휴대폰 인증 구현하기 (서류 제출 없이)
서류 지옥 없이 SMS 인증을 붙이고 싶다면?
사이드 프로젝트에 SMS 인증을 넣으려고 알아봤더니, 사업자등록증 제출하라고? 이용증명원까지? 토이 프로젝트 하나 만드는 건데 서류부터 막힌다면 개발 의욕이 확 꺾이죠.
Firebase Phone Auth도 방법이지만, Firebase 프로젝트 설정부터 SHA-1 키 등록, Google Cloud Console 설정까지 생각보다 손이 많이 갑니다. 특히 MVP 단계에서는 과한 설정이 될 수 있어요.
이 글에서는 REST API 두 개만으로 Flutter 앱에 SMS 인증을 구현하는 방법을 단계별로 알려드립니다. Firebase 없이, 복잡한 설정 없이, 코드 복사해서 바로 쓸 수 있는 실전 가이드입니다.
이 글에서 배울 내용
- Flutter에서 REST API로 SMS 인증 요청/검증하기
pin_code_fields패키지로 OTP 입력 UI 만들기http패키지로 POST 요청 보내기- 실무에서 쓸 수 있는 에러 처리와 타이머 구현
Step 1: 프로젝트 설정 및 패키지 설치
먼저 Flutter 프로젝트에 필요한 패키지를 추가합니다.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
pin_code_fields: ^8.0.1
flutter pub get
Step 2: SMS 인증 API 서비스 클래스 만들기
REST API와 통신하는 서비스 클래스를 작성합니다. Send(발송)와 Verify(검증), 단 두 개의 엔드포인트만 사용합니다.
// lib/services/auth_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class SmsAuthService {
static const String _baseUrl = 'https://api.easyauth.io/v1';
final String _apiKey;
SmsAuthService(this._apiKey);
/// 인증번호 발송
Future> sendCode(String phoneNumber) async {
final response = await http.post(
Uri.parse('$_baseUrl/send'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_apiKey',
},
body: jsonEncode({
'phone_number': phoneNumber,
}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('인증번호 발송 실패: ${response.statusCode}');
}
}
/// 인증번호 검증
Future> verifyCode({
required String phoneNumber,
required String code,
}) async {
final response = await http.post(
Uri.parse('$_baseUrl/verify'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_apiKey',
},
body: jsonEncode({
'phone_number': phoneNumber,
'code': code,
}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('인증 실패: ${response.statusCode}');
}
}
}
> 핵심 포인트: /send와 /verify, 이 두 엔드포인트만 알면 SMS 인증 구현은 끝입니다.
Step 3: 전화번호 입력 화면 만들기
// lib/screens/phone_input_screen.dart
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'otp_verify_screen.dart';
class PhoneInputScreen extends StatefulWidget {
const PhoneInputScreen({super.key});
@override
State createState() => _PhoneInputScreenState();
}
class _PhoneInputScreenState extends State {
final _phoneController = TextEditingController();
final _authService = SmsAuthService('YOUR_API_KEY');
bool _isLoading = false;
Future _sendVerificationCode() async {
final phone = _phoneController.text.trim();
if (phone.isEmpty || phone.length < 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('올바른 전화번호를 입력해주세요')),
);
return;
}
setState(() => _isLoading = true);
try {
await _authService.sendCode(phone);
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OtpVerifyScreen(
phoneNumber: phone,
authService: _authService,
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('발송 실패: $e')),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('휴대폰 인증')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'휴대폰 번호를 입력해주세요',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('인증번호가 SMS로 발송됩니다'),
const SizedBox(height: 24),
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
hintText: '01012345678',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _sendVerificationCode,
child: _isLoading
? const CircularProgressIndicator()
: const Text('인증번호 받기'),
),
],
),
),
);
}
}
Step 4: OTP 입력 및 검증 화면
pin_code_fields 패키지를 사용해 깔끔한 OTP 입력 UI를 만듭니다.
// lib/screens/otp_verify_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import '../services/auth_service.dart';
class OtpVerifyScreen extends StatefulWidget {
final String phoneNumber;
final SmsAuthService authService;
const OtpVerifyScreen({
super.key,
required this.phoneNumber,
required this.authService,
});
@override
State createState() => _OtpVerifyScreenState();
}
class _OtpVerifyScreenState extends State {
String _otpCode = '';
bool _isLoading = false;
int _remainingSeconds = 180;
Timer? _timer;
@override
void initState() {
super.initState();
_startTimer();
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_remainingSeconds > 0) {
setState(() => _remainingSeconds--);
} else {
timer.cancel();
}
});
}
String get _timerText {
final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
Future _verifyCode() async {
if (_otpCode.length != 6) return;
setState(() => _isLoading = true);
try {
final result = await widget.authService.verifyCode(
phoneNumber: widget.phoneNumber,
code: _otpCode,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('인증 성공!'),
backgroundColor: Colors.green,
),
);
// 인증 성공 후 다음 화면으로 이동
Navigator.popUntil(context, (route) => route.isFirst);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('인증 실패: $e')),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('인증번호 입력')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'${widget.phoneNumber}로\n인증번호를 보냈습니다',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'남은 시간: $_timerText',
style: TextStyle(
color: _remainingSeconds < 30 ? Colors.red : Colors.grey,
),
),
const SizedBox(height: 32),
PinCodeTextField(
appContext: context,
length: 6,
onChanged: (value) => _otpCode = value,
onCompleted: (_) => _verifyCode(),
pinTheme: PinTheme(
shape: PinCodeFieldShape.box,
borderRadius: BorderRadius.circular(8),
fieldHeight: 50,
fieldWidth: 45,
activeFillColor: Colors.white,
activeColor: Colors.blue,
selectedColor: Colors.blue,
inactiveColor: Colors.grey.shade300,
),
keyboardType: TextInputType.number,
animationType: AnimationType.fade,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _verifyCode,
child: _isLoading
? const CircularProgressIndicator()
: const Text('인증하기'),
),
TextButton(
onPressed: _remainingSeconds == 0
? () async {
await widget.authService.sendCode(widget.phoneNumber);
setState(() => _remainingSeconds = 180);
_startTimer();
}
: null,
child: const Text('인증번호 재발송'),
),
],
),
),
);
}
}
전체 코드 흐름 요약
사용자 → 전화번호 입력 → POST /send → SMS 수신
→ OTP 입력 → POST /verify → 인증 완료 ✓
정말 이게 전부입니다. 복잡한 Firebase 설정도, SHA 키 등록도, Google Cloud Console도 필요 없습니다.
실무 팁 & 보안 고려사항
1. API 키 보안
// ❌ 하드코딩 금지
const apiKey = 'sk-live-xxxxx';
// ✅ 환경변수 또는 --dart-define 사용
const apiKey = String.fromEnvironment('SMS_API_KEY');
flutter run --dart-define=SMS_API_KEY=your-key-here
2. 요청 제한 (Rate Limiting)
같은 번호로 반복 요청을 막아야 합니다. 클라이언트 측에서 타이머를 활용하고, 서버 측 rate limiting도 API 제공사에서 처리해줍니다.
3. 전화번호 포맷 검증
bool isValidKoreanPhone(String phone) {
return RegExp(r'^01[016789]\d{7,8}$').hasMatch(phone);
}
4. OTP 자동 입력 지원
Android에서는 sms_autofill 또는 otp_autofill 패키지를 추가하면 SMS가 오면 자동으로 OTP를 읽어와서 입력해줍니다. iOS에서는 키보드 상단에 자동 완성이 표시됩니다.
5. 에러 처리 패턴
try {
await authService.sendCode(phone);
} on SocketException {
// 네트워크 오류
} on TimeoutException {
// 타임아웃
} on Exception catch (e) {
// API 오류
}
Firebase vs REST API 방식 비교
| 항목 | Firebase Phone Auth | REST API 방식 | |------|-------------------|---------------| | 초기 설정 | Firebase 프로젝트 + SHA키 + Cloud Console | API 키 발급만 | | 서류 제출 | 불필요 (but 복잡한 설정) | 불필요 | | 소요 시간 | 30분~1시간 | 5분 | | 가격 | 월 10건 무료, 이후 건당 $0.06 | 건당 15~25원 | | 커스터마이징 | 제한적 | 자유로움 | | 플랫폼 의존성 | Firebase SDK 필요 | HTTP 요청만 |
마무리
Flutter MVP에 SMS 인증을 추가하는 건 생각보다 훨씬 간단합니다. POST /send와 POST /verify, 이 두 API만 호출하면 끝이에요.
빠르게 MVP를 만들어야 하는 상황이라면, 서류 제출 없이 가입 후 바로 시작할 수 있는 EasyAuth를 추천합니다. 가입하면 10건 무료로 테스트할 수 있고, 건당 15~25원으로 기존 SMS API 대비 합리적인 가격에 이용할 수 있습니다. Firebase 설정에 시간 쓰지 말고, 5분 만에 인증 기능 붙이고 진짜 중요한 비즈니스 로직에 집중하세요.
태그: #Flutter #SMS인증 #OTP #MVP #모바일인증 #EasyAuth
비트베이크에서 광고를 시작해보세요
광고 문의하기