'로또 사고 싶은날' 개발기 6편: 앱의 생명! 초고속 QR 코드 스캐너 기능 구현 (CameraX & ML Kit)

2026. 4. 17. 07:21AI Product Building/Lotto App

반응형

 

안녕하세요! 직접 코딩하며 만들어 나가는 '로또 사고 싶은날' 앱 개발기, 대망의 6회차입니다. 지난 5회차에서는 동행복권 실시간 당첨 번호를 가져와 앱에 표시하는 방법을 다루었습니다. 이제 우리는 지난주 로또 1등 번호가 무엇인지는 알게 되었습니다. 그렇다면 다음으로 사용자가 할 행동은 무엇일까요? 바로 지갑에서 로또 용지 여러 장을 꺼내어 내 번호가 맞았는지 맞춰보는 것입니다. 로또 앱에서 수동으로 번호를 하나하나 입력하는 사람은 거의 없습니다. 즉, 이 앱의 가장 핵심이자 사용자가 가장 많이 쓰는 알파요 오메가 기능은 바로 'QR 스캐너'입니다.

과거의 로또 앱들을 사용해 보셨다면 아시겠지만, 종이가 조금 구겨져 있거나 어두운 방안에서는 카메라가 도무지 초점을 잡지 못해 속이 터졌던 경험이 한두 번쯤 있으실 겁니다. 앱 로딩 속도가 아무리 빨라도 QR 인식이 느리면 사용자들은 가차 없이 앱을 지워버립니다. 따라서 이번 편에서는 구글의 최신 머신러닝 기술인 ML KitCameraX를 결합하여, 어플을 켜자마자 종이를 스치기만 해도 0.1초 만에 당첨 여부를 판독해 내는 미칠듯한 스피드의 QR 스캐너를 구현하는 방법과, 스캔 된 난해한 URL 데이터를 어떻게 예쁘게 해독하는지 플러터(Flutter) 기반으로 상세히 파헤쳐 보겠습니다.

👉 '로또 사고 싶은날' 구글스토어 다운로드 바로가기 👈


1. 극강의 스피드를 위해: zxing을 버리고 Google ML Kit를 선택한 이유

플러터에서 QR 코드를 인식하는 가장 대중적인 구형 라이브러리들은 대부분 'zxing'이라는 단일 소프트웨어 디코딩 방식을 기반으로 합니다. 이 방식은 화면에 잡힌 픽셀을 CPU 연산만으로 하나하나 분석하기 때문에, 빛이 부족하거나 코드가 기울어져 있으면 인식률이 기하급수적으로 떨어집니다. 로또 앱 개발자에게 이는 재앙과도 같습니다.

이 문제를 타파하기 위해 저는 구글이 자체 개발한 온디바이스(기기 내장) 머신러닝 엔진인 ML Kit (기계학습 키트) 바코드 스캐닝 라이브러리를 도입하기로 결정했습니다. 또한 카메라 하드웨어 제어는 안드로이드의 최신, 가장 빠르고 안정적인 카메라 API인 CameraX를 이용했습니다. 다행히도 플러터 생태계에는 이 두 가지 최신 네이티브 기술을 완벽하게 다트로 포팅해 놓은 mobile_scanner라는 훌륭한 패키지가 존재합니다. 2회차에서 pubspec.yaml에 이 패키지를 추가했던 것 기억하시죠? 이 패키지를 사용하면 종이의 질감, 빛의 반사, 심지어 거꾸로 뒤집힌 바코드조차 머신러닝 모델이 추론을 통해 즉각적으로 인식해 내는 놀라운 마법을 구현할 수 있습니다.

2. 부드러운 스캐너 화면 그리기 및 카메라 권한(Permission) 처리

카메라 기능을 쓰려면 안드로이드와 iOS 양쪽 모두 철저한 '권한(Permission)' 동의 절차를 거쳐야 합니다. 만약 사용자가 권한을 "거부"했는데 무작정 카메라 위젯을 띄우려고 하면 앱은 펑 하고 터지듯 종료됩니다(Crash). 따라서 스캐너 화면에 진입하기 전에 반드시 permission_handler 패키지를 이용해 권한 상태를 확인하고, 권한이 없다면 "로또 번호를 확인하려면 카메라 권한이 필요합니다"라는 친절한 안내 문구와 함께 설정 창으로 유도하는 안전장치를 구현해야 합니다.

위험을 넘겨 카메라가 켜졌다면, 이제 예쁜 UI를 씌워줄 차례입니다. MobileScanner 위젯을 전체 화면에 깔아준 뒤, 그 위에 Stack 위젯을 사용하여 투명한 사각형 구멍이 뚫린 반투명 검은색 오버레이(Overlay)를 덮어줍니다. 사용자의 시선을 화면 중앙의 사각형으로 집중시키기 위함입니다. 여기에 끝없이 위아래로 움직이는 녹색 레이저 선 애니메이션(AnimationController 활용)을 추가하여, 앱이 현재 스캔을 위해 '살아 움직이고 있다'는 피드백(UX)을 강렬하게 주었습니다.

3. 동행복권 QR 코드 URL 구조 완벽 해부 및 데이터 추출 로직

자, 이제 카메라 렌즈를 로또 용지 위쪽의 QR 코드에 갖다 대면 카메라 라이브러리는 0.1초 만에 텍스트 덩어리를 우리에게 던져줍니다. 여러분이 로또 용지에 있는 QR 코드를 기본 카메라로 찍었을 때 넘어가게 되는 주소가 바로 이 텍스트입니다. 그 구조는 대략 이렇습니다.

http://m.dhlottery.co.kr/?v=1100m011223344546q050607080910

암호문 같아 보이지만 규칙이 매우 명확합니다. ?v= 바로 뒤에 오는 4자리 숫자인 1100이 '회차' 번호입니다. 그 다음 영어 소문자(m, q 등)는 각각 해당 줄(A열, B열...)을 구분하는 구분자입니다. 문자와 문자 사이에 있는 12자리 숫자 011223344546이 바로 내 로또 번호 6개(01, 12, 23, 34, 45, 46)가 두 자리씩 차례로 붙어있는 형태입니다. 처음 이 패턴을 분석해 내고 어찌나 짜릿했는지 모릅니다.

4. 정규 표현식(RegEx)으로 번호 발라내기 - 코드로 보기

웹 브라우저를 열어 저 URL로 이동하게 놔두면 촌스러운 동행복권 모바일 웹페이지가 뜹니다. 우리는 '로또 사고 싶은날' 앱만의 특화된 아름다운 결과 창을 보여주어야 하므로, URL 넘어가기를 막고 직접 저 문자열(URL) 속에서 회차 번호와 내 번호 리스트를 깔끔하게 추출해야 합니다. 이를 위해 문자열 치환과 정규표현식(Regular Expression) 기술을 활용하여 QrDataParser라는 파싱 모듈을 구현했습니다.

// lib/utils/qr_data_parser.dart

class QrDataParser {
  /// 로또 URL 문자열을 받아 [회차 번호]와 [내가 구매한 로또 번호 리스트]를 Map으로 반환합니다.
  static Map<String, dynamic> parseLottoUrl(String url) {
    try {
      // 1. 유효한 동행복권 URL인지 1차 방어 (엉뚱한 과자 봉지 QR을 찍는 경우 대비)
      if (!url.contains('dhlottery.co.kr/?v=')) {
        return {'isValid': false};
      }

      // 2. 'v=' 이후의 핵심 데이터 블록만 잘라내기
      String dataBlock = url.split('v=')[1]; 
      
      // 3. 첫 4자리는 항상 회차 번호임 (예: '1100')
      int drwNo = int.parse(dataBlock.substring(0, 4));
      
      // 4. 회차 번호를 제외한 나머지 뒷부분은 게임 데이터
      String gameData = dataBlock.substring(4);
      
      // 5. a, b, c, m, q 등의 소문자를 구분선(,)으로 억지로 치환
      // 그러면 "m0112...q0506..." 가 ",0112...,0506..." 형태로 바뀜
      gameData = gameData.replaceAll(RegExp(r'[a-z]'), ',');
      
      // 6. 콤마를 기준으로 분리하여 배열(List)로 만든 뒤, 빈 문자열 제거
      List<String> games = gameData.split(',').where((e) => e.length > 10).toList();
      
      List<List<int>> myLines = [];
      
      // 7. 각 줄(12자리 문자열)을 2글자씩 끊어서 숫자로 바꾸어 저장 
      // (예: "0112" -> 숫자로 파싱하여 [1, 12] 완성)
      for (var line in games) {
        List<int> numbers = [];
        for (int i = 0; i < 12; i += 2) {
          numbers.add(int.parse(line.substring(i, i + 2)));
        }
        myLines.add(numbers); // 한 줄 데이터 완성
      }

      return {
        'isValid': true,
        'drwNo': drwNo,
        'myLines': myLines, // A~E열까지 내가 산 모든 로또 번호 2차원 배열 완성!
      };
    } catch (e) {
      // 파싱 중간에 글자가 깨지거나 규칙과 안 맞으면 무조건 에러 처리
      print('🛠 QR Parsing Error: $e');
      return {'isValid': false};
    }
  }
}

위 코드의 parseLottoUrl 함수에 스캔된 URL을 통과시키기만 하면, 마법처럼 '1100회차' 이고 내가 구매한 번호 5열이 Dart 코딩에서 즉각적으로 연산하기 수월한 정수형 이중 배열(List<List<int>>) 형태로 깔끔하게 떨어져 나옵니다. 정말 직관적이고 강력한 방법이죠?

이렇게 파싱해서 추출해 낸 '나의 번호'와, 지난 5회차에서 API로 가져와 둔 '실제 1등 번호'를 비교 로직(for 문과 교집합 비교)에 태우기만 하면 "축하합니다! 5만 원 당첨입니다!"라는 결과를 화면에 띄울 수 있게 되었습니다! QR 스캐너 연동을 끝내고 제가 실제 용지로 첫 스캔 테스트를 했을 때의 그 소름 돋게 빨랐던 쾌감은 아직도 생생합니다.

그런데 말입니다. 만약 사용자가 로또 용지 여러 장을 확인한다면, 이 결과들을 어딘가에는 저장해 두어야 앱 안에 차곡차곡 '나의 최근 스캔 내역'이 텍스트 통계 형태로 쌓이겠죠? 다음번 7회차 포스팅에서는 제가 그토록 강조했던 핵심 기능, 엄청나게 빠른 앱 구동 속도의 1등 공신인 **로컬 DB(Hive)를 활용해 스캔 내역 및 사용자 데이터를 기기 내부에 아주 안전하게 저장하는 심화 과정**을 들여다보도록 하겠습니다!

💡 답답했던 구형 스캐너는 잊으세요. 카메라를 대자마자 결과가 튀어나오는 신세계를 폰에서 직접 테스트해보세요!
👉 [초스피드 QR 인식! 로또 사고 싶은날 - 안드로이드 다운로드]

```

반응형