비트베이크

Flutter MVP에 5분 만에 휴대폰 인증 구현하기 (서류 제출 없이)

2026-03-20T01:04:06.320Z

FLUTTER-MVP-SMS

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 /sendPOST /verify, 이 두 API만 호출하면 끝이에요.

빠르게 MVP를 만들어야 하는 상황이라면, 서류 제출 없이 가입 후 바로 시작할 수 있는 EasyAuth를 추천합니다. 가입하면 10건 무료로 테스트할 수 있고, 건당 15~25원으로 기존 SMS API 대비 합리적인 가격에 이용할 수 있습니다. Firebase 설정에 시간 쓰지 말고, 5분 만에 인증 기능 붙이고 진짜 중요한 비즈니스 로직에 집중하세요.


태그: #Flutter #SMS인증 #OTP #MVP #모바일인증 #EasyAuth

비트베이크에서 광고를 시작해보세요

광고 문의하기

다른 글 보기

2026-06-18T06:01:39.386Z

2026년 부동산: 청약 대출 금리 전망과 성공적인 내집마련 전략

2026년 부동산 시장은 금리, 정책, 공급 등 다양한 변수로 인해 복잡합니다. 이 글에서는 2026년 상반기 부동산 시장 전망과 함께 정부 정책 변화, 주택담보대출 금리 최적화 전략, 그리고 성공적인 청약 당첨을 위한 지역 및 단지 선택 팁을 상세히 다룹니다. 현명한 내집마련 의사결정을 위한 실질적인 가이드를 제공합니다.

2026-06-18T05:01:46.246Z

AI 웨어러블 건강 최적화 2026: 나만의 맞춤 로드맵

2026년, AI 웨어러블 기기가 선사할 개인 맞춤 건강 관리의 혁신을 소개합니다. AI 코칭으로 최적화된 영양, 운동, 수면 관리와 예측 예방 전략으로 나만의 건강 로드맵을 설계하세요.

2026-06-18T05:01:38.929Z

2026 여름 출산준비물 리스트: 신생아부터 첫 휴가까지 필수템!

2026년 여름 출산을 앞둔 예비 부모를 위한 완벽 가이드! 신생아 여름용품부터 첫 휴가를 위한 필수템까지, 더위로부터 아기를 보호할 쿨링 아이템과 외출/휴가용품, 여름 의류를 상세히 소개합니다. 육아 선배들의 꿀팁과 체크리스트로 현명한 여름 출산준비를 시작하세요.

2026-06-18T05:01:32.846Z

2026년 AI PC 구매 가이드: 나에게 맞는 인공지능 노트북은?

2026년 AI PC 시대, NPU 기반 인공지능 노트북 구매를 위한 완벽 가이드! 코파일럿+ 핵심 기능부터 인텔, AMD, 퀄컴 제조사별 라인업 비교, 예산 및 용도별 추천 모델까지, 나에게 맞는 최신 AI PC를 현명하게 선택하는 방법을 알아보세요.

서비스

피드자주 묻는 질문고객센터

문의

비트베이크

레임스튜디오 | 사업자 등록번호 : 542-40-01042

경기도 남양주시 와부읍 수례로 116번길 16, 4층 402-제이270호

트위터인스타그램네이버 블로그