본문으로 바로가기
기술

Web Bluetooth API 완벽 가이드 — 브라우저에서 BLE 디바이스 직접 제어

A
AlwaysCorp 기술팀· 기술·개발 콘텐츠 전문
||9분 읽기
#WebBluetooth#BLE#GATT#웹API#IoT#브라우저#Chromium#Web USB#센서

웹에서 직접 BLE를 다룬다는 것

스마트 전구 하나 켜겠다고 전용 앱을 깔던 시대가 길었습니다. Web Bluetooth API는 그 흐름을 깨고, HTTPS 페이지에서 사용자 클릭 한 번으로 BLE 디바이스에 직접 연결할 수 있게 해줍니다. Chromium 계열 브라우저(Chrome·Edge·Samsung Internet)에서 안정적으로 동작하며, 2024년 기준 전 세계 모바일 브라우저 점유율의 70% 이상을 커버합니다.

가장 강력한 활용처는 하드웨어 데모·관리도구·일회성 설정 마법사입니다. 디바이스를 처음 페어링할 때, 펌웨어를 업데이트할 때, QR 코드 옆에 "지금 연결하기" 버튼 하나만 두면 끝납니다. 사파리(iOS Safari)는 아직 미지원이지만, PWA + WebKit 우회가 아니라면 대부분의 데스크탑·안드로이드 사용 케이스를 커버할 수 있습니다.

GATT 모델만 이해하면 끝

BLE 디바이스는 Service → Characteristic → Descriptor의 3계층 트리로 자신을 노출합니다. 이게 GATT(Generic Attribute Profile)입니다. Web Bluetooth API는 이 트리를 그대로 객체로 매핑합니다.

BLE GATT 계층 구조 — Service와 Characteristic 모델

핵심만 외워두면 됩니다.

  • Service — 기능 모음. 표준 UUID(0x180D=심박, 0x180F=배터리)이거나 커스텀 128bit UUID
  • Characteristic — 실제 값. Read·Write·Notify 속성 조합
  • Notify — 디바이스가 값이 바뀔 때마다 push해줌 (폴링 불필요)

최소 코드로 연결하기

async function connectHeartRate() {
  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ["heart_rate"] }],
    optionalServices: ["battery_service"],
  });
  device.addEventListener("gattserverdisconnected", () => {
    console.log("연결 끊김");
  });

  const server = await device.gatt!.connect();
  const service = await server.getPrimaryService("heart_rate");
  const char = await service.getCharacteristic("heart_rate_measurement");

  await char.startNotifications();
  char.addEventListener("characteristicvaluechanged", (e) => {
    const v = (e.target as BluetoothRemoteGATTCharacteristic).value!;
    const flags = v.getUint8(0);
    const bpm = (flags & 0x1) ? v.getUint16(1, true) : v.getUint8(1);
    setHeartRate(bpm);
  });
}

권한과 사용자 제스처 — 가장 흔한 함정

requestDevice()반드시 사용자 클릭 핸들러 안에서만 호출할 수 있습니다. useEffect에서 페이지 로드 시 자동 실행하려고 하면 SecurityError로 즉시 거부됩니다. 이 제약은 광고나 악성 사이트가 사용자 모르게 디바이스 목록을 긁는 걸 막기 위한 의도된 설계입니다.

또 다른 함정은 선택 가능 서비스입니다. requestDevicefilters에 적은 서비스 외에는 접근이 막히기 때문에, 배터리 잔량까지 읽고 싶다면 반드시 optionalServices에 미리 선언해야 합니다. 사용자는 "이 디바이스에 연결" 한 번만 동의하면 그 안의 선언된 서비스 전부에 자동 권한이 부여됩니다.

자동 재연결 패턴

BLE는 거리·간섭으로 자주 끊깁니다. 사용자가 매번 다이얼로그를 다시 띄우게 하면 UX가 망가지므로, navigator.bluetooth.getDevices() API로 한 번 권한 받은 디바이스를 기억해뒀다 자동으로 다시 붙입니다.

const devices = await navigator.bluetooth.getDevices();
const known = devices.find((d) => d.name === "MyHeartRate");
if (known) {
  await known.watchAdvertisements();
  known.addEventListener("advertisementreceived", async () => {
    if (!known.gatt!.connected) await known.gatt!.connect();
  });
}

보안 모델

웹 보안 측면에서 Web Bluetooth는 다음 세 겹의 방어선을 갖습니다.

  1. HTTPS 강제localhost 외엔 SSL 필수
  2. 사용자 명시적 동의 — 디바이스 목록을 사용자가 직접 선택
  3. Origin 격리 — 한 사이트에서 받은 권한은 다른 사이트에서 못 씀

악의적 사이트가 슬쩍 디바이스 ID를 핑거프린팅으로 쓰는 걸 막기 위해, 개발자가 디바이스 ID를 직접 받지 못합니다. device.id는 origin과 디바이스 조합으로 매번 달라지는 해시값입니다.

다음 단계

Web Bluetooth가 자신감 있게 다뤄지면 Web USB(시리얼 디바이스), Web Serial(임베디드 보드), Web HID(게임패드·키오스크)도 같은 정신·같은 권한 모델로 정복할 수 있습니다. 사파리 미지원이 부담된다면 Capacitor의 BLE 플러그인으로 동일 코드를 모바일 앱으로 포장하는 것도 한 방법입니다. 핵심은, 더 이상 IoT가 네이티브 앱의 전유물이 아니라는 점입니다.

A

AlwaysCorp 기술팀

기술·개발 콘텐츠 전문

얼웨이즈 블로그에서 유용한 정보와 인사이트를 공유합니다.