Chrome 확장 프로그램에서 WebHID 사용하기: Input·Output·Feature Report

키보드와 마우스만 HID(Human Interface Device)인 것은 아닙니다. 바코드 스캐너, 커스텀 버튼 패드, 계측 장치처럼 운영체제의 범용 드라이버를 사용하면서도 제조사 고유 데이터를 주고받는 장치도 HID 프로토콜을 사용합니다.

Chrome의 WebHID API를 이용하면 네이티브 드라이버용 프로그램을 별도로 만들지 않고 JavaScript에서 이런 장치를 선택하고, 리포트 구조를 확인하고, 바이트 데이터를 읽고 쓸 수 있습니다. 이 글에서는 Manifest V3 확장 프로그램의 사이드 패널에서 장치를 연결하는 흐름을 중심으로 정리합니다.

먼저 보는 핵심
장치 선택은 사용자의 클릭으로 시작하고, 연결 후에는 device.collections에서 지원 리포트를 먼저 확인합니다. Input은 이벤트로 받고, Output과 Feature 쓰기는 장치 문서에 정의된 ID와 길이를 지켜야 합니다.


WebHID가 다루는 세 가지 리포트

HID 통신을 시작하기 전에 리포트의 방향부터 구분해야 합니다.

종류 방향 일반적인 용도
Input Report 장치 → 브라우저 버튼 상태, 센서 값, 스캔 결과 수신
Output Report 브라우저 → 장치 LED, 진동, 동작 명령 전송
Feature Report 양방향 설정 교환 동작 모드, 보정값, 펌웨어 설정 읽기·쓰기

각 장치가 어떤 리포트 ID와 바이트 길이를 지원하는지는 HID 리포트 디스크립터에 의해 결정됩니다. WebHID에서는 device.collections를 통해 컬렉션과 리포트 정의를 살펴볼 수 있습니다. 임의의 ID와 데이터를 보내기 전에 장치 제조사의 프로토콜 문서와 이 정보를 먼저 확인해야 합니다.


Manifest 권한과 실행 위치

Chrome 확장 프로그램에서 WebHID 자체를 쓰기 위한 별도의 manifest 권한은 필요하지 않습니다. 대신 requestDevice()를 호출하면 Chrome이 장치 선택 창을 열고, 사용자가 특정 장치를 직접 허용합니다. Chrome 확장 프로그램에서 WebHID를 지원하는 범위와 권한 흐름은 Chrome 공식 WebHID 문서에서 확인할 수 있습니다.

장치 선택 창은 사용자 클릭에 이어 사이드 패널이나 팝업 같은 확장 페이지에서 열어야 합니다. 확장 서비스 워커에서는 requestDevice()를 호출할 수 없습니다.

{
  "manifest_version": 3,
  "permissions": ["storage", "sidePanel"],
  "background": { "service_worker": "background.js" },
  "side_panel": { "default_path": "sidepanel.html" }
}

제품용 확장 프로그램이라면 filters: []로 모든 장치를 노출하기보다 VID, PID 또는 usage로 선택 범위를 좁히는 편이 안전합니다.

const devices = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x1234, productId: 0x0001 }]
});

장치 선택과 다시 연결하기

첫 연결에는 requestDevice()를, 이미 권한을 받은 장치를 다시 찾을 때는 getDevices()를 사용합니다.

async function selectHidDevice() {
  if (!("hid" in navigator)) {
    throw new Error("이 브라우저는 WebHID를 지원하지 않습니다.");
  }

  const devices = await navigator.hid.requestDevice({ filters: [] });
  if (devices.length === 0) return null;

  const device = devices[0];
  if (!device.opened) await device.open();
  return device;
}

async function reconnectGrantedDevice() {
  const devices = await navigator.hid.getDevices();
  const device = devices[0];
  if (device && !device.opened) await device.open();
  return device ?? null;
}

사용자가 선택 창을 취소하면 정상적인 사용자 행동으로 처리해야 합니다. 연결 실패와 같은 빨간 오류로 표시하기보다 “선택된 장치 없음” 상태로 돌아가는 것이 좋습니다.


컬렉션과 리포트 구조 검사하기

연결에 성공했다고 곧바로 데이터를 보내지 말고 장치가 노출한 컬렉션을 먼저 표시합니다.

function inspectCollections(device) {
  return device.collections.map((collection) => ({
    usagePage: collection.usagePage,
    usage: collection.usage,
    inputReportIds: collection.inputReports.map((r) => r.reportId),
    outputReportIds: collection.outputReports.map((r) => r.reportId),
    featureReportIds: collection.featureReports.map((r) => r.reportId)
  }));
}

여러 컬렉션이 같은 장치에 있을 수 있고, 운영체제나 Chrome이 보안상 보호하는 키보드·마우스·인증 장치 컬렉션은 접근이 제한될 수 있습니다. “장치가 목록에 보인다”와 “모든 리포트를 읽고 쓸 수 있다”는 같은 의미가 아닙니다.


Input Report 실시간 수신

입력 데이터는 inputreport 이벤트로 전달됩니다. event.dataDataView이므로 바이트 단위로 읽어 로그에 표시할 수 있습니다.

function onInputReport(event) {
  const bytes = new Uint8Array(
    event.data.buffer,
    event.data.byteOffset,
    event.data.byteLength
  );

  const hex = [...bytes]
    .map((value) => value.toString(16).padStart(2, "0"))
    .join(" ");

  console.log(`IN id=${event.reportId}: ${hex}`);
}

device.removeEventListener("inputreport", onInputReport);
device.addEventListener("inputreport", onInputReport);

다시 연결할 때 같은 리스너가 중복 등록되지 않게 제거 후 추가합니다. 입력 빈도가 높은 장치는 DOM에 로그 행을 무한히 붙이면 화면이 느려질 수 있으므로 표시 개수를 제한하고, 전체 기록은 별도 배열이나 파일 스트림으로 관리하는 편이 좋습니다.


Output Report 보내기

장치로 명령을 보낼 때는 sendReport(reportId, data)를 사용합니다.

function parseHexBytes(text) {
  const tokens = text.trim().split(/\s+/);
  if (tokens.some((token) => !/^[0-9a-fA-F]{2}$/.test(token))) {
    throw new Error("각 바이트를 00~FF 형식으로 입력하세요.");
  }
  return Uint8Array.from(tokens, (token) => Number.parseInt(token, 16));
}

const payload = parseHexBytes("01 ff 20 00");
await device.sendReport(2, payload);

리포트 ID가 없는 장치는 0을 사용합니다. 데이터 앞에 ID 바이트를 임의로 한 번 더 넣지 말고 해당 장치의 디스크립터와 API 규칙을 확인해야 합니다. 잘못된 명령이 장치 설정을 바꿀 수 있으므로 범용 검사 도구에는 전송 전 확인, 길이 제한, 읽기 전용 모드를 두는 것이 좋습니다.


Feature Report 읽기와 쓰기

Feature Report는 이벤트가 아니라 명시적인 메서드 호출로 다룹니다. receiveFeatureReport()DataView를 반환합니다.

async function readFeature(device, reportId) {
  const view = await device.receiveFeatureReport(reportId);
  return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}

async function writeFeature(device, reportId, bytes) {
  await device.sendFeatureReport(reportId, Uint8Array.from(bytes));
}

리포트 ID가 없는 장치에는 0을 전달합니다. 자세한 반환 형식과 제한은 MDN의 receiveFeatureReport 문서에서 확인할 수 있습니다.

Feature Report는 장치의 비휘발성 설정이나 동작 모드를 바꿀 수도 있습니다. 제조사 문서가 없는 값을 시험 장치가 아닌 실사용 장치에 전송해서는 안 됩니다.


연결 해제와 오류 처리

USB 케이블은 언제든 빠질 수 있습니다. 전역 disconnect 이벤트에서 현재 장치와 같은지 확인하고 UI 상태를 초기화합니다.

navigator.hid.addEventListener("disconnect", ({ device }) => {
  if (device === currentDevice) {
    currentDevice = null;
    setStatus("장치 연결이 해제되었습니다.");
  }
});

실제 도구에서는 다음 경로를 각각 시험해야 합니다.

  1. 장치 선택 취소
  2. 지원하지 않는 브라우저
  3. 권한을 받았지만 open() 실패
  4. 잘못된 리포트 ID 또는 길이
  5. 전송 도중 케이블 분리
  6. 같은 장치 재연결과 이벤트 리스너 중복
  7. Linux의 hidraw 접근 권한 문제

Chrome의 HID·USB 관련 이벤트는 chrome://device-log에서 확인할 수 있어 연결 실패를 진단할 때 유용합니다.


마무리

WebHID 검사 도구의 핵심 흐름은 장치 선택, open(), 컬렉션 검사, Input 이벤트 수신, Output·Feature Report 전송, 연결 해제 처리입니다. UI를 만드는 것보다 장치가 정의한 리포트 ID와 길이를 존중하고, 사용자가 허용한 범위 안에서만 통신하는 것이 더 중요합니다.

처음에는 알려진 테스트 장치 하나와 읽기 중심 기능으로 시작하세요. 그 다음 명확한 프로토콜 문서를 기준으로 Output과 Feature 쓰기를 추가하면 브라우저 안에서도 안전하고 유용한 HID 진단 도구를 만들 수 있습니다.

댓글

이 블로그의 인기 게시물

pyautogui 예제 모니터 특정 위치 색상 구하고 비교해서 클릭 이벤트 하기

vscode 에서 WSL 개발환경 동작하지 않는 경우

React에서 Socket.IO Client 연결하기: CORS와 useEffect 정리