@uiwwsw/virtual-keyboard
v2.2.0
Published
Accessible React virtual keyboard with Korean composition and configurable input policies.
Maintainers
Readme
Virtual Keyboard
한글 조합과 커스텀 키패드를 지원하는 React 가상 키보드입니다. 입력란은 실제 <input inputMode="none">을 사용해 커서·길게 누르기·선택을 브라우저에 맡기며, 화면 키패드는 Shadow DOM 안의 실제 버튼으로 렌더링됩니다.
데모 · npm · MIT License
Installation
npm install @uiwwsw/virtual-keyboardReact / React DOM 18 또는 19가 필요합니다. Intl.Segmenter, Pointer Events, ResizeObserver를 지원하는 최신 브라우저를 대상으로 합니다. 개발·빌드에는 Node.js 24.15 이상(24 LTS)과 npm을 사용합니다.
Usage
import { useState } from "react";
import { VirtualInput, VirtualInputProvider } from "@uiwwsw/virtual-keyboard";
export default function App() {
const [name, setName] = useState("");
return (
<VirtualInputProvider keyboardVisibility="always">
<p id="name-label">이름</p>
<VirtualInput
aria-labelledby="name-label"
value={name}
onValueChange={setName}
placeholder="이름을 입력하세요"
maxLength={40}
/>
</VirtualInputProvider>
);
}추가 CSS 파일을 불러올 필요가 없습니다. className과 style은 입력란을 감싼 컨테이너에 적용됩니다. 내부 input은 여백·글꼴·색상을 이어받고 컨테이너 전체를 터치 영역으로 사용합니다. 기본 keyboardVisibility="auto"는 모바일 또는 터치 중심 장치에서 화면 키보드를 표시합니다. 데스크톱 데모에는 always, 물리 키보드만 사용할 때는 never를 지정하세요.
value를 제공하면 부모가 입력 상태를 관리합니다. defaultValue는 최초 값에만 사용됩니다. 입력 정책은 사용자의 새 입력과 붙여넣기에 적용되며, 부모가 전달하는 value와 기존 값은 임의로 변경하지 않습니다.
Input modes
| mode | 허용 문자 / 동작 | 기본 키패드 |
| -------------- | ---------------------------------------------- | ------------------- |
| text | 자유 입력, 한영 전환 가능 | QWERTY |
| hangul | 한글 자판 고정, 영문 제거. 숫자·공백·기호 허용 | QWERTY |
| number | 0–9, . | 숫자 패드 |
| tel | 0–9, +, *, # | 전화 패드 |
| alpha | A–Z, a–z | QWERTY |
| alphanumeric | 영문과 숫자 | 숫자 행 + QWERTY |
| custom | 사용자 정의 필터·값·레이아웃 | Provider의 레이아웃 |
number는 문자 필터입니다. 소수점 개수, 부호, 금액 범위 같은 업무 규칙은 앱에서 검증하세요. 모든 모드는 한 줄 입력이며 붙여넣은 줄바꿈·탭은 기본 자유 입력에서 공백으로 바뀝니다.
모드별 레이아웃보다 입력란의 layout이 우선합니다. text와 custom 입력란은 자신의 레이아웃이 없으면 Provider의 레이아웃을 사용합니다. 강제 언어 모드는 자유 입력의 저장된 언어 선호도를 바꾸지 않습니다.
Custom layout
label은 화면 표시용이며 실제로 입력되는 것은 value입니다. type을 생략하면 문자 또는 여러 글자 매크로로 처리합니다.
import {
VirtualInput,
VirtualInputProvider,
type KeypadLayout,
} from "@uiwwsw/virtual-keyboard";
const phoneLayout: KeypadLayout = [
[{ value: "1" }, { value: "2" }, { value: "3" }],
[{ value: "4" }, { value: "5" }, { value: "6" }],
[{ value: "7" }, { value: "8" }, { value: "9" }],
[
{ label: "휴대폰", value: "010", width: 2 },
{ value: "0" },
{ label: "⌫", value: "Backspace", type: "action" },
],
];
function PhoneField() {
return (
<VirtualInputProvider>
<VirtualInput aria-label="전화번호" mode="tel" layout={phoneLayout} />
</VirtualInputProvider>
);
}동작 키에는 type: "action"을 사용하세요. 지원 값은 Backspace, Delete, Shift, HangulMode, Enter, ArrowLeft, ArrowRight, EnterSelectionMode, ExitSelectionMode, ToggleSelectionAdjust, Copy, Paste, Cut, SelectAll, Undo, Redo입니다. value: " "는 공백, value: "\n"는 Enter로 처리합니다. width는 행 안에서 차지하는 상대 너비이며 height는 호환성을 위해 타입에 남아 있지만 행 높이를 변경하지 않습니다.
<VirtualInput
aria-label="분류 코드"
mode="custom"
filterKey={(key) => /^[ABC123]+$/.test(key)}
sanitizeValue={(text) => text.replace(/[^ABC123]/g, "")}
/>filterKey는 문자·매크로 입력을 허용하거나 거절합니다. sanitizeValue는 삽입할 문자열을 정리하며 붙여넣기에도 적용됩니다. 두 함수는 같은 문자 정책을 사용하도록 작성하세요.
API
VirtualInputProvider
| 속성 | 타입 / 기본값 | 설명 |
| -------------------- | ------------------------------------------ | -------------------------------- |
| children | ReactNode | 입력란을 포함한 콘텐츠 |
| layout | KeypadLayout / QWERTY | 자유·커스텀 모드의 기본 레이아웃 |
| defaultHangulMode | boolean / true | 저장된 설정이 없을 때의 언어 |
| theme | "light" \| "dark" / 시스템 설정 | 키패드 테마 |
| keyboardVisibility | "auto" \| "always" \| "never" / "auto" | 화면 키패드 표시 조건 |
키패드는 현재 포커스된 편집 가능한 입력란이 있을 때 표시됩니다. 한 페이지에 Provider가 여러 개 있어도 하나만 열립니다. 장치의 화면 크기와 safe area에 맞춰 배치하고, 기존 body 스타일을 덮어쓰지 않습니다.
VirtualInput
| 속성 | 타입 / 기본값 | 설명 |
| ----------------------- | ------------------------------------------------ | ----------------------------------------------------------- |
| value, defaultValue | string | 제어 값 / 초기 값 |
| onValueChange | (value: string) => void | 값이 실제로 바뀔 때 호출하는 권장 API |
| onChange | (event: ChangeEvent<HTMLInputElement>) => void | 기존 API 호환. target.value, currentTarget.value만 제공 |
| mode | InputMode / "text" | 입력 정책 |
| layout | KeypadLayout | 입력란별 키패드 |
| filterKey | (key: string) => boolean | 문자·매크로 필터 |
| sanitizeValue | (text: string) => string | 삽입 문자열 정리 |
| placeholder | string | 비어 있을 때 표시하는 안내 |
| disabled, readOnly | boolean / false | 편집 차단. 화면 키패드를 열지 않음 |
| maxLength | number | UTF-16 단위 최대 길이. 초과 편집은 전체 거절 |
| onClipboardError | (error: Error) => void | 클립보드 API 사용 실패 알림 |
HTMLAttributes<HTMLDivElement> 호환 속성을 지원합니다. id, tabIndex, aria-*는 실제 input에, className, style과 이벤트 핸들러는 컨테이너에 적용합니다. onFocus, onBlur, onKeyDown, 포인터·클립보드 이벤트는 input에서 버블링하며 currentTarget은 기존처럼 컨테이너입니다. 소비자 이벤트 핸들러의 preventDefault()를 존중합니다.
입력란은 한 줄 텍스트 input입니다. inputMode="none"으로 시스템 화면 키보드를 억제하고 가상 키패드를 사용합니다. 네이티브 IME 조합 중에는 키를 가로채지 않고 조합 완료 시 입력 정책을 적용합니다. 공개 API에 type="password", 자동완성, name을 통한 폼 제출 설정은 제공하지 않습니다. 폼에 연결할 때는 부모의 상태로 제출을 처리하세요. Enter는 조합을 끝내며 onKeyDown에서 앱의 완료 동작을 연결할 수 있습니다. onChange의 호환 이벤트를 실제 DOM 이벤트로 사용하지 마세요.
Ref API
ref로 VirtualInputHandle을 받을 수 있습니다. focus(options?), blur(), getValue(), setSelectionRange(start, end), selectAll(), undo(), redo()를 제공합니다.
import { useRef } from "react";
import type { VirtualInputHandle } from "@uiwwsw/virtual-keyboard";
const ref = useRef<VirtualInputHandle>(null);
// <VirtualInput ref={ref} aria-label="이름" />
ref.current?.focus();
ref.current?.setSelectionRange(0, 2);Keyboard and accessibility
- 입력란에
aria-label,aria-labelledby또는id와 연결한label htmlFor를 제공하세요. - 물리 키보드의 영문 키를 현재 한글 모드에 맞춰 해석하며, 이미 전달된 한글 키도 지원합니다. 운영체제 IME 전체를 대체하는 입력란은 아닙니다.
- 한글 조합 중 Backspace는 자모를 지우고, 조합이 끝나면 글자 단위로 지웁니다. 이모지·결합 문자는 중간에서 나누지 않습니다.
- 방향키, Home/End, Shift + 방향키, Ctrl/Cmd + A, 복사·잘라내기·붙여넣기, Escape를 지원합니다.
- Ctrl/Cmd + Z로 실행 취소, Ctrl/Cmd + Shift + Z 또는 Ctrl + Y로 다시 실행합니다. 기록은 입력란별로 최대 100개를 보관하고 외부에서 제어 값이 바뀌면 초기화합니다.
- 키패드 상단의 숫자·기호 전환으로 숫자·구두점·특수문자를 입력할 수 있습니다.
- Shift는 한 번 누르면 다음 문자에 적용되고, 두 번 누르면 고정됩니다. 세 번째 입력으로 해제됩니다.
- 탭, 길게 누르기, 드래그, 스크롤은 실제 input의 기본 제스처로 처리합니다. 앱에서 터치 위치를 커서로 환산하거나 손을 뗄 때 다시 선택하지 않습니다.
selectionStart·selectionEnd·selectionDirection을 편집 엔진과 동기화합니다. 자체 길게 누르기 타이머와 가짜 커서가 없어 브라우저가 정한 커서·선택 상태를 뒤늦게 덮어쓰지 않습니다.- 선택하면 현재 자판 위에
복사·잘라내기·붙여넣기가 바로 표시됩니다.전체 선택은편집화면에서 사용할 수 있습니다. 선택 손잡이와 기본 복사 메뉴의 표시·모양은 기기와 브라우저가 결정합니다.편집키의 화살표·범위 조절도 계속 사용할 수 있습니다. - 화면 키는 손을 뗄 때 실행됩니다. 누른 채 키 밖으로 움직이거나 터치가 취소되면 실행하지 않습니다. 지우기·화살표는 길게 누르면 반복하고 키 밖으로 움직이면 멈춥니다.
- 키패드의 클립보드 버튼은 보안 컨텍스트와 브라우저 권한이 필요할 수 있습니다. 복사는 Clipboard API 실패 시 실제 선택 영역의 기본 복사 이벤트를 대체 경로로 시도합니다. 실패는 보이는 상태 메시지와
onClipboardError로 전달합니다. 기본 복사 메뉴와 물리 키보드 단축키도 지원합니다.
입력란과 키는 실제 HTML input·button으로 렌더링되며 스크린 리더에 노출됩니다. 자동 접근성 검사는 보조기기 실기기 검증을 대체하지 않습니다.
Copy and paste
- 처음 들어와 붙여넣기: 입력란을 탭하고 키패드 상단의
붙여넣기를 누릅니다. 빈 입력란과 숫자·전화번호 모드에서도 바로 사용할 수 있습니다. - 복사한 내용을 다른 위치에 붙여넣기: 글자를 선택해
복사한 다음 같은 입력란이나 다른 입력란에서 원하는 위치를 탭하고붙여넣기를 누릅니다. 선택 범위가 있으면 그 부분을 교체합니다. - 붙여넣은 내용 끝에 커서를 두고 입력란으로 포커스를 돌려 계속 입력할 수 있습니다. 붙여넣기는 한 번의 실행 취소로 되돌릴 수 있습니다.
클립보드는 붙여넣기를 명시적으로 실행할 때 읽습니다. 읽는 동안 버튼에 진행 상태를 표시하고 연속 실행을 막습니다. 대기 중 커서·값·입력란·입력 정책이 바뀌면 요청을 취소하므로 새 위치에서 다시 눌러 주세요. 빈 클립보드, 허용되지 않는 내용, 길이 제한, 부모가 거절한 값은 완료 메시지와 구분해 알립니다.
브라우저가 클립보드 읽기를 제한하면 현재 선택 범위를 유지하고 기본 붙여넣기 메뉴를 안내합니다. 입력란을 길게 눌러 붙여넣기를 선택하거나 Ctrl/Cmd + V를 사용할 수 있습니다. 기본 붙여넣기 이벤트에도 같은 입력 정책과 실행 취소를 적용합니다. OS의 권한 창과 붙여넣기 메뉴는 브라우저가 표시합니다. Clipboard API의 브라우저별 제약을 참고하세요.
Development
npm ci
npm run dev
npm run check
npx playwright install chromium webkit
npm run test:e2e| 명령어 | 결과 |
| ----------------------- | ---------------------------------------------------------- |
| npm run build | 데모 사이트 → demo-dist/ |
| npm run build-package | 라이브러리 JavaScript·타입 → dist/ |
| npm run test:package | 패키지 포함 파일·ESM import·서버 렌더링 검사 |
| npm test | 입력 엔진·컴포넌트 회귀 테스트 |
| npm run test:e2e | 데스크톱·모바일 Chromium 및 모바일 WebKit 동작·접근성 검사 |
| npm run format | Prettier 포맷 적용 |
| npm run check | 포맷·린트·테스트·빌드·패키지 검증 |
npm의 package-lock.json을 의존성 기준으로 사용합니다. Vite 라이브러리 빌드에서 React, React DOM, es-hangul은 외부 의존성으로 유지합니다. 데모를 호스팅할 때 출력 디렉터리를 demo-dist로 설정하세요. Next.js처럼 React Server Components를 사용하는 앱에서는 상태를 사용하는 예제 컴포넌트에 "use client"를 추가하세요.
Migration from 1.x
2.0은 React 18 이상을 지원하며 입력란을 일반 DOM 요소로 렌더링합니다. 이전 custom element 또는 Canvas 구조를 선택하던 CSS를 className/style로 옮기세요. 기존 onChange는 유지되며 문자열 상태에는 onValueChange를 권장합니다. 데모 빌드 경로는 demo-dist/로 변경되었고 Vercel 설정에도 반영했습니다. 전체 변경 내역을 참고하세요.
Migration from 2.0
2.1부터 입력란 내부는 실제 input입니다. 공개 값·ref API와 컨테이너의 className/style은 유지합니다. 내부 문자 span이나 가짜 커서를 선택하던 CSS·DOM 코드는 제거하세요. 선택 범위를 직접 읽을 때는 input의 selectionStart, selectionEnd, selectionDirection을 사용합니다. id와 접근성 속성은 실제 input으로 이동하므로 label htmlFor를 연결할 수 있습니다.
