UI 컴포넌트
React, Vue, Svelte용 완성형 휴대폰 인증 폼. 헤드리스가 기본이고 테마는 선택입니다. @k-otp/sdk의 일부이며 `npm i @k-otp/sdk`로 설치합니다.
UI 컴포넌트는 @k-otp/sdk 패키지의 일부입니다(SDK 참고).
npm i @k-otp/sdkReact(>= 18), Vue(>= 3.3), Svelte(>= 4)는 선택적 peer 의존성이므로 따로 설치할 것은 없습니다. 앱이 이미 쓰는 프레임워크의 서브패스(@k-otp/sdk/ui/react, /ui/vue, /ui/svelte)를 임포트하세요. 기본 테마는 선택적 스타일시트 @k-otp/sdk/ui/theme.css입니다.
React, Vue, Svelte용 접근성을 갖춘 휴대폰 인증 폼입니다. 휴대폰 번호 입력, 쿨다운이 있는 인증번호 받기와 재전송, SMS 자동 입력을 지원하는 분할 인증번호 입력, 확인, 만료 카운트다운, 상태 메시지를 한국어 또는 영어로 제공합니다.
헤드리스가 기본입니다. 모든 파트는 스타일 없는 기본 요소를 렌더링하고 상태를 data-* 속성으로 노출하며, render prop이나 slot으로 전부 바꿀 수 있습니다. 기본 테마는 선택적인 스타일시트입니다.
| 임포트 | 내용 |
|---|---|
@k-otp/sdk/ui | 프레임워크 독립 모델: 번호 파싱, 인증번호 입력 모델, 폼 상태 머신(createOtpForm), 한국어/영어 메시지, 카운트다운 서식, WebOTP |
@k-otp/sdk/ui/react | React 18, 19 컴포넌트("use client" 모듈) |
@k-otp/sdk/ui/vue | Vue 3.3+ 컴포넌트 |
@k-otp/sdk/ui/svelte | Svelte 4, 5 컴포넌트 |
@k-otp/sdk/ui/theme.css | 선택적 기본 테마 |
훅 임포트(@k-otp/sdk/react, /vue, /svelte)는 UI 코드를 불러오지 않고, 각 UI 임포트는 자기 프레임워크만 불러옵니다.
한 줄 사용
import { OtpForm } from "@k-otp/sdk/ui/react";
import "@k-otp/sdk/ui/theme.css"; // 선택
<OtpForm
options={{ apiKey: "pk_..." }} // 또는 <OtpProvider> 안에서, 또는 client={...}
purpose="signup"
onVerified={(result) => console.log(result.issueId)}
/>;<script setup lang="ts">
import { OtpForm } from "@k-otp/sdk/ui/vue";
import "@k-otp/sdk/ui/theme.css";
</script>
<!-- app.use(createOtpPlugin({ apiKey: "pk_..." })) 를 쓰거나 client / options 를 넘기세요 -->
<template>
<OtpForm purpose="signup" @verified="(result) => console.log(result.issueId)" />
</template><script>
import { OtpForm } from "@k-otp/sdk/ui/svelte";
import "@k-otp/sdk/ui/theme.css";
</script>
<OtpForm options={{ apiKey: "pk_..." }} purpose="signup" onVerified={(r) => console.log(r.issueId)} />pk_ 키는 allowedOrigins에 정확히 등록된 출처에서만 동작합니다(인증 참고). 브라우저에서의 인증은 코드가 맞았다는 사실을 브라우저에 알려 줄 뿐입니다. 백엔드가 권한을 준다면 서버에서 sk_ 키로 getStatus를 확인하세요.
한국 휴대폰 번호는 전송 전에 정규화됩니다. 010-1234-5678, +82 10-1234-5678, 0082 10 1234 5678은 모두 01012345678로 전송됩니다. 유선 번호는 거부되고, 해외 번호는 allowInternational일 때만 E.164 형식으로 허용됩니다.
설정
모든 폼은 같은 설정을 받습니다.
| 설정 | 기본값 | 설명 |
|---|---|---|
purpose | 필수 | issue의 용도(purpose) 라벨. |
issue | 그 밖의 issue 필드: templateId, templateVariables, metadata 등. | |
codeLength | 6 | 인증번호 자릿수. |
resendCooldownMs | 30000 | 전송한 번호에 대한 로컬 쿨다운. 제한이 아니라 UX 장치이며, 실제 제한은 API의 레이트 리밋입니다. |
autoSubmit | true | 모든 자리를 입력하면 바로 확인합니다. |
webOtp | true | 전송 후 브라우저가 SMS에서 코드를 채우도록 요청합니다(아래 참고). |
clearCodeOnMismatch | true | 틀린 코드를 지웁니다. |
allowInternational | false | 한국 외 E.164 번호를 허용합니다. |
defaultPhoneNumber | 번호 입력의 초기값. | |
locale | "ko" | "ko", "en", "auto". |
messages | 템플릿이나 함수로 메시지를 바꿉니다. | |
id | 루트 id. 각 파트의 id는 이 값에서 만들어집니다. |
이벤트는 sent, verified, error, phaseChange입니다. React와 Svelte는 onSent, onVerified, onError, onPhaseChange prop을 받고, Vue는 sent, verified, error, phase-change를 emit합니다.
폼이 언마운트·초기화되거나 번호 단계로 돌아갈 때 진행 중이던 요청, 또는 더 새로운 전송이 대체한 요청은 조용히 버려집니다. 그 요청에 대한 오류, sent, verified가 없습니다. 같은 번호로 다시 보내면 그 요청의 멱등키를 재사용하므로, 앞선 요청이 서버에 도달했더라도 메시지가 두 번 나가지 않습니다.
헤드리스 조합
프리셋은 원하는 대로 배치하고 스타일을 입힐 수 있는 파트로 만들어져 있습니다.
| 파트 | 렌더링 | data-k-otp 값 |
|---|---|---|
OtpForm.Root | <form>, 플로우를 소유 | root |
OtpForm.PhoneField | 라벨, <input type="tel">, 설명, 오류 | phone-field, phone-label, phone-input 등 |
OtpForm.SendButton | 전송, "0:27 후 재전송", 또는 "다시 시도" | send-button |
OtpForm.CodeField | 라벨, 분할 입력, 설명, 오류 | code-field, code-input, code-segment 등 |
OtpForm.VerifyButton | 확인, 또는 "0:05 후 다시 시도" | verify-button |
OtpForm.Countdown | 인증번호 만료(role="timer", aria-live="off") | countdown |
OtpForm.Message | 상태 또는 오류(role="status", polite 라이브 리전) | message |
OtpForm.EditPhoneButton | 번호 단계로 돌아가기 | edit-phone |
OtpCodeInput | 단독으로 쓰는 분할 인증번호 입력 | code-input, code-segment |
Svelte에는 네임스페이스가 없어 파트 이름이 OtpFormRoot, OtpFormPhoneField 등입니다. React의 @k-otp/sdk/ui/react는 "use client" 모듈이므로 점 표기 파트와 함수 prop은 클라이언트 컴포넌트 안에서 쓰세요. 서버 컴포넌트는 직렬화 가능한 prop만 넘겨 이름 있는 export(OtpFormRoot, OtpFormPhoneField 등)를 렌더링할 수 있습니다.
<OtpForm.Root purpose="login" className="my-form" onVerified={done}>
{({ state }) => (
<>
<OtpForm.PhoneField>
{({ labelProps, inputProps, errorProps, parts }) => (
<>
<label {...labelProps}>휴대폰 번호</label>
<input {...inputProps} className="my-input" />
{parts.text.phoneError && <p {...errorProps}>{parts.text.phoneError}</p>}
</>
)}
</OtpForm.PhoneField>
{state.issued && <OtpForm.CodeField />}
{state.issued && <OtpForm.VerifyButton />}
<OtpForm.SendButton />
<OtpForm.Message />
</>
)}
</OtpForm.Root>Vue는 같은 값을 주는 scoped slot을, Svelte는 let: slot prop(Svelte 5에서는 snippet도 가능)을 씁니다.
상태 속성
모든 파트에는 data-k-otp="<파트>"와 data-state가 있습니다.
- 루트, 필드, 입력,
edit-phone에서data-state는 단계입니다.phone,sending,code,verifying,verified,failed(현재 인증번호를 더 쓸 수 없는 경우. 예: 만료, 시도 초과. 다시 보내면 처음부터 시작)입니다. - 전송/확인 버튼에서는
idle,loading,cooldown입니다. 코드 칸은filled,empty, 카운트다운은running,expired,idle, 메시지는info,success,error,idle입니다. data-invalid는 유효하지 않은 필드를 표시하고(aria-invalid="true"도 함께),data-disabled는 잠겼거나 지금은 아무 일도 하지 않는 입력과 버튼을 표시합니다(버튼에는aria-disabled="true"도 붙습니다).
접근성
- 라벨은 실제
<label for>이고, 설명과 오류는aria-describedby로 연결됩니다. - 인증번호 입력은
role="group"이며 각 칸은 "6자리 중 1번째 숫자"처럼 이름이 붙습니다. 첫 칸에는 iOS/Android SMS 자동 입력을 위한autocomplete="one-time-code"가, 모든 칸에는inputmode="numeric"이 있습니다. - 인증번호 입력은 탭 정지점 하나입니다. 어느 칸에서든 Tab / Shift+Tab으로 빠져나가고, 방향키, Home, End로 칸을 옮깁니다. 붙여넣기는 여러 칸에 나뉘어 들어가며, 입력기(IME)로 조합한 숫자는 조합이 끝날 때 한 번만 반영됩니다.
- 포커스는 흐름을 따라갑니다. 전송 후와 틀린 코드 후에는 첫 코드 칸, 인증 후에는 결과 메시지, "번호 변경" 후에는 번호 입력으로 이동합니다.
- 버튼은 요청이나 쿨다운 중에
disabled대신aria-disabled를 써서 포커스를 유지합니다. 카운트다운은 매초 읽어 주지 않습니다.
테마
@k-otp/sdk/ui/theme.css는 선택 사항입니다. [data-k-otp] 요소만 명시도 0 선택자로 꾸미므로 직접 지정한 클래스가 항상 이깁니다. 라이트/다크는 prefers-color-scheme을 따르고, 폼이나 상위 요소에 data-k-otp-theme="light" 또는 "dark"를 주면 고정됩니다. 커스텀 속성으로 조정합니다.
:root {
--k-otp-accent: #0f766e;
--k-otp-radius: 0.5rem;
--k-otp-font: inherit;
}그 밖의 토큰으로 --k-otp-surface, --k-otp-text, --k-otp-muted, --k-otp-danger, --k-otp-success, --k-otp-focus-ring, --k-otp-max-width, --k-otp-segment-size 등이 있습니다. 기본값은 라이트와 다크 모두에서 입력란 테두리와 포커스 표시를 3:1 이상, 글자를 4.5:1 이상으로 유지하므로 덮어쓸 때 염두에 두세요. 고대비(forced-colors) 모드에서는 시스템 색을 쓰고, prefers-reduced-motion: reduce에서는 움직임이 사라집니다. 스타일시트가 없으면 파트는 스타일이 없는 상태입니다.
언어와 메시지
locale의 기본값은 서버와 브라우저 모두 "ko"이고, "en"도 쓸 수 있습니다. "auto"는 페이지의 <html lang>을 따릅니다. ko*이거나 lang이 없으면 한국어, 그 밖의 언어는 영어입니다. 서버와 첫 클라이언트 렌더링에서는 한국어로 그린 뒤 마운트 직후 전환하고 이후 lang 변경도 따라가므로 하이드레이션이 항상 일치합니다.
messages로 어떤 메시지 키든 템플릿({name} 자리표시자)이나 함수로 바꿀 수 있습니다.
<OtpForm
purpose="signup"
locale="en"
messages={{
"send.idle": "Text me a code",
"reason.MISMATCH": ({ attempts }) => `Wrong code (${attempts} left)`,
}}
/>모든 SDK 오류 코드, 검증 실패 사유, 건너뛴 동작에 메시지가 있습니다. 전송 결과가 불분명한 실패(타임아웃, 네트워크 오류, 5xx)에는 같은 멱등키를 재사용하므로 다시 시도해도 안전하다는 안내가 표시됩니다.
WebOTP (Android Chrome의 SMS 자동 입력)
전송 후 브라우저가 지원하면 navigator.credentials.get({ otp })를 호출합니다. 기능 감지를 거치며 확인, "번호 변경", 언마운트 시 중단됩니다. Chrome은 SMS가 출처에 묶인 줄로 끝날 때만 한 번의 동의 후 코드를 채웁니다.
[K-OTP] 인증번호 123456
@www.example.com #123456마지막 줄은 @<페이지 도메인> #<인증번호> 형식이어야 합니다. K-OTP는 발급 요청이 WebOTP를 요청할 때만 SMS에 이 줄을 붙입니다. 폼의 issue 설정에 { webOtp: true }(POST /v1/issue의 webOtp 필드)를 넣고, allowedOrigins에 페이지 출처(https, 경로·포트 없음)가 등록된 pk_ 키를 사용하세요. 그렇지 않으면 SMS에 이 줄이 없어 브라우저 요청이 그냥 완료되지 않을 뿐이므로, 폼의 webOtp 설정을 켜 두어도 무해합니다. iOS/macOS Safari와 Android 키보드는 어느 쪽이든 autocomplete="one-time-code"로 코드를 제안합니다. 끄려면 webOtp={false}를 쓰세요.
브라우저는 카카오톡 메시지를 읽을 수 없으므로 WebOTP는 SMS로 도착한 인증번호만 채웁니다. 발급 응답의 webOtp.status가 결과를 알려 줍니다.
fallback_only: 알림톡 발송(기본값). SMS 자동 대체 발송에만 줄이 들어가므로, 알림톡이 실패해 SMS가 발송될 때만 자동 입력됩니다.skipped,reason: "channel_alimtalk": SMS 대체 발송이 없는 알림톡 발송(smsFallback: false로 껐거나(API 1.8.0) 대체 발송을 쓸 수 없는 경우). SMS가 발송되지 않으므로 WebOTP로 채워지지 않습니다.applied: SMS 발송(channel: "sms"또는 SMS 전용 템플릿). SMS에 줄이 들어갑니다.skipped,reason: "host_too_long": 줄을 붙이면 SMS 한 건을 넘기 때문에 일반 본문을 보냈습니다.
채널과 SMS 대체 발송을 참고하세요.
서버 사이드 렌더링
어떤 UI 임포트든 서버에서 불러오고 렌더링해도 요청도, 타이머도 없고 window에 접근하지 않습니다. 서버 렌더링을 한다면 id를 넘기고(React와 Vue 3.5는 일치하는 id를 만들지만 Svelte와 이전 Vue는 클라이언트에서 달라지는 임의 id를 만듭니다) 양쪽의 locale을 같게 유지하세요.
UI 컴포넌트는 pk_ 키만 씁니다. sk_ 클라이언트는 @k-otp/sdk/server이며, 브라우저 번들에서는 호출하면 오류를 던지는 스텁이 됩니다(SDK 참고).
Svelte 4, 5
@k-otp/sdk/ui/svelte는 컴파일된 런타임 위에 .svelte 소스(svelte export 조건)로 컴포넌트를 제공하며, Svelte 4와 5가 모두 받아들이는 문법으로 작성되어 있습니다. 앱의 Svelte가 직접 컴파일하므로 항상 런타임과 맞습니다. Vite에서는 @sveltejs/vite-plugin-svelte나 SvelteKit을 쓰세요. compilerOptions.runes: true를 전역으로 강제한다면, Svelte 4 문법으로 작성된 다른 라이브러리와 마찬가지로 node_modules는 제외하세요.