지문 인식 프로토콜을 TypeScript npm 패키지로 만든 과정
SFM 지문 인식기의 패킷 생성·검증을 공통화하고 Serial·TCP 전송 계층을 분리해 npm 패키지로 배포한 과정을 정리합니다.
지문 인식 장비를 여러 애플리케이션에서 사용하려면 장비 명령을 호출하는 코드뿐 아니라 패킷 생성, 체크섬 검증, 응답 해석과 통신 오류 처리까지 동일하게 유지해야 한다. 이 로직이 애플리케이션마다 복사되면 장비 펌웨어나 통신 방식이 바뀔 때 구현이 쉽게 갈라진다.
이를 줄이기 위해 SFM 지문 인식 프로토콜을 TypeScript 패키지로 분리하고, 공통 프로토콜과 Serial·TCP 전송 계층을 나눴다.
패킷 형식을 라이브러리 경계로 삼았다
장비 프로토콜은 통신 방식과 무관하게 정해진 패킷을 주고받는다. 구현에서 다룬 주요 형식은 다음과 같았다.
| 구분 | 시작 바이트 | 패킷 길이 |
|---|---|---|
| 일반 패킷 | 0x40 | 13바이트 |
| 네트워크 패킷 | 0x41 | 15바이트 |
| 보안 패킷 | 명령별 구성 | 35바이트 |
패킷 생성기는 명령과 파라미터를 바이트 배열에 배치하고 체크섬을 계산했다. 응답 처리기는 시작·종료 바이트, 체크섬과 명령 일치 여부를 검증한 뒤 결과를 상위 코드에 전달했다.
아래 코드 예시는 설명을 위해 식별자와 운영 설정값을 일반화했습니다.
export function buildDevicePacket(
command: number,
param = 0,
size = 0,
flag = 0,
): Buffer {
const packet = Buffer.alloc(PACKET_LENGTH);
packet[0] = START_MARKER;
packet[1] = command;
packet.writeUInt32LE(param >>> 0, 2);
packet.writeUInt32LE(size >>> 0, 6);
packet[10] = flag;
packet[11] = computeChecksum(packet, 11);
packet[12] = END_MARKER;
return packet;
}
애플리케이션은 바이트 위치나 체크섬 계산을 알 필요 없이 등록, 검색, 삭제 같은 장비 명령을 호출하도록 만들었다.
프로토콜과 전송 계층을 분리했다
공통 DeviceClient는 명령 패킷 생성과 응답 검증을 담당하고, 실제 데이터 전송은 인터페이스 뒤로 숨겼다.
flowchart TB
A[애플리케이션] --> B[DeviceClient]
B --> C[패킷 생성·응답 검증]
B --> D{전송 구현}
D --> E[Serial]
D --> F[TCP]이 구조에서는 Serial과 TCP가 같은 프로토콜을 공유한다. 전송 방식이 추가되더라도 지문 명령과 응답 해석을 다시 구현하지 않아도 된다.
Serial 구현은 115200 baud, 64바이트 단위 쓰기와 drain을 사용했다. 응답 수신 중에는 50ms의 inter-character timeout을 두어 하나의 패킷이 끝났는지 판별했다. TCP 구현은 소켓의 연결·데이터·오류·종료 이벤트를 같은 전송 인터페이스로 변환했다.
늦게 도착한 응답도 방어했다
직전 요청의 응답이 늦게 도착하면 현재 명령의 응답으로 잘못 해석할 수 있다. 응답의 명령 코드가 현재 요청과 다르면 즉시 성공 처리하지 않고 다음 패킷을 기다렸다.
let retries = 0;
while (retries < MAX_STALE_RESPONSES) {
const response = await this.readNextPacket(
PACKET_LENGTH,
"normal",
this.responseTimeout,
);
const parsed = parseDevicePacket(response);
if (parsed.valid && parsed.command === command) {
return parsed;
}
if (parsed.valid && parsed.command !== command) {
retries++;
continue;
}
}
무한히 기다리지 않도록 건너뛸 수 있는 오래된 패킷 수를 최대 10개로 제한했다. 패킷 구조가 잘못됐거나 체크섬이 맞지 않을 때도 정상 응답으로 넘기지 않았다.
배포 가능한 패키지로 구성했다
패키지 저장소는 Lerna의 independent 버전 전략을 사용하고, 각 패키지는 배포 전 tsc를 실행하도록 구성했다. Conventional Commits를 기반으로 변경 이력을 관리하고 npm을 배포 채널로 사용했다.
{
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
이 작업의 핵심은 단순히 공통 함수를 묶는 것이 아니었다. 장비 프로토콜의 경계와 전송 방식의 경계를 분리하고, 잘못된 패킷이 애플리케이션 상태로 올라오지 않도록 검증 책임을 패키지 안에 모은 것이었다.
그 결과 사용하는 애플리케이션은 Serial이나 TCP 연결을 선택한 뒤 같은 지문 명령 API를 사용할 수 있게 됐다. 장비 연동의 복잡한 바이트 처리와 오류 검증은 재사용 가능한 패키지에서 일관되게 관리할 수 있었다.