@plott/webkit-browser0.4.0
브라우저·앱 WebView 공통 — 한글 입력(IME) 중 Enter 거르기, 화면 키보드가 가린 영역 CSS 변수, presigned 업로드(진행률·상태 검사·필드 순서), 파일 검사(형식·크기·실제 바이트), 파일 저장, 던지지 않는 저장소, 외부 결제·본인 확인 뒤 돌아올 기록 위치
설치
pnpm add @plott/webkit-browser@0.4.0진입점(exports)
| import | 타입 | 파일 |
|---|---|---|
@plott/webkit-browser | ./dist/index.d.ts | ./dist/index.js |
브라우저와 앱 WebView 에서 쓰는 작은 도구들입니다. 읽는 사람은 입력칸·바텀 시트·파일 업로드·외부 결제 복귀를 만드는 프론트엔드 개발자입니다.
런타임 의존성은 없습니다. checkFile 의 내용 검사(첫 바이트로 형식을 가리는 일)만 선택 peer @plott/webkit-sanitize 의 /files 를 부릅니다. checkFile 을 쓰는 앱은 @plott/webkit-sanitize 를 직접 설치하고, 그 밖의 이름만 쓰는 앱은 설치하지 않습니다(@plott/webkit-react 만 설치한 앱은 HTML 파서를 받지 않습니다). React 에 묶이지 않아 Capacitor 셸·바닐라 화면에서도 씁니다.
진입점
| 묶음 | 이름 | 하는 일 |
|---|---|---|
| 한글 입력 | onEnter, isSubmitEnter, shouldSubmitOnEnter, isComposing | 조합 중 Enter 를 거르고, 폼 입력에서 Enter 를 제출로 볼지 정한다 |
| 화면 키보드 | watchViewport, subscribeViewport, readViewport | 키보드가 가린 영역을 CSS 변수로, 또는 값만 받는다 |
| 업로드 | uploadPresignedPost, uploadPresignedPut, UploadError | presigned 주소로 올린다(진행률·취소·상태 검사) |
| 파일 | checkFile, saveBlob | 형식·크기·실제 바이트 검사, Blob 저장 |
| 저장소 | safeStorage | 던지지 않는 localStorage·sessionStorage |
| 외부 화면 복귀 | markHistory, returnToMark | 결제창·본인 확인에서 돌아온 뒤 «뒤로» 를 바로잡는다 |
붙이기
import { onEnter } from "@plott/webkit-browser";
declare const addTag: (value: string) => void;declare const value: string;
export const onKeyDown = onEnter(() => addTag(value));import { watchViewport } from "@plott/webkit-browser";
export const stopWatching = watchViewport();여러 컴포넌트가 같은 상태를 보면 subscribeViewport 로 값만 받습니다. <html> 에 --vv-* 변수와 data-keyboard 속성을 쓰지 않고, 처음 값과 바뀐 값만 listener 로 옵니다. watchViewport 는 이 구독 위에서 변수와 속성을 씁니다.
import { subscribeViewport } from "@plott/webkit-browser";
declare const setInset: (bottom: number) => void;
export const stopSubscribing = subscribeViewport((state) => setInset(state.bottom));폼 안 입력에서 Enter 를 제출로 볼지는 shouldSubmitOnEnter(event) 로 정합니다.
import { shouldSubmitOnEnter } from "@plott/webkit-browser";
declare const submit: () => void;
export const onKeyDown = (e: { key: string; target: { tagName?: string } | null; preventDefault(): void }) => { if (!shouldSubmitOnEnter(e)) return; e.preventDefault(); submit();};.sheet { position: fixed; bottom: var(--vv-bottom, 0); max-height: var(--vv-height, 92dvh) }html[data-keyboard="open"] .sheet { padding-bottom: 0 }업로드 정책(issueUploadPolicy)은 앱이 자기 인증으로 서버에서 받습니다.
import { checkFile, uploadPresignedPost, type FileProblem, type PresignedPostPolicy } from "@plott/webkit-browser";
declare const file: File;declare const signal: AbortSignal;declare const t: { upload: Record<FileProblem["reason"], string> };declare const toast: (message: string) => void;declare const setProgress: (ratio: number) => void;declare const issueUploadPolicy: (file: File) => Promise<PresignedPostPolicy>;
export async function upload() { const problem = await checkFile(file, { accept: ["image/*"], maxBytes: 10 * 1024 * 1024 }); if (problem) return toast(t.upload[problem.reason]); const policy = await issueUploadPolicy(file); await uploadPresignedPost(policy, file, { onProgress: ({ loaded, total }) => setProgress(loaded / total), signal });}markHistory 는 결제창으로 떠나기 직전에, returnToMark 는 결제 완료·취소로 돌아온 화면에서 부릅니다.
import { markHistory, returnToMark } from "@plott/webkit-browser";
declare const router: { replace(href: string): void };
markHistory("payment");if (!returnToMark("payment")) router.replace("/checkout/done");기본으로 지키는 것
- 한글 조합 중 Enter 를 거릅니다. 조합을 끝내는 Enter 가 한 번 더 와서 태그가 두 개 생기거나 문장이 둘로 저장됩니다. React 이벤트의
nativeEvent.isComposing과keyCode 229를 봅니다. Safari 는 조합을 끝내는 Enter 를key: "Enter",isComposing: false,keyCode: 229로 보내므로 229 는 늘 조합으로 봅니다. 모든 keydown 에 229 를 주는 안드로이드 키보드를 받아야 하는 화면은onEnter(handler, { trust229: false })로 끕니다 — 그때는 229 를key가Process·Unidentified(또는 없음)일 때만 조합으로 봅니다. Shift+Enter 는 줄바꿈이라 지나갑니다.shouldSubmitOnEnter는 여기에 Ctrl·Meta·Alt 를 함께 누른 Enter 와textarea(event.target.tagName)의 Enter 도 제출로 보지 않습니다.onEnter의 handler 가false를 돌려주면preventDefault를 하지 않습니다. - 키보드 높이가 아니라 보이는 영역의 아랫변을 알립니다. iOS 는 키보드가 열려도 고정 요소의 기준을 줄이지 않고 보이는 영역만 끌어올려, 키보드 높이만큼 올린 시트는 입력칸이 잘립니다. 보이는 높이가 150px 이상 줄 때만 키보드로 봅니다(주소창 접힘·핀치 확대·포커스만으로는 열지 않습니다).
watchViewport(200)처럼 숫자만 주면 threshold 입니다. 변수 이름 앞머리(cssPrefix, 기본--vv-)와 키보드 속성 이름(attribute, 기본data-keyboard)은 옵션으로 바꿉니다. 잘못된 이름(--로 시작하지 않는 접두사, 속성 이름이 아닌 값)은@plott/webkit-browser:로 시작하는TypeError입니다. 안드로이드adjustResize는 창(innerHeight)도 함께 줄어 위 비교로는 차이가 없으므로, 가로 폭(방향)마다 본 가장 큰innerHeight와도 견줍니다 — 입력칸(input·textarea·select·contenteditable)에 포커스가 있을 때만. 한계: 기준은 페이지를 연 뒤 본 값이라 키보드가 열린 채로 열린 화면에서는 알 수 없고, 입력칸 포커스 중에 창이 줄어드는 분할 화면·창 크기 조절은 키보드로 오인할 수 있습니다. - 업로드는 응답 상태를 봅니다. 2xx 가 아니면
UploadError(상태·본문,kind:http·network·timeout)입니다. presigned POST 는 필드를 먼저, 파일을 마지막에 넣습니다(S3 는 파일 뒤의 필드를 버립니다). 필드 순서가 중요하면 객체 대신[이름, 값][]배열로 주세요(객체는"10"·"2"같은 정수 키를 숫자 순으로 앞에 당깁니다). PUT 의 Content-Type 은 서명할 때와 같아야 하고, 헤더 이름은 대소문자를 가리지 않고 하나만 보냅니다(우선순위headers>contentType>file.type).timeoutMs를 주면 그 시간 뒤kind: "timeout"으로 끝나고, 요청이 끝나거나xhr의open·setRequestHeader·send가 던지면signal의 abort 리스너를 치우고, 던진 오류로 거절합니다.xhr옵션으로XMLHttpRequest를 바꿔 끼울 수 있습니다(테스트용). - 파일 형식은 확장자·MIME 만 믿지 않습니다. png·jpg·gif·webp·pdf 는 확장자나 MIME 이 그렇게 말하면 첫 바이트가 맞아야 통과합니다(이름만
.png인 HTML, 확장자 없는photo+image/png인 HTML, 확장자와 MIME 이 서로 다른 형식을 말하는 파일을 막습니다).image/*는image/svg+xml을 받지 않습니다 — 스크립트를 품을 수 있어, 받으려면accept에 따로 적습니다. 종류를 가릴 수 없는 이미지(bmp·heic 등)도 내용이 마크업이면 막습니다. 빈 파일도 막습니다. 확장자·MIME 을 기존 종류에 더 이어 붙이려면checkFile(file, { extraKinds: { extensions: { jfif: "jpeg" }, mimeTypes: { "image/pjpeg": "jpeg" } } })로 적습니다(종류는png·jpeg·gif·webp·pdf·zip, 조회는 자기 키만 봅니다). @plott/webkit-sanitize가 없어도checkFile밖은 그대로 돕니다. 선택 peer 라서 설치하지 않으면 내용 검사를 하는checkFile만@plott/webkit-browser:로 시작하는Error(설치하거나verifyContent: false를 주라는 안내,cause에 원래 오류)로 거절합니다. 빈 파일·크기·accept검사와verifyContent: false는 설치와 상관없이 통과합니다.- 저장소가 막혀도 던지지 않습니다. 사파리 개인 창·차단된 사이트 데이터에서는 메모리로 동작합니다(
persistent: false). 용량이 가득 찬 저장소는 «없는 저장소» 가 아니라서 기존 값은 계속 읽히고set만false를 돌려줍니다. 깨진 JSON 은null이고,setJson은 JSON 으로 만들 수 없는 값(순환 참조·BigInt·undefined·함수)에false를 돌려줍니다.remove는 저장소가 지우지 못해도 던지지 않습니다. - 외부 화면에서 돌아오면 떠나기 전 위치까지 뒤로 갑니다.
history.replace는 다른 오리진의 기록을 지우지 못해 «뒤로» 에 결제창의 «세션이 끝났습니다» 가 나옵니다.markHistory는 실제로 저장했을 때만true입니다 — 저장소가 막혔거나 가득 차면false이니 그때는 돌아와서replace로 자리를 정합니다. 기록을 셀 수 없으면(길이가maxHistory이상 — 기본 50(크롬), 사파리는returnToMark(key, { maxHistory: 100 })—, 표시 없음, 30분 지남)returnToMark가false를 돌려주고 화면이replace로 자리를 정합니다. 숫자만 주면maxAgeMs입니다. 표시 키는webkit:history-mark:로 시작하고markHistory(key, { prefix })·returnToMark(key, { prefix })로 바꿉니다(두 쪽에 같은 값을 줍니다). saveBlob은 링크를 문서에 붙였다가 누르고 30초 뒤에 해제합니다(saveBlob(blob, name, { revokeAfterMs })로 바꿉니다). 붙이지 않은 링크의download를 무시하거나, 바로 해제하면 내려받기가 끊기는 브라우저가 있습니다. 앱 WebView 는download를 처리하지 못하는 경우가 많아 브릿지의 저장 동작을 씁니다.