“내가 앱에 등록한 BLE(저전력 블루투스) 기기가 주변에 있는지 주기적으로 확인하는 기능을 구현하라” 라는 요구사항을 전달받았다면 어떻게 구현하시겠습니까?
회사에서 BLE 스캔을 통해 만들어진 앱을 백그라운드 상태에서도 구현해야하는 상황이었는데요. 기존에 작성된 코드에서는 flutter_blue_plus
라는 BLE 스캔과 연결을 동시에 수행할 수 있는 대표적인 라이브러리를 사용중이었습니다. 백그라운드에서도 사용 가능하도록 프로젝트에서는 세팅이 되어있었지만, 이상하게도 백그라운드에서는 BLE 스캔이 동작하지 않았습니다.
기존에 많이 쓰이는 flutter_blue_plus
같은 라이브러리는 OS가 제공하는 표준 BLE 스캔 API를 그대로 호출합니다. 문제는 이 표준 BLE 스캔이 OS 차원의 절전 정책(DOZE, Background Execution Limit)이나 권한 제약에 가로막혀, 백그라운드에서 안정적으로 작동하기 어렵다는 점입니다. 안드로이드 기기의 경우 Foreground Service를 사용하면 어느 정도 가능하지만, 화면을 껐을때 스캔이 멈추는 상황이 발생했습니다. 또한 iOS는 백그라운드 BLE 스캔을 더욱 제한적으로만 허용하여, 사실상 구현이 까다롭다는 한계가 있습니다.
이처럼 기본 BLE 스캔이 백그라운드에서 잘 동작하지 않는 이유는, OS가 ‘평범한 BLE 스캔’을 절전에 의해 제약하기 때문입니다. 반면 Beacon(iBeacon, Eddystone, AltBeacon 등) 방식을 쓰게 되면, 안드로이드, iOS 모두에서 “위치 서비스”로 분류되어 화면이 꺼진 상황에서도 스캔을 지속할 수 있습니다. 특히 iBeacon은 Apple이 정의한 BLE Manufacturer Data(0x004C) 규격으로, iOS에서는 이를 Core Location
서비스로 해석해 백그라운드에서도 지역(Region)을 모니터링해 주며, 안드로이드도 Android Beacon Library 등의 기술을 통해 유사한 형태를 지원합니다. 이 때문에 Beacon(특히 iBeacon)이 GATT 연결용 BLE 기기와 달리, 백그라운드 상태에서도 꾸준히 광고를 수신하고 진입·이탈 같은 이벤트를 파악할 수 있게 됩니다.
flutter_beacon
패키지를 사용한 간단한 iBeacon 스캔 예시는 아래와 같습니다. 이 코드는 20초 간격으로 스캔을 시작하고, 10초 동안 Ranging을 수행한 뒤 결과를 서버에 데이터를 전송하는 구조입니다. 스캔 중에는 발견된 맥주소를 추려서 scannedMacAddresses
에 담고, 앱 내 AssetList
의 Asset과 대조하여 내가 등록한 에셋이 근처에서 스캔되었는가를 판단합니다.
class BLEProvider extends ChangeNotifier {
Timer? _scanTimer;
bool _isIBeaconScanning = false;
// 스캔한 데이터의 맥주소 목록
final Set<String> scannedMacAddresses = {};
// BEACON - Region Monitoring 설정
StreamSubscription<RangingResult>? _iBeaconRangingSub;
final List<Region> mkTagRegions = [
Region(
identifier: '식별할 ID',
proximityUUID: '식별할 UUID', // iOS는 not null이다
),
];
/// 외부에서 사용할 메서드
Future<void> beaconInitSetting() async {
_scanTimer = Timer.periodic(
const Duration(seconds: 20),
(_) => startBeaconScan(),
);
}
/// 20초마다 BEACON 스캔이 실행됨
Future<void> startBeaconScan() async {
if (_isIBeaconScanning) return;
_isIBeaconScanning = true;
// 스캔을 시작하기 전에 이전 값들을 비워놓기
scannedMacAddresses.clear();
try {
await flutterBeacon.initializeScanning;
_iBeaconRangingSub = flutterBeacon.ranging(mkTagRegions).listen((result) {
for (final beacon in result.beacons) {
final mac = beacon.macAddress;
if (mac != null && mac.isNotEmpty) {
final macNoColon = mac.replaceAll(':', '').toUpperCase();
scannedMacAddresses.add(macNoColon);
}
}
});
await Future.delayed(const Duration(seconds: 10));
await _iBeaconRangingSub?.cancel();
final context = nav.currentState!.overlay!.context;
final assetListProvider = Provider.of<AssetListProvider>(
context,
listen: false,
);
// 전역 Provider에서 불러온 에셋의 맥주소
for (var asset in assetListProvider.allAssetList) {
final tagId = asset.tagId.toUpperCase();
if (scannedMacAddresses.contains(tagId)) {
// 내가 가지고있는 에셋 목록과 일치하는 맥주소 발견!
} else {
// 내가 가지고있는 에셋 목록과 일치하는 맥주소 발견 못 함
}
}
} catch (e, trace) {
// 에러 처리
} finally {
_isIBeaconScanning = false;
}
}
}
코드에서 확인할 수 있듯이, flutter_beacon
은 Region(Ranging)을 통해 iBeacon 데이터를 받아오고, 이를 바탕으로 원하는 로직을 수행합니다. 백그라운드 상태에서도 OS가 위치 서비스 차원에서 Beacon Ranging을 계속 시도해 주기 때문에, 기존의 표준 BLE 스캔 코드보다 훨씬 안정적으로 동작한다는 것이 특징입니다.
iOS에서 이 코드를 동작시키려면 Info.plist에 위치 권한(Always)과 Background Modes(Location Updates)를 추가로 세팅해야 합니다. 안드로이드도 위치 권한(Bluetooth 스캔 권한 포함)과 포그라운드 서비스 사용, 그리고 일부 제조사(샤오미·화웨이 등)의 배터리 절전 예외 처리를 해 주어야 백그라운드 동작이 안정적입니다. 또한 iOS에서는 Beacon 프로토콜상 맥주소가 노출되지 않는 경우가 많아, MAC 기반 식별이 필요하다면 펌웨어 측에서 Manufacturer Data나 Service Data에 별도로 해당 정보를 넣는 방법을 고려해야 합니다.
백그라운드에서 저전력 블루투스 광고 데이터를 원활히 스캔하기 위해 iBeacon을 적용하는 방법에 대해 글을 작성해보았습니다. 일반 BLE 스캔 라이브러리에서는 광고를 단순히 수신하는 기능만 제공하기 때문에 OS의 절전 정책을 우회하기 어려운데요. Region Monitoring과 같은 "위치 서비스" 기반을 활용하여 안정적인 스캔을 할 수 있는 flutter_beacon
이나 beacons_plugin
등 Beacon 전용 패키지를 사용해보시는 것을 권장합니다.
잘못된 내용이 있거나 추가로 궁금하신 부분은 편하게 댓글 남겨주세요! 확인 후 답변 남겨놓도록 하겠습니다.
글 읽어주셔서 감사합니다.