본문 바로가기

React

React 앱에서 블루투스 디바이스 연동하기

브라우저의 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 기기와 빠르게 연동할 수 있는 실용적 방법입니다. 브라우저/보안 제약을 감안해 버튼 중심 플로우와 명확한 에러 처리, 최소 훅 설계로 생산성을 높이시기 바랍니다.