Web Serial API 시작하기

최종 업데이트: 2019년 11월 8일

빌드할 항목

이 Codelab에서는 Web Serial API를 사용하여 BBC micro:bit 보드와 상호작용하고 5x5 LED 매트릭스에 이미지를 표시하는 웹페이지를 빌드합니다. Web Serial API와 읽기, 쓰기 가능, 변환 스트림을 사용하여 브라우저를 통해 직렬 기기와 통신하는 방법을 알아봅니다.

학습할 내용

  • 웹 직렬 포트를 열고 닫는 방법
  • 읽기 루프를 사용하여 입력 스트림의 데이터를 처리하는 방법
  • 쓰기 스트림을 통해 데이터를 보내는 방법

필요한 항목

이 Codelab에서는 비용이 저렴하고 몇 가지 입력 (버튼) 및 출력 (5x5 LED 디스플레이)을 제공하고 추가 입력 및 출력을 제공할 수 있으므로 이를 사용하고자 했습니다. Espruino 사이트에서 BBC micro:bit 페이지를 참조하여 micro:bit에서 할 수 있는 작업을 알아보세요.

Web Serial API는 웹사이트에서 스크립트를 사용하여 직렬 기기에서 읽고 쓸 수 있는 방법을 제공합니다. API는 웹사이트에서 마이크로컨트롤러 및 3D 프린터와 같은 직렬 기기와 통신할 수 있도록 하여 웹과 실제 환경을 연결합니다.

웹 기술을 사용하여 빌드된 제어 소프트웨어의 예가 많이 있습니다. 예를 들면 다음과 같습니다.

어떤 웹사이트는 사용자가 직접 설치한 네이티브 에이전트 애플리케이션을 통해 기기와 통신합니다. 그 외의 경우에는 Electron과 같은 프레임워크를 통해 패키지 네이티브 애플리케이션에서 애플리케이션이 제공됩니다. 어떤 경우에는 사용자가 컴파일된 애플리케이션을 USB 플래시 드라이브로 기기에 복사하는 등 추가 단계를 진행해야 합니다.

사이트와 해당 사이트에서 제어하는 기기 간에 직접 통신을 제공하여 사용자 환경을 개선할 수 있습니다.

Web Serial API 사용 설정

Web Serial API는 현재 개발 중이며 플래그 뒤에만 제공됩니다. chrome://flags에서 #enable-experimental-web-platform-features 플래그를 사용 설정해야 합니다.

코드 가져오기

Google에서는 이 Codelab에 필요한 모든 것을 Glitch 프로젝트에 통합했습니다.

  1. 새 브라우저 탭을 열고 https://web-serial-codelab-start.glitch.me/로 이동합니다.
  2. Remix Glitch 링크를 클릭하여 나만의 시작 프로젝트 버전을 만듭니다.
  3. Show 버튼을 클릭한 다음 새 창에서를 선택하여 코드가 작동하는 모습을 확인합니다.

Web Serial API가 지원되는지 확인하기

가장 먼저 해야 할 일은 현재 브라우저에서 Web Serial API가 지원되는지 확인하는 것입니다. 그렇게 하려면 serialnavigator에 있는지 확인합니다.

DOMContentLoaded 이벤트에서 다음 코드를 프로젝트에 추가합니다.

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
if ('serial' in navigator) {
  const notSupported = document.getElementById('notSupported');
  notSupported.classList.add('hidden');
}

이는 웹 일련번호가 지원되는지 확인합니다. 지원되는 경우 이 코드는 웹 일련번호가 지원되지 않음을 나타내는 배너를 숨깁니다.

직접 해 보기

  1. 페이지를 로드합니다.
  2. Web Serial이 지원되지 않음을 나타내는 빨간색 배너가 페이지에 표시되는지 확인합니다.

직렬 포트 열기

다음으로 직렬 포트를 열어야 합니다. 대부분의 최신 API와 마찬가지로 Web Serial API는 비동기적입니다. 이렇게 하면 입력을 대기할 때 UI가 차단되지 않지만, 웹페이지에서 직렬 데이터를 언제든지 수신할 수 있고 수신 방법이 필요하기 때문에 이 또한 중요합니다.

컴퓨터에는 여러 직렬 기기가 있을 수 있으므로 브라우저에서 포트를 요청하려고 하면 사용자에게 연결할 기기를 선택하라는 메시지가 표시됩니다.

프로젝트에 다음 코드를 추가합니다.

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudrate: 9600 });

requestPort 호출은 사용자에게 연결할 기기를 묻는 메시지를 표시합니다. port.open를 호출하면 포트가 열립니다. 직렬 기기와 통신하려는 속도도 제공해야 합니다. BBC micro:bit는 USB-to-serial 칩과 메인 프로세서 간에 9,600baud 연결을 사용합니다.

또한 연결 버튼을 연결하고 사용자가 클릭할 때 connect()를 호출하도록 합니다.

프로젝트에 다음 코드를 추가합니다.

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

직접 해 보기

이제 이 프로젝트에서 시작하는 데 필요한 최소 리소스가 거의 남아 있습니다. 연결 버튼을 클릭하면 연결할 직렬 기기를 선택한 다음 micro:bit에 연결하라는 메시지가 표시됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. 탭에 직렬 기기에 연결했음을 나타내는 아이콘이 표시됩니다.

직렬 포트에서 데이터를 수신 대기하도록 입력 스트림 설정

연결이 설정되면 입력 데이터를 설정하고 리더에서 기기의 데이터를 읽어야 합니다. 먼저 port.readable를 호출하여 포트에서 읽을 수 있는 스트림을 가져옵니다. 기기에서 다시 텍스트를 반환한다는 것을 알고 있으므로 텍스트 디코더를 통해 파이핑합니다. 다음으로 리더를 가져와 읽기 루프를 시작합니다.

프로젝트에 다음 코드를 추가합니다.

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

읽기 루프는 루프에서 실행되고 기본 스레드를 차단하지 않고 콘텐츠를 기다리는 비동기 함수입니다. 새 데이터가 도착하면 판독기는 valuedone 부울이라는 두 가지 속성을 반환합니다. done가 true이면 포트가 닫혔거나 더 이상 들어오는 데이터가 없습니다.

프로젝트에 다음 코드를 추가합니다.

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

직접 해 보기

이제 프로젝트를 기기에 연결할 수 있으며 기기에서 받은 모든 데이터를 로그 요소에 추가합니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. Espruino 로고가 표시됩니다.

데이터를 직렬 포트로 전송하도록 출력 스트림 설정

직렬 통신은 일반적으로 양방향입니다. 직렬 포트에서 데이터를 수신하는 것 외에도 데이터를 포트로 보내려고 합니다. 입력 스트림과 마찬가지로, 출력 스트림은 micro:bit로만 전송됩니다.

먼저 텍스트 인코더 스트림을 만들고 port.writeable로 스트림을 파이핑합니다.

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

일련번호를 통해 Espruino 펌웨어와 연결하면 BBC micro:bit 보드가 Node.js 셸에 있는 것과 유사한 자바스크립트(PLP(Read-eval-print 루프)) 역할을 합니다. 다음으로, 데이터를 스트림으로 보내는 메서드를 제공해야 합니다. 아래 코드는 출력 스트림에서 작성자를 가져온 후 write를 사용하여 각 줄을 보냅니다. 전송되는 각 줄에는 줄바꿈 문자 (\n)가 포함되어 전송된 명령어를 평가할 수 있도록 micro:bit에 알려줍니다.

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

시스템을 알려진 상태로 되돌리고 보낸 문자를 에코하지 못하도록 하려면 CTRL-C를 보내고 에코를 꺼야 합니다.

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

직접 해 보기

이제 프로젝트는 micro:bit에서 데이터를 주고받을 수 있습니다. 명령어를 제대로 전송할 수 있는지 확인해 보겠습니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. Chrome DevTools에서 콘솔 탭을 열고
    writeToStream('console.log("yes")');를 입력합니다.

페이지에 다음과 같이 출력됩니다.

매트릭스 그리드 문자열 빌드

micro:bit의 LED 매트릭스를 제어하려면 show()를 호출해야 합니다. 이 메서드는 내장 5x5 LED 화면에 그래픽을 표시합니다. 이 값은 바이너리 숫자 또는 문자열을 사용합니다.

체크박스를 반복해서 검토하여 어떤 체크박스가 선택되어 있는지 나타내는 1초와 0초 배열을 생성합니다. 그런 다음 체크박스의 순서가 매트릭스의 LED 순서와 반대이므로 배열을 반전해야 합니다. 다음으로, 배열을 문자열로 변환하고 micro:bit로 전송할 명령어를 만듭니다.

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

체크박스를 연결하여 매트릭스 업데이트

다음으로 체크박스의 변경사항을 수신 대기해야 하며 변경사항이 있으면 micro:bit에 전송해야 합니다. 특성 감지 코드(// CODELAB: Add feature detection here.)에 다음 줄을 추가합니다.

script.js - DOMContentLoaded

initCheckboxes();

또한 micro:bit가 처음 연결될 때 그리드를 재설정하여 행복한 얼굴이 표시되도록 합니다. drawGrid() 함수는 이미 제공되고 있습니다. 이 함수는 sendGrid()과 비슷하게 작동합니다. 이 함수는 1s와 0의 배열을 취하고 적절히 체크박스를 선택합니다.

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

직접 해 보기

이제 페이지에서 micro:bit 연결을 열면 행복한 얼굴이 표시됩니다. 체크박스를 클릭하면 LED 매트릭스의 화면이 업데이트됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bit LED 매트릭스에 미소가 표시됩니다.
  5. 체크박스를 변경하여 LED 매트릭스에 다른 패턴을 그립니다.

마이크로비트 버튼에 워치 이벤트 추가

micro:bit에는 LED 매트릭스 양쪽에 있는 버튼이 두 개 있습니다. Espruino는 버튼을 누르면 이벤트/콜백을 전송하는 setWatch 함수를 제공합니다. 두 버튼을 모두 수신 대기하고 싶기 때문에 함수를 일반으로 만들고 이벤트 세부정보를 출력하도록 합니다.

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

이제 직렬 포트가 기기에 연결될 때마다 두 버튼 (예: 마이크로:비트 보드의 BTN1 및 BTN2)을 연결해야 합니다.

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

직접 해 보기

연결 시 행복한 얼굴을 표시하는 것 외에도 micro:bit의 버튼 중 하나를 누르면 어떤 버튼이 눌렸는지 나타내는 텍스트가 페이지에 추가됩니다. 각 문자가 한 줄에 하나씩 표시될 가능성이 높습니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bits LED 매트릭스에 스마일 표시가 나타날 것입니다.
  5. micro:bit의 버튼을 누르고 버튼을 누르는 동안 세부정보가 담긴 새 텍스트를 페이지에 추가하는지 확인합니다.

기본 스트림 처리

micro:bit 버튼 중 하나가 푸시되면 micro:bit는 스트림을 통해 데이터를 직렬 포트로 전송합니다. 스트림은 매우 유용하지만 한 번에 모든 데이터를 다 가져올 필요는 없으며 임의로 분할할 수 있기 때문에 문제가 될 수 있습니다.

현재 앱은 수신 스트림이 수신되는 readLoop (인쇄 형식)를 출력하고 있습니다. 대부분의 경우 각 문자는 한 줄에 하나씩 표시되지만, 대부분은 그다지 유용하지 않습니다. 스트림을 개별 행으로 파싱하는 것이 가장 좋으며, 각 메시지는 자체 줄로 표시됩니다.

TransformStream를 사용한 스트림 변환

이를 위해 수신 스트림을 파싱하고 파싱된 데이터를 반환할 수 있는 변환 스트림(TransformStream)을 사용할 수 있습니다. 변환 스트림은 스트림 소스 (이 경우 micro:bit)와 스트림을 소비하는 요소 (이 경우 readLoop) 사이에 있을 수 있으며, 마지막으로 소비되기 전에 임의의 변환을 적용할 수 있습니다. 조립 라인이라고 생각하면 됩니다. 위젯이 선 아래로 들어가면 선의 각 단계에서 위젯을 수정하므로 최종 목적지에 도달할 때까지 완전히 작동하는 위젯입니다.

자세한 내용은 MDN & Streams API 개념을 참고하세요.

LineBreakTransformer를 사용하여 스트림 변환

LineBreakTransformer 클래스를 만들어 스트림을 가져와 줄바꿈(\r\n)에 따라 청크합니다. 클래스에는 transformflush 메서드 두 개가 필요합니다. transform 메서드는 스트림에서 새 데이터를 수신할 때마다 호출됩니다. 데이터를 큐에 추가하거나 나중을 위해 저장할 수 있습니다. flush 메서드는 스트림이 종료될 때 호출되며 아직 처리되지 않은 모든 데이터를 처리합니다.

transform 메서드에서 container에 새 데이터를 추가한 다음 container에 줄바꿈이 있는지 확인합니다. 있는 경우 배열로 분할한 다음 줄을 반복하고 controller.enqueue()를 호출하여 파싱된 줄을 보냅니다.

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

스트림이 닫히면 enqueue를 사용하여 컨테이너의 나머지 데이터를 플러시합니다.

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

마지막으로, 새로운 LineBreakTransformer를 통해 수신 스트림을 파이핑해야 합니다. 원래 입력 스트림은 TextDecoderStream을 통해서만 파이핑되었으므로 새로운 LineBreakTransformer를 통해 파이핑하려면 pipeThrough를 추가해야 합니다.

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

직접 해 보기

이제 micro:bit 버튼 중 하나를 누르면 출력된 데이터가 한 줄로 반환됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bit LED 매트릭스에 미소가 표시됩니다.
  5. micro:bit의 버튼을 누르고 다음과 같이 표시되는지 확인합니다.

JSONTransformer를 사용하여 스트림 변환

readLoop에서 문자열을 JSON으로 파싱하려고 할 수 있습니다. 대신 데이터를 JSON 객체로 변환하는 매우 간단한 JSON 변환기를 만듭니다. 데이터가 유효한 JSON이 아닌 경우 가져온 것을 반환하기만 하면 됩니다.

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

다음으로, LineBreakTransformer를 통과한 후 JSONTransformer를 통해 스트림을 파이핑합니다. JSON이 한 줄에만 전송되므로 JSONTransformer를 단순하게 유지할 수 있습니다.

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

직접 해 보기

이제 micro:bit 버튼 중 하나를 누르면 페이지에 [object Object]이 출력됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bit LED 매트릭스에 미소가 표시됩니다.
  5. micro:bit의 버튼을 누르고 다음과 같이 표시되는지 확인합니다.

버튼을 누를 때 응답

micro:bit 버튼 누름에 응답하려면 readLoop를 업데이트하여 수신된 데이터가 button 속성이 있는 object인지 확인해야 합니다. 그런 다음, buttonPushed을 호출하여 버튼 푸시를 처리합니다.

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

micro:bit 버튼이 푸시되면 LED 매트릭스의 디스플레이가 변경되어야 합니다. 다음 코드를 사용하여 매트릭스를 설정하세요.

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

직접 해 보기

이제 micro:bit 버튼 중 하나를 누르면 LED 매트릭스가 행복한 얼굴이나 슬픈 얼굴로 바뀝니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bits LED 매트릭스에 스마일 표시가 나타날 것입니다.
  5. micro:bit의 버튼을 누르고 LED 매트릭스가 변경되는지 확인합니다.

마지막 단계는 연결 해제 기능을 연결하여 사용자가 작업을 완료한 후 포트를 닫는 것입니다.

사용자가 Connect/Disconnect 버튼을 클릭하면 포트를 닫습니다.

사용자가 연결/연결 해제 버튼을 클릭하면 연결을 종료해야 합니다. 포트가 이미 열려 있다면 disconnect()를 호출하고 페이지가 더 이상 직렬 기기에 연결되지 않도록 UI를 업데이트합니다.

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

스트림 및 포트 닫기

disconnect 함수에서 입력 스트림을 닫고 출력 스트림을 닫은 다음 포트를 닫아야 합니다. 입력 스트림을 닫으려면 reader.cancel()를 호출합니다. cancel 호출은 비동기식이므로 await이 완료될 때까지 기다려야 합니다.

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone;
  reader = null;
  inputDone = null;
}

출력 스트림을 닫으려면 writer를 가져오고 close()를 호출한 후 outputDone 객체가 닫힐 때까지 기다립니다.

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

마지막으로 직렬 포트를 닫고 닫힐 때까지 기다립니다.

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

직접 해 보기

이제 원하는 대로 직렬 포트를 열고 닫을 수 있습니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. micro:bit LED 매트릭스에 스마일 이미지가 표시되어야 합니다.
  5. 연결 해제 버튼을 누르고 LED 매트릭스가 꺼지고 콘솔에 오류가 없는지 확인합니다.

수고하셨습니다. Web Serial API를 사용하는 첫 번째 웹 앱을 성공적으로 빌드했습니다.

Web Serial API에 관한 최신 정보 및 Chrome팀에서 준비하고 있는 새롭고 흥미로운 웹 기능을 모두 확인하려면 https://goo.gle/fugu-api-tracker를 방문하세요.