대부분의 병원 EMR 시스템은 임상 진료 기록을 구조화된 텍스트나 일반적인 문진 양식으로 처리합니다. 저희가 치과 진료 플랫폼인 Dentiva를 구축하는 과정에서 즉각적인 엔지니어링 장벽에 부딪혔습니다. 바로 치의학은 텍스트 박스로 돌아가지 않는다는 점입니다. 이는 공간적이고, 치아 하나하나의 시각적 기록으로 이루어집니다.
성인의 입에는 32개의 치아가 있으며, 각 치아는 교합면(occlusal), 근심면(mesial), 원심면(distal), 협측면(buccal), 설측면(lingual)의 다섯 가지 고유한 해부학적 표면, 치근 구조, 그리고 치주 포켓 측정값을 가지고 있습니다. 만약 치과 의사가 46번 치아에 Class II 복합 레스토레이션을 표시하기 위해 드롭다운 메뉴를 두 분 동안 헤매게 된다면, 그는 노트북을 닫고 다시 종이 차트로 돌아갈 것입니다.
여기에 저희는 Next.js, SVG paths, 그리고 Supabase를 사용하여 클라이언트 측 성능 저하 없이 인터랙티브 FDI 차팅 캔버스를 설계한 방법을 소개합니다.
1. HTML Canvas가 실패하고 SVG가 성공한 이유
저희의 초기 프로토타입은 치아 윤곽을 렌더링하기 위해 HTML5 Canvas를 사용했습니다. 캔버스는 게임에는 훌륭하지만, 인터랙티브 임상 치과도그램(odontogram)에는 적합하지 않은 도구임이 밝혀졌습니다.
모든 치아는 호버 이벤트(hover events), 개별 표면 클릭 트리거, 활성 선택 상태, 그리고 시술(예: 크라운, 근관 치료 또는 발치)을 추가할 때 즉각적인 시각적 업데이트가 필요합니다. 원시 캔버스에서 160개의 표면 폴리곤에 대한 히트 테스트(hit-testing)를 관리하는 것은 부풀어 오른 수학 루틴과 모바일 태블릿 및 데스크톱 모니터 전반의 지저분한 좌표 스케일링을 만들어냈습니다.
저희는 완전히 컴포넌트화되고 선언적인 SVG로 전환했습니다. 각 치아는 국제 FDI 두 자리 표기법(성인의 경우 11부터 48번, 소아의 경우 51부터 85번)에 매핑된 독립적인 React 컴포넌트입니다:
interface ToothSurfaceProps {
toothId: number;
surface: 'occlusal' | 'mesial' | 'distal' | 'buccal' | 'lingual';
...
벡터를 사용함으로써 저희는 캔버스 리페인트 루프 없이 고해상도(high-DPI) 디스플레이에서의 선명한 렌더링, 네이티브 DOM 이벤트 처리, 그리고 작은 번들 크기를 얻을 수 있었습니다.
2. 상태 관리: 진료실의 속도를 유지하기 위해
치과의사들은 구강 검진 중에 빠르게 병변을 표시합니다. 만약 어금니를 선택하는 것이 전체 32개 치아 도트그램(odontogram)에 대한 최상위 React 재렌더링(re-render)을 유발한다면, 프레임 드롭이 눈에 띄게 됩니다.
이를 방지하기 위해, 우리는 시각적 활성 상태와 영구적인 데이터베이스 쓰기 작업을 분리했습니다.
- 로컬 컴포넌트 상태(Local Component State): 표면 토글, 활성 도구(예: 우식증 준비, 골절, 크라운 또는 발치) 및 치아 다중 선택은 의견을 강요하지 않는 클라이언트 스토어(unopinionated client store)를 통해 처리됩니다. 개별 치아 SVG는 자체 props가 변경될 때만 재렌더링됩니다.
- 낙관적 UI(Optimistic UI): 진료 보조원이 근관 치료 절차를 기록할 때, 치아 그래픽은 화면에 즉시 업데이트됩니다. 우리는 백엔드 왕복(round trip)을 기다리며 UI가 멈추는 것을 허용하지 않습니다.
3. 데이터베이스 스키마: 정규화된 테이블 대 JSONB
의료 도구에서 흔한 설계 실수는 전체 차트 상태를 단일 비정형 JSON 블롭(JSON blob) 열에 쏟아붓는 것입니다. 초기 설정에는 빠르지만, 나중에 임상 쿼리(clinical queries)를 실행하는 것은 거의 불가능하게 만듭니다(예: 이번 달에 치아 16번에서 근관 치료를 받은 모든 환자를 조회).
Supabase의 PostgreSQL에서는 데이터 모델을 구조화된 임상 엔티티로 분할했습니다:
clinical_charts 테이블: 차트 메타데이터, 환자 ID, 제공자 ID 및 타임스탬프를 포함합니다.
chart_entries 테이블: 특정 치아 코드와 표면에 연결된 개별 절차 및 병변을 추적합니다.
create table public.chart_entries (
id uuid primary key default gen_random_uuid(),
chart_id uuid references public.clinical_charts(id) on delete cascade,
...
chart_id와 tooth_number를 함께 인덱싱함으로써, 기록이 수년간의 방문으로 증가하더라도 완전한 치과 병력에 대한 초기 로드 시간은 20밀리초 미만을 유지합니다.
4. 오프라인 복원력 처리: 진료실에서
진료실 운영 구역의 클리닉 인터넷 연결은 악명 높을 정도로 불안정합니다. 만약 광대역 연결이 절차 도중에 끊기더라도, 차트 데이터가 사라져서는 안 됩니다.
로컬 브라우저 스토리지를 사용하여 백그라운드 동기화 큐를 설정했습니다. 치과 진료실에서 발생하는 모든 변경 사항은 '대기 중인(pending)' 뮤테이션 태그와 함께 로컬에 먼저 기록됩니다. 네트워크 연결이 복구되면, 이 뮤테이션들은 Idempotency Key를 사용하여 Supabase와 조정되며, 절차 항목의 중복을 방지합니다.
임상 도구를 구축하려면 무엇보다 지연 시간(latency)과 촉각적 반응성(tactile responsiveness)을 우선순위에 두어야 합니다. 가벼운 SVG 컴포넌트와 예측 가능한 PostgreSQL 스키마로 전환함으로써, 우리는 부피가 큰 종이 차팅을 실질적인 일상 진료실 운영에서 즉각적으로 느껴지는 디지털 워크플로우로 대체할 수 있었습니다.