브라우저의 Web Bluetooth API를 활용하면 React 앱에서 BLE(GATT) 디바이스와 직접 통신할 수 있습니다. 이 글은 실무 관점에서 최소 구현으로 빠르게 연결하고, 읽기/쓰기/알림을 처리하며, 배포 전 체크포인트까지 정리합니다.
1. Web Bluetooth 개요와 한계
Web Bluetooth는 보안 문맥(HTTPS)에서 사용자 제스처를 통해 BLE 디바이스를 검색하고 GATT 서비스/특성에 접근하는 표준 API입니다. 크롬/엣지(데스크톱/안드로이드)에서 안정적으로 동작하며, iOS Safari 및 데스크톱 Safari는 아직 공식 지원하지 않습니다. 따라서 크로스 브라우저 대응이 필요합니다.
2. 사전 체크: HTTPS, 지원 브라우저, 권한
로컬은 https://localhost 또는 http://localhost(개발 예외)에서 테스트하며, 프로덕션은 반드시 HTTPS여야 합니다. 사용자 제스처(클릭 등) 없이 requestDevice를 호출하면 실패합니다.
// 브라우저/보안 문맥 체크
if (!('bluetooth' in navigator)) {
console.warn('이 브라우저는 Web Bluetooth를 지원하지 않습니다.');
}
if (!isSecureContext) {
console.warn('HTTPS 환경에서만 Web Bluetooth가 동작합니다.');
}
3. 기본 흐름: 검색 → 연결 → 서비스/특성
1) navigator.bluetooth.requestDevice로 사용자에게 디바이스 선택 UI 표시 → 2) device.gatt.connect()로 GATT 서버 연결 → 3) server.getPrimaryService로 서비스 접근 → 4) service.getCharacteristic으로 특성 접근 → 5) readValue/writeValue 또는 startNotifications로 통신합니다.
4. React 컴포넌트 예제: 배터리 서비스 읽기
가장 단순한 흐름을 버튼 핸들러에 구현합니다. 주의: requestDevice는 반드시 클릭 등 사용자 제스처에서 호출해야 합니다.
import React, { useRef, useState } from 'react';
function BleDemo() {
const [battery, setBattery] = useState(null);
const deviceRef = useRef(null);
const serverRef = useRef(null);
const onDisconnected = () => {
setBattery(null);
};
const connect = async () => {
try {
if (!navigator.bluetooth) {
alert('이 브라우저는 Web Bluetooth를 지원하지 않습니다.');
return;
}
const device = await navigator.bluetooth.requestDevice({
// 가능하면 filters를 사용하면 검색 목록이 간결해집니다.
filters: [{ services: ['battery_service'] }],
optionalServices: ['device_information']
});
deviceRef.current = device;
device.addEventListener('gattserverdisconnected', onDisconnected);
const server = await device.gatt.connect();
serverRef.current = server;
const service = await server.getPrimaryService('battery_service');
const char = await service.getCharacteristic('battery_level');
const value = await char.readValue();
const percent = value.getUint8(0);
setBattery(percent);
} catch (err) {
console.error(err);
alert('BLE 연결 실패: ' + (err?.message || String(err)));
}
};
const disconnect = () => {
try {
const device = deviceRef.current;
if (device?.gatt?.connected) {
device.removeEventListener('gattserverdisconnected', onDisconnected);
device.gatt.disconnect();
}
setBattery(null);
} catch (_) {}
};
return (
<div>
<button onClick={connect}>연결 및 배터리 읽기</button>
<button onClick={disconnect}>연결 해제</button>
<p>배터리: {battery !== null ? battery + '%' : '미연결'}</p>
</div>
);
}
export default BleDemo;
5. 알림 구독(Notifications) 수신
센서 스트림을 받아야 한다면 startNotifications를 사용합니다. 값은 DataView로 수신되며, 파싱은 기기 프로토콜에 맞춰 처리합니다.
// 예: 커스텀 서비스/특성 UUID
const SERVICE_UUID = '00001234-0000-1000-8000-00805f9b34fb';
const CHAR_NOTIFY_UUID = '00005678-0000-1000-8000-00805f9b34fb';
async function subscribe(server, onData) {
const service = await server.getPrimaryService(SERVICE_UUID);
const ch = await service.getCharacteristic(CHAR_NOTIFY_UUID);
ch.addEventListener('characteristicvaluechanged', (e) => {
const dv = e.target.value; // DataView
// 예: 16비트 정수 2개 파싱
const v1 = dv.getInt16(0, /* littleEndian */ true);
const v2 = dv.getInt16(2, true);
onData({ v1, v2 });
});
await ch.startNotifications();
return () => {
ch.stopNotifications().catch(() => {});
ch.removeEventListener('characteristicvaluechanged', () => {});
};
}
6. 커스텀 훅 설계 예시
프로젝트에서 재사용하려면 최소 기능 훅을 만듭니다. 상태와 연결/해제, 읽기/쓰기만 노출하는 형태가 실용적입니다.
import { useCallback, useRef, useState } from 'react';
export function useWebBluetooth() {
const [connected, setConnected] = useState(false);
const [error, setError] = useState(null);
const deviceRef = useRef(null);
const serverRef = useRef(null);
const connect = useCallback(async ({ filters, optionalServices }) => {
setError(null);
try {
const device = await navigator.bluetooth.requestDevice({ filters, optionalServices });
deviceRef.current = device;
device.addEventListener('gattserverdisconnected', () => setConnected(false));
const server = await device.gatt.connect();
serverRef.current = server;
setConnected(true);
return { device, server };
} catch (e) {
setError(e);
throw e;
}
}, []);
const disconnect = useCallback(() => {
try {
const d = deviceRef.current;
if (d?.gatt?.connected) d.gatt.disconnect();
setConnected(false);
} catch (e) { setError(e); }
}, []);
const read = useCallback(async (serviceUuid, charUuid) => {
const s = await serverRef.current.getPrimaryService(serviceUuid);
const c = await s.getCharacteristic(charUuid);
return c.readValue();
}, []);
const write = useCallback(async (serviceUuid, charUuid, data) => {
const s = await serverRef.current.getPrimaryService(serviceUuid);
const c = await s.getCharacteristic(charUuid);
await c.writeValue(data); // data: ArrayBufferView
}, []);
return { connected, error, connect, disconnect, read, write };
}
7. 권한/에러 처리 팁
- 사용자 제스처에서만 requestDevice를 호출합니다. 모달/버튼 클릭 핸들러 내부에 배치합니다.
- filters를 가급적 사용하고, 필요 서비스는 optionalServices에 명시합니다. acceptAllDevices는 UX와 보안 면에서 최소화합니다.
- 연결 실패/해제 시 재시도 버튼과 상태 초기화를 제공합니다. gattserverdisconnected 이벤트로 UI를 업데이트합니다.
- DataView는 엔디언과 비트폭을 정확히 맞춥니다. 기기 프로토콜 문서를 확인합니다.
- 동일 기기 재연결 문제 시 OS 블루투스 설정에서 기기를 제거 후 재시도하거나, 브라우저 권한을 초기화합니다.
8. 보안 및 개인정보 고려
디바이스 식별자나 측정 데이터는 필요 최소한만 저장하고, 로컬 저장 시 암호화 또는 세션 단위 저장을 고려합니다. 민감 데이터는 서버 전송 전 동의와 마스킹 정책을 마련합니다.
9. 배포 체크리스트
- HTTPS 활성화 및 HSTS 적용
- 지원 브라우저 안내 배너와 대안 경로(네이티브 앱/데스크톱 앱) 제공
- 연결 상태, 권한 거부, 미지원 브라우저 메시지 등 UX 시나리오 검증
- chrome://bluetooth-internals로 연결/서비스/특성 확인
- PWA라면 백그라운드 동작이 필요할 수 있으나, 브라우저 제약을 인지하고 설계합니다.
10. 미지원 환경 대안
- 모바일 iOS/사파리: React Native + react-native-ble-plx 등 네이티브 BLE 권장
- 데스크톱 주변기기: WebUSB 또는 Web Serial이 적합한 경우 고려
11. 디버깅 빠른 팁
- chrome://bluetooth-internals에서 광고, GATT 테이블, 로그를 확인합니다.
- navigator.bluetooth.getDevices()로 이전에 허용된 디바이스 목록을 가져와 자동 재연결 UX를 구성할 수 있습니다(사용자 허용이 선행된 기기에 한함).
- 연결이 잦게 끊기면 RSSI, 전원 상태, 간섭 환경(2.4GHz)을 점검합니다.
정리하면, Web Bluetooth는 React 앱에서 IoT 기기와 빠르게 연동할 수 있는 실용적 방법입니다. 브라우저/보안 제약을 감안해 버튼 중심 플로우와 명확한 에러 처리, 최소 훅 설계로 생산성을 높이시기 바랍니다.
'React' 카테고리의 다른 글
| React 앱에서 사용자 활동 로그 시각화하기 (1) | 2026.07.10 |
|---|---|
| React에서 브라우저 Speech Synthesis API 활용하여 음성 출력하기 (0) | 2026.07.09 |
| React에서 오프라인 모드 전환 UI 구성하기 (0) | 2026.07.08 |
| React 앱에서 지도 경로 탐색(Route Finding) 기능 추가하기 (0) | 2026.07.08 |
| React에서 스크롤 기반 콘텐츠 페이드 인/아웃 구현하기 (0) | 2026.07.08 |