Web Bluetooth API로 브라우저에서 하드웨어 제어하기
Web Bluetooth API로 브라우저에서 하드웨어 제어하기
웹 브라우저에서 Bluetooth 디바이스를 제어할 수 있다는 사실을 알고 계신가요? Web Bluetooth API를 사용하면 별도의 네이티브 앱 없이도 BLE(Bluetooth Low Energy) 디바이스와 통신할 수 있습니다.
최근 포토카드 키오스크 프로젝트에서 ESP32를 웹에서 직접 제어하는 기능을 구현했습니다. 이 경험을 바탕으로 Web Bluetooth API 사용법을 정리합니다.
Web Bluetooth API란?
Web Bluetooth API는 웹 페이지에서 Bluetooth Low Energy 디바이스에 연결하고 통신할 수 있게 해주는 Web API입니다.
지원 환경
- Chrome (Windows, macOS, Linux, Android)
- Edge (Chromium 기반)
- Opera
- iOS에서는 지원하지 않음 (Safari 미지원)
보안 요구사항
Web Bluetooth는 보안상의 이유로 다음 조건에서만 동작합니다:
- HTTPS 환경 또는 localhost
- 사용자 제스처(클릭 등)를 통한 연결 요청
- 사용자가 직접 디바이스를 선택해야 함
기본 사용법
1. 디바이스 검색 및 연결
// BLE 디바이스 연결
const connectToDevice = async () => {
try {
// 사용자에게 디바이스 선택 팝업 표시
const device = await navigator.bluetooth.requestDevice({
filters: [
{ services: ['4fafc201-1fb5-459e-8fcc-c5c9c331914b'] }, // 서비스 UUID로 필터링
// 또는 이름으로 필터링
// { name: 'MyDevice' },
// { namePrefix: 'Arduino' }
]
});
console.log('디바이스 선택됨:', device.name);
// GATT 서버에 연결
const server = await device.gatt!.connect();
console.log('연결 성공');
return { device, server };
} catch (error) {
console.error('연결 실패:', error);
throw error;
}
};
requestDevice를 호출하면 브라우저가 주변 BLE 디바이스 목록을 보여주고, 사용자가 연결할 디바이스를 선택합니다.
2. 서비스와 특성(Characteristic) 가져오기
BLE 통신은 서비스(Service)와 특성(Characteristic) 개념으로 구성됩니다.
- 서비스: 관련된 기능들의 그룹 (예: 심박수 측정 서비스)
- 특성: 실제 데이터를 읽고 쓰는 단위 (예: 현재 심박수 값)
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
const CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
const getCharacteristic = async (server: BluetoothRemoteGATTServer) => {
// Primary Service 가져오기
const service = await server.getPrimaryService(SERVICE_UUID);
// Characteristic 가져오기
const characteristic = await service.getCharacteristic(CHARACTERISTIC_UUID);
return characteristic;
};
3. 데이터 쓰기
const sendCommand = async (characteristic: BluetoothRemoteGATTCharacteristic, command: string) => {
const encoder = new TextEncoder();
const data = encoder.encode(command);
await characteristic.writeValue(data);
console.log('명령 전송:', command);
};
// 사용 예시
await sendCommand(characteristic, 'TRIGGER');
4. 데이터 읽기
// 단일 읽기
const readValue = async (characteristic: BluetoothRemoteGATTCharacteristic) => {
const value = await characteristic.readValue();
const decoder = new TextDecoder();
return decoder.decode(value);
};
// 알림(Notification) 구독 - 실시간 데이터 수신
const subscribeToNotifications = async (characteristic: BluetoothRemoteGATTCharacteristic) => {
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const target = event.target as BluetoothRemoteGATTCharacteristic;
const value = target.value;
const decoder = new TextDecoder();
console.log('수신된 데이터:', decoder.decode(value));
});
};
React에서 사용하기
실제 프로젝트에서 사용한 커스텀 훅입니다.
// hooks/useBleConnection.ts
import { useState, useCallback } from 'react';
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
const CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
interface BleState {
device: BluetoothDevice | null;
isConnected: boolean;
isConnecting: boolean;
error: string | null;
}
export const useBleConnection = () => {
const [state, setState] = useState<BleState>({
device: null,
isConnected: false,
isConnecting: false,
error: null
});
const [characteristic, setCharacteristic] =
useState<BluetoothRemoteGATTCharacteristic | null>(null);
const connect = useCallback(async () => {
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
// 1. 디바이스 선택
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }]
});
// 2. 연결 해제 이벤트 리스너
device.addEventListener('gattserverdisconnected', () => {
setState(prev => ({ ...prev, isConnected: false }));
setCharacteristic(null);
});
// 3. GATT 서버 연결
const server = await device.gatt!.connect();
// 4. 서비스 및 특성 가져오기
const service = await server.getPrimaryService(SERVICE_UUID);
const char = await service.getCharacteristic(CHARACTERISTIC_UUID);
setCharacteristic(char);
setState({
device,
isConnected: true,
isConnecting: false,
error: null
});
} catch (error) {
const message = error instanceof Error ? error.message : '연결 실패';
setState(prev => ({
...prev,
isConnecting: false,
error: message
}));
}
}, []);
const disconnect = useCallback(() => {
if (state.device?.gatt?.connected) {
state.device.gatt.disconnect();
}
setState({
device: null,
isConnected: false,
isConnecting: false,
error: null
});
setCharacteristic(null);
}, [state.device]);
const sendCommand = useCallback(async (command: string) => {
if (!characteristic) {
throw new Error('연결되지 않음');
}
const encoder = new TextEncoder();
await characteristic.writeValue(encoder.encode(command));
}, [characteristic]);
return {
...state,
connect,
disconnect,
sendCommand
};
};
컴포넌트에서 사용
// components/BleController.tsx
import { useBleConnection } from '@/hooks/useBleConnection';
export const BleController = () => {
const {
isConnected,
isConnecting,
error,
connect,
disconnect,
sendCommand
} = useBleConnection();
const handleTrigger = async () => {
try {
await sendCommand('TRIGGER');
} catch (e) {
console.error('명령 전송 실패:', e);
}
};
return (
<div className="flex flex-col gap-4">
{error && (
<p className="text-red-500">{error}</p>
)}
{!isConnected ? (
<button
onClick={connect}
disabled={isConnecting}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
{isConnecting ? '연결 중...' : 'BLE 연결'}
</button>
) : (
<>
<p className="text-green-500">연결됨</p>
<button
onClick={handleTrigger}
className="px-4 py-2 bg-green-500 text-white rounded"
>
릴레이 트리거
</button>
<button
onClick={disconnect}
className="px-4 py-2 bg-gray-500 text-white rounded"
>
연결 해제
</button>
</>
)}
</div>
);
};
ESP32 펌웨어 예제
웹에서 명령을 받을 ESP32 펌웨어 코드입니다.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define RELAY_PIN 5
BLEServer *pServer = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
deviceConnected = true;
Serial.println("Client connected");
}
void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
Serial.println("Client disconnected");
// 재광고 시작
pServer->getAdvertising()->start();
}
};
class CharacteristicCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
if (value == "TRIGGER") {
Serial.println("Trigger received!");
digitalWrite(RELAY_PIN, HIGH);
delay(2000); // 2초간 릴레이 활성화
digitalWrite(RELAY_PIN, LOW);
}
}
};
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
// BLE 초기화
BLEDevice::init("PhotoCardKiosk");
// BLE 서버 생성
pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
// BLE 서비스 생성
BLEService *pService = pServer->createService(SERVICE_UUID);
// BLE 특성 생성
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->setCallbacks(new CharacteristicCallbacks());
pCharacteristic->addDescriptor(new BLE2902());
// 서비스 시작
pService->start();
// 광고 시작
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();
Serial.println("BLE server ready!");
}
void loop() {
delay(1000);
}
자주 발생하는 문제
1. HTTPS 요구사항
Web Bluetooth는 보안 컨텍스트(Secure Context)에서만 동작합니다.
# Next.js에서 HTTPS로 개발 서버 실행
next dev --experimental-https
2. 사용자 제스처 필요
requestDevice는 반드시 사용자 제스처(클릭 등)에 의해 호출되어야 합니다.
// 잘못된 예 - 페이지 로드 시 자동 연결 시도
useEffect(() => {
navigator.bluetooth.requestDevice({ ... }); // 실패
}, []);
// 올바른 예 - 버튼 클릭 시 연결
<button onClick={connect}>연결</button>
3. 연결 해제 처리
BLE 연결은 불안정할 수 있으므로 연결 해제 이벤트를 처리해야 합니다.
device.addEventListener('gattserverdisconnected', () => {
console.log('연결 해제됨');
// 재연결 시도 또는 UI 업데이트
});
4. UUID 형식
UUID는 반드시 소문자로 작성해야 합니다.
// 잘못된 예
const SERVICE_UUID = '4FAFC201-1FB5-459E-8FCC-C5C9C331914B';
// 올바른 예
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
브라우저 지원 확인
const checkBleSupport = () => {
if (!navigator.bluetooth) {
return { supported: false, reason: 'Web Bluetooth API를 지원하지 않는 브라우저입니다.' };
}
if (!window.isSecureContext) {
return { supported: false, reason: 'HTTPS 환경에서만 사용할 수 있습니다.' };
}
return { supported: true };
};
마무리
Web Bluetooth API를 사용하면 별도의 네이티브 앱 개발 없이 웹에서 직접 BLE 디바이스를 제어할 수 있습니다.
핵심 포인트:
- HTTPS 환경 필수
- 사용자 제스처를 통한 연결 요청
- 연결 해제 이벤트 처리
- UUID는 소문자로 작성
IoT 프로젝트에서 웹 기반 인터페이스가 필요하다면 Web Bluetooth를 고려해보세요. 별도 앱 설치가 필요 없어 사용자 진입 장벽을 낮출 수 있습니다.
참고 자료