본문 바로가기
튜토리얼/앱

[Flutter] QR 코드 생성/스캔 앱 만들기 — qr_flutter + mobile_scanner

by 루까(Luka) 2026. 3. 3.
반응형

만든 이유

React로 날씨 앱을 만들었을 때, 딱 하나 아쉬운 게 있었다. "모바일 앱은 웹이랑 다르다"는 걸 체감하지 못한다는 것. 카메라나 센서처럼 네이티브 기능은 웹에서 온전히 다루기 어렵다. Flutter QR 스캐너를 선택한 이유는 간단하다. 카메라 접근 + 실시간 이미지 처리라는 딱 모바일스러운 기능을 가장 직접적으로 경험할 수 있는 예제다.


기술 선택

패키지 역할 선택 이유
qr_flutter 4.1 QR 코드 렌더링 위젯으로 바로 사용, 의존성 없음
mobile_scanner 6.0 카메라 스캔 ML Kit 기반, 정확도 높음

qr_code_scannerscan 같은 오래된 패키지도 있지만 유지보수가 중단됐거나 Xcode 15+ 호환 문제가 있다. mobile_scanner는 Google ML Kit를 쓰는 만큼 인식률이 좋고 pub.dev 관리도 활발하다.


앱 구조

lib/
├── main.dart              # MaterialApp, BottomNavigationBar
└── screens/
    ├── generate_screen.dart  # QR 생성 탭
    └── scan_screen.dart      # QR 스캔 탭

탭 구조가 전부다. 상태 관리도 각 Screen 내부의 setState로 충분하다.


핵심 구현

QR 생성 — qr_flutter

// generate_screen.dart
QrImageView(
  data: _qrData,
  version: QrVersions.auto,  // 데이터 길이에 따라 자동 버전 선택
  size: 220,
)

qr_flutterQrImageView 위젯 하나로 끝난다. data에 문자열 넣으면 QR 코드가 즉시 렌더링된다. URL이든 텍스트든 구분 없이 처리한다.

생성 후 클립보드 복사:

Future<void> _copyToClipboard() async {
  await Clipboard.setData(ClipboardData(text: _qrData));
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('클립보드에 복사됐다'), duration: Duration(seconds: 1)),
  );
}

mounted 체크는 필수다. 비동기 작업 후 위젯이 이미 dispose된 경우에 context를 접근하면 예외가 발생한다.


QR 스캔 — mobile_scanner

// scan_screen.dart
MobileScannerController _scannerController = MobileScannerController();

MobileScanner(
  controller: _scannerController,
  onDetect: _onDetect,
)

MobileScanner 위젯이 카메라 뷰 전체를 차지하고, onDetect 콜백으로 인식된 바코드를 받는다.

void _onDetect(BarcodeCapture capture) {
  if (!_isScanning) return;
  final barcode = capture.barcodes.firstOrNull;
  if (barcode == null || barcode.rawValue == null) return;

  final value = barcode.rawValue!;
  setState(() {
    _scannedValue = value;
    _isScanning = false;  // 한 번 인식 후 멈춤
    if (!_history.contains(value)) {
      _history.insert(0, value);
      if (_history.length > 10) _history.removeLast();
    }
  });
}

_isScanning 플래그로 중복 인식을 막는다. 카메라는 프레임마다 콜백을 호출하기 때문에, 한 번 인식되면 즉시 스캔을 멈추지 않으면 같은 QR 코드가 수십 번 처리된다.


iOS 권한 설정

ios/Runner/Info.plist에 추가 필수:

<key>NSCameraUsageDescription</key>
<string>QR 코드 스캔을 위해 카메라 접근 권한이 필요합니다.</string>

이게 없으면 카메라 접근 시 앱이 바로 크래시 난다.


스캔 UI 구성

┌─────────────────────────┐
│  QR 스캔                │
│  카메라에 QR 코드를...   │
├─────────────────────────┤
│  ┌───────────────────┐  │
│  │   카메라 뷰       │  │
│  │   ┌─────────┐    │  │
│  │   │ 가이드  │    │  │ ← 스캔 영역 가이드 오버레이
│  │   └─────────┘    │  │
│  └───────────────────┘  │
├─────────────────────────┤
│  스캔 결과 + 복사 버튼  │
│  최근 스캔 기록 (최대 10)│
└─────────────────────────┘

가이드 오버레이는 단순한 ContainerBorder만 그린 것. 실제 스캔 영역 제한은 아니고, 사용자에게 "여기에 카메라를 대세요"를 알려주는 UI일 뿐이다.


비교 테이블: QR 관련 패키지

패키지 생성 스캔 시뮬레이터 유지보수
qr_flutter 활발
mobile_scanner ⚠️ 활발
qr_code_scanner 중단
scan 중단
flutter_zxing 활발

flutter_zxing은 생성과 스캔을 모두 지원하고 시뮬레이터에서도 동작하지만, C++ ZXing 라이브러리를 컴파일하기 때문에 빌드 시간이 길다. 단일 앱에 두 기능을 모두 쓴다면 선택지다.


삽질 기록

mobile_scanner + iOS 26 시뮬레이터 충돌

mobile_scanner 6.x는 Google ML Kit를 사용하는데, ML Kit의 MLImage.framework가 iOS 시뮬레이터용 arm64 슬라이스를 포함하지 않는다. Xcode 26에서는 iOS 26 시뮬레이터가 arm64만 지원하기 때문에 링크 에러가 발생한다.

Error (Xcode): Building for 'iOS-simulator', but linking in object file
(.../MLImage.framework/MLImage[arm64][2]) built for 'iOS'

해결: iOS 실기기(arm64) 빌드로 전환. 카메라를 사용하는 앱은 어차피 실기기에서 테스트해야 한다. iOS 시뮬레이터 빌드는 qr_flutter QR 생성 화면만 확인 용도로 사용한다.

# 실기기 빌드 (코드사인 없이 컴파일 확인용)
flutter build ios --no-codesign
# → ✓ Built build/ios/iphoneos/Runner.app (35.2MB)

Flutter 3.x + Xcode 26 시뮬레이터 인식 문제

Generated.xcconfigEXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64가 자동 생성돼서, 시뮬레이터 빌드가 x86_64만 타겟했다. Xcode 26 + iOS 26 시뮬레이터는 arm64만 지원하므로 설치가 불가능했다.

근본 원인은 Flutter의 레거시 시뮬레이터 설정이 Xcode 26 환경과 충돌하는 것. ML Kit 문제와 맞물려 시뮬레이터에서 완전한 앱 실행은 불가능하다. 실기기가 필요하다.


마무리

QR 생성은 단순하다. qr_flutter 위젯 하나면 끝난다. 스캔은 좀 다르다. 카메라 권한, 프레임 중복 인식 방지, ML Kit 의존성 등 고려할 요소가 있다. 그래도 mobile_scanner가 처리해주는 게 많아서 코드 자체는 단순하다.

카메라 기능이 들어가는 순간 시뮬레이터의 한계가 드러난다. 이건 Flutter 문제가 아니라 iOS 시뮬레이터 자체의 한계다. 카메라 앱은 실기기에서 테스트한다.


기술 스택: Flutter 3.32 · Dart 3.8 · qr_flutter 4.1 · mobile_scanner 6.0 · iOS · Android
소스 코드: GitHub

반응형