만든 이유
React로 날씨 앱을 만들었을 때, 딱 하나 아쉬운 게 있었다. "모바일 앱은 웹이랑 다르다"는 걸 체감하지 못한다는 것. 카메라나 센서처럼 네이티브 기능은 웹에서 온전히 다루기 어렵다. Flutter QR 스캐너를 선택한 이유는 간단하다. 카메라 접근 + 실시간 이미지 처리라는 딱 모바일스러운 기능을 가장 직접적으로 경험할 수 있는 예제다.
기술 선택
| 패키지 | 역할 | 선택 이유 |
|---|---|---|
qr_flutter 4.1 |
QR 코드 렌더링 | 위젯으로 바로 사용, 의존성 없음 |
mobile_scanner 6.0 |
카메라 스캔 | ML Kit 기반, 정확도 높음 |
qr_code_scanner나 scan 같은 오래된 패키지도 있지만 유지보수가 중단됐거나 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_flutter는 QrImageView 위젯 하나로 끝난다. 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)│
└─────────────────────────┘
가이드 오버레이는 단순한 Container에 Border만 그린 것. 실제 스캔 영역 제한은 아니고, 사용자에게 "여기에 카메라를 대세요"를 알려주는 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.xcconfig에 EXCLUDED_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
'튜토리얼 > 앱' 카테고리의 다른 글
| [Flutter] Hive로 메모앱 만들기 — NoSQL 로컬 저장소 + TypeAdapter (0) | 2026.03.03 |
|---|---|
| [Flutter] 뽀모도로 타이머 앱 만들기 — 로컬 알림 + 커스텀 링 UI (1) | 2026.03.03 |
| [Flutter] 가계부 앱 만들기 — sqflite로 로컬 저장 구현 (0) | 2026.03.03 |