@vircle/sdk-web 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @vircle/sdk-web
2
2
 
3
- VircleCore를 확장하여 웹 환경에 특화된 기능을 제공하는 웹 애플리케이션용 Vircle Analytics SDK입니다. 자동 추적, 브라우저 컨텍스트 수집, 웹 스토리지 지원 등 웹 환경에 최적화된 기능을 제공합니다.
3
+ VircleCore를 확장하여 웹 환경에 특화된 기능을 제공하는 웹 애플리케이션용 Vircle Analytics SDK입니다. 자동 추적, 브라우저 컨텍스트 수집, 웹 스토리지 지원, 하이브리드 암호화 등 웹 환경에 최적화된 기능을 제공합니다.
4
4
 
5
- ## 🚀 주요 기능
5
+ ## 주요 기능
6
6
 
7
7
  ### 핵심 기능
8
8
  - **VircleCore 상속**: 코어 SDK의 모든 기능 + 웹 특화 기능
@@ -13,13 +13,19 @@ VircleCore를 확장하여 웹 환경에 특화된 기능을 제공하는 웹
13
13
  ### 성능 최적화
14
14
  - **requestIdleCallback**: UI 블로킹 없는 백그라운드 처리
15
15
  - **컨텍스트 캐싱**: 반복적인 DOM 접근 방지
16
- - **LocalStorage**: 영속적 데이터 저장 LRU 캐시 정리
16
+ - **스마트 스토리지**: IndexedDB/LocalStorage 자동 선택으로 대용량 데이터 지원
17
17
  - **이벤트 배칭**: 네트워크 요청 최소화
18
+ - **큐 크기 제한**: TaskScheduler 큐 오버플로우 방지 (기본 1000)
19
+
20
+ ### 세션 관리
21
+ - **세션 타임아웃**: 비활성 상태 감지 후 자동 세션 로테이션 (기본 30분)
22
+ - **sendBeacon**: 페이지 종료 시 이벤트 손실 방지
18
23
 
19
24
  ### 보안 및 프라이버시
20
- - **페이로드 암호화**: 하이브리드 암호화 (AES-256-GCM + RSA-OAEP)
25
+ - **페이로드 암호화**: Web Crypto API 기반 하이브리드 암호화 (AES-256-GCM + RSA-OAEP)
21
26
  - **추적 제외**: data-vircle-ignore 속성으로 민감 정보 보호
22
27
  - **안전한 에러 처리**: SDK 안정성을 위한 모든 외부 API 호출 보호
28
+ - **전용 에러 클래스**: 상세한 에러 코드 및 디버깅 정보 제공
23
29
 
24
30
  ## 설치
25
31
 
@@ -33,21 +39,25 @@ pnpm add @vircle/sdk-web
33
39
 
34
40
  ## 예제 실행
35
41
 
42
+ 예제는 `packages/demo-web` 디렉토리에서 확인할 수 있습니다:
43
+
36
44
  ```bash
37
45
  # 저장소 클론 후
38
- cd packages/sdk-web
46
+ cd packages/demo-web
39
47
 
40
- # 의존성 설치 및 빌드
41
- pnpm install
42
- pnpm build
48
+ # ESM 예제
49
+ open esm/index.html
43
50
 
44
- # 예제 서버 실행
45
- pnpm example
51
+ # React 예제
52
+ cd react && pnpm install && pnpm start
46
53
 
47
- # 브라우저에서 http://localhost:3000 접속
54
+ # Next.js 예제
55
+ cd nextjs && pnpm install && pnpm dev
48
56
  ```
49
57
 
50
- ## 📖 빠른 시작
58
+ 자세한 내용은 [demo-web README](../demo-web/README.md)를 참조하세요.
59
+
60
+ ## 빠른 시작
51
61
 
52
62
  ### 기본 사용법
53
63
 
@@ -57,27 +67,32 @@ import { VircleWeb } from '@vircle/sdk-web';
57
67
  // SDK 초기화 (VircleCore 설정 + 웹 전용 설정)
58
68
  const vircle = new VircleWeb({
59
69
  apiKey: 'your-api-key',
60
- environment: 'production', // 'development' | 'production'
61
-
70
+ environment: 'production',
71
+
62
72
  // 배치 처리 설정 (VircleCore 기능)
63
73
  batch: {
64
- size: 20, // 배치당 이벤트 수
65
- timeout: 5000, // 대기 시간 (ms)
66
- flushInterval: 10000, // 자동 플러시 간격
74
+ size: 50, // 기본값: 50
75
+ timeout: 5000, // 기본값: 5000ms
76
+ flushInterval: 10000, // 기본값: 10000ms
67
77
  },
68
-
78
+
69
79
  // 웹 전용 자동 추적 설정
70
- autoTrack: {
71
- pageView: true,
72
- clicks: true,
73
- forms: true,
74
- errors: true,
75
- },
80
+ trackPageViews: true,
81
+ trackClicks: true,
82
+ trackForms: true,
83
+ trackErrors: true,
84
+ singlePageApp: true,
85
+
86
+ // 암호화 활성화
87
+ enableEncryption: true,
88
+
89
+ // 스토리지 설정
90
+ storageType: 'auto',
91
+ storagePrefix: 'vircle_',
76
92
  }, {
77
93
  // 웹 전용 옵션
78
- enableEncryption: true, // 페이로드 암호화
79
- flushOnUnload: true, // 페이지 이탈 시 전송
80
- contextCacheTime: 300000, // 컨텍스트 캐싱 시간
94
+ flushOnUnload: true,
95
+ contextCacheTime: 300000,
81
96
  });
82
97
 
83
98
  // SDK 초기화 필수!
@@ -105,11 +120,9 @@ await vircle.identify('user-123', {
105
120
 
106
121
  const vircle = new VircleWeb({
107
122
  apiKey: 'your-api-key',
108
- environment: 'production', // 'development' | 'production'
109
- autoTrack: {
110
- pageView: true,
111
- clicks: true,
112
- },
123
+ environment: 'production',
124
+ trackPageViews: true,
125
+ trackClicks: true,
113
126
  });
114
127
 
115
128
  await vircle.initialize();
@@ -124,7 +137,8 @@ await vircle.identify('user-123', {
124
137
  ```html
125
138
  <script src="https://cdn.jsdelivr.net/npm/@vircle/sdk-web/dist/vircle-web-sdk.min.js"></script>
126
139
  <script>
127
- const vircle = new VircleWebSDK.VircleWeb({
140
+ // UMD 빌드는 VircleWebSDK 전역 변수로 VircleWeb 클래스를 직접 노출합니다
141
+ const vircle = new VircleWebSDK({
128
142
  apiKey: 'your-api-key',
129
143
  });
130
144
 
@@ -134,26 +148,29 @@ await vircle.identify('user-123', {
134
148
  </script>
135
149
  ```
136
150
 
137
- ## 🏗️ 아키텍처
151
+ ## 아키텍처
138
152
 
139
153
  ```
140
154
  ┌────────────────────────────────────────────┐
141
155
  │ VircleWeb │
142
156
  │ (VircleCore 상속 + 웹 전용 기능) │
143
157
  ├────────────────────────────────────────────┤
144
- │ • 자동 추적 시스템 (setupAutoTracking)
158
+ │ • 자동 추적 (페이지뷰/클릭/폼/에러/SPA)
145
159
  │ • 웹 컨텍스트 수집 (WebContextCollector) │
146
- │ • LocalStorage 어댑터
147
- │ • 브라우저 이벤트 핸들러
160
+ │ • 세션 타임아웃 / 로테이션
161
+ │ • sendBeacon (페이지 이탈 보호)
162
+ │ • StorageFactory (IndexedDB/LocalStorage) │
163
+ │ • 하이브리드 암호화 (WebExtendedCryptoAdapter)│
164
+ │ • 에러 클래스 (VircleWebError 계열) │
148
165
  └────────────────────┬───────────────────────┘
149
166
  │ extends
150
167
  ┌────────────────────┴───────────────────────┐
151
168
  │ VircleCore │
152
- (이벤트 처리, 전송, 플러그인, 암호화 등)
169
+ (이벤트/전송/플러그인/암호화/원격 설정/스토리지)
153
170
  └────────────────────────────────────────────┘
154
171
  ```
155
172
 
156
- ## 🔍 웹 전용 기능 상세
173
+ ## 웹 전용 기능 상세
157
174
 
158
175
  ### 자동 추적 시스템
159
176
 
@@ -168,73 +185,264 @@ SDK는 `setupAutoTracking` 메서드를 통해 다양한 브라우저 이벤트
168
185
  #### 클릭 추적
169
186
  - **전역 이벤트 위임**: `document`에 단일 리스너로 모든 클릭 감지
170
187
  - **요소 정보 수집**: 태그명, 텍스트, 클래스, ID
188
+ - **추적 대상**: `A`, `BUTTON`, `INPUT`, `SELECT`, `TEXTAREA` 요소만 추적
171
189
  - **추적 제외**: `data-vircle-ignore` 속성으로 민감 요소 제외
190
+ - **data-* 속성 수집**: idle 시간에 비동기로 추가 data 속성 수집
172
191
 
173
192
  #### 폼 제출 추적
174
193
  - **자동 감지**: 모든 `form` 태그의 `submit` 이벤트
175
- - **수집 정보**: 폼 ID, action, method
194
+ - **수집 정보**: 폼 ID, name, action, method
176
195
  - **보안**: 폼 데이터는 수집하지 않음
177
196
 
178
197
  #### 에러 추적
179
- - **JavaScript 에러**: `window.onerror` 이벤트
180
- - **Promise 거부**: `unhandledrejection` 이벤트
181
- - **에러 정보**: 메시지, 스택, 파일명, 라인 번호
198
+ - **JavaScript 에러**: `ErrorEvent` 리스너 (메시지, 파일명, 라인, 컬럼, 스택)
199
+ - **Promise 거부**: `PromiseRejectionEvent` 리스너 (reason)
182
200
 
183
201
  ### 웹 컨텍스트 수집 (WebContextCollector)
184
202
 
185
- 자동으로 수집되는 브라우저 환경 정보:
203
+ `collect()` 호출 `device`, `page`, `app`, `custom` 4가지 컨텍스트를 **병렬로** 수집합니다.
204
+
205
+ #### device (DeviceContext)
206
+
207
+ User-Agent 기반 디바이스/브라우저/OS 감지 및 화면 정보 수집
208
+
209
+ | 필드 | 타입 | 소스 | 설명 |
210
+ |------|------|------|------|
211
+ | `type` | `'mobile' \| 'tablet' \| 'desktop'` | `navigator.userAgent` | UA 패턴 매칭으로 판별 |
212
+ | `os` | `string` | `navigator.userAgent` | Windows, macOS, Linux, Android, iOS, Chrome OS |
213
+ | `osVersion` | `string \| undefined` | `navigator.userAgent` | OS 버전 (예: `14.0`) |
214
+ | `browser` | `string` | `navigator.userAgent` | Edge, Chrome, Firefox, Safari, Opera, IE |
215
+ | `browserVersion` | `string \| undefined` | `navigator.userAgent` | 브라우저 버전 (예: `120.0`) |
216
+ | `screenResolution` | `string` | `screen.width/height` | `"1920x1080"` 형태 |
217
+ | `viewport` | `{ width, height }` | `window.innerWidth/Height` | 현재 뷰포트 크기 |
218
+ | `language` | `string` | `navigator.language` | 브라우저 언어 (예: `ko-KR`) |
219
+ | `timezone` | `string` | `Intl.DateTimeFormat` | IANA 타임존 (예: `Asia/Seoul`) |
220
+
221
+ #### page (PageContext)
222
+
223
+ 현재 페이지 URL 파싱 결과
224
+
225
+ | 필드 | 타입 | 소스 | 설명 |
226
+ |------|------|------|------|
227
+ | `url` | `string` | `window.location.href` | 전체 URL |
228
+ | `title` | `string` | `document.title` | 페이지 제목 |
229
+ | `path` | `string` | `URL.pathname` | 경로 (예: `/products`) |
230
+ | `search` | `string` | `URL.search` | 쿼리스트링 (예: `?category=electronics`) |
231
+ | `hash` | `string` | `URL.hash` | 해시 프래그먼트 |
232
+
233
+ #### app (AppContext)
234
+
235
+ 애플리케이션 메타 정보 (HTML meta 태그 기반)
236
+
237
+ | 필드 | 타입 | 소스 | 설명 |
238
+ |------|------|------|------|
239
+ | `name` | `string` | `<meta name="application-name">` 또는 `<meta property="og:site_name">` 또는 `document.title` | 앱 이름 |
240
+ | `version` | `string \| undefined` | `<meta name="version">` | 앱 버전 |
241
+ | `environment` | `'development' \| 'production'` | `window.location.hostname` | localhost/staging → development, 그 외 → production |
242
+
243
+ #### custom (Record<string, unknown>)
244
+
245
+ 추가 브라우저 환경 정보 (가용한 경우에만 수집)
246
+
247
+ | 필드 | 타입 | 조건 | 설명 |
248
+ |------|------|------|------|
249
+ | `referrer` | `string` | `document.referrer` 존재 시 | 이전 페이지 URL |
250
+ | `campaign` | `{ source, medium, campaign, term, content }` | URL에 `utm_*` 파라미터 존재 시 | UTM 캠페인 파라미터 (`utm_` 접두사 제거) |
251
+ | `cookieEnabled` | `boolean` | 항상 | 쿠키 활성화 여부 |
252
+ | `doNotTrack` | `boolean` | `navigator.doNotTrack` 존재 시 | DNT 설정 여부 |
253
+ | `connection` | `{ effectiveType, downlink, rtt, saveData }` | Network Information API 지원 시 | 네트워크 연결 정보 |
254
+ | `memory` | `{ usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit }` | `performance.memory` 지원 시 (Chrome 전용) | JS 힙 메모리 사용량 |
255
+
256
+ #### 수집 결과 예시
186
257
 
187
258
  ```typescript
188
259
  {
189
260
  device: {
190
- type: 'desktop', // mobile | tablet | desktop
191
- browser: 'Chrome', // 브라우저 이름
192
- browserVersion: '120.0.0', // 브라우저 버전
193
- os: 'macOS', // 운영체제
194
- osVersion: '14.0', // OS 버전
195
- screenWidth: 1920, // 화면 너비
196
- screenHeight: 1080, // 화면 높이
197
- viewportWidth: 1200, // 뷰포트 너비
198
- viewportHeight: 800, // 뷰포트 높이
199
- language: 'ko-KR', // 언어
200
- timezone: 'Asia/Seoul' // 타임존
261
+ type: 'desktop',
262
+ os: 'macOS',
263
+ osVersion: '14.0',
264
+ browser: 'Chrome',
265
+ browserVersion: '120.0',
266
+ screenResolution: '1920x1080',
267
+ viewport: { width: 1200, height: 800 },
268
+ language: 'ko-KR',
269
+ timezone: 'Asia/Seoul'
201
270
  },
202
271
  page: {
203
272
  url: 'https://example.com/products?category=electronics',
204
273
  title: '제품 목록',
205
274
  path: '/products',
206
275
  search: '?category=electronics',
207
- hash: '',
208
- referrer: 'https://google.com'
276
+ hash: ''
277
+ },
278
+ app: {
279
+ name: 'My Store',
280
+ version: '2.1.0',
281
+ environment: 'production'
209
282
  },
210
283
  custom: {
211
- utm_source: 'google',
212
- utm_medium: 'cpc',
213
- utm_campaign: 'summer_sale',
214
- cookiesEnabled: true,
215
- network: {
284
+ referrer: 'https://google.com',
285
+ campaign: {
286
+ source: 'google',
287
+ medium: 'cpc',
288
+ campaign: 'summer_sale'
289
+ },
290
+ cookieEnabled: true,
291
+ doNotTrack: false,
292
+ connection: {
216
293
  effectiveType: '4g',
217
- downlink: 10.0
294
+ downlink: 10.0,
295
+ rtt: 50,
296
+ saveData: false
297
+ },
298
+ memory: {
299
+ usedJSHeapSize: 12345678,
300
+ totalJSHeapSize: 33554432,
301
+ jsHeapSizeLimit: 2147483648
218
302
  }
219
303
  }
220
304
  }
221
305
  ```
222
306
 
223
- ### LocalStorage 어댑터
307
+ ### 스토리지 어댑터
224
308
 
225
- 브라우저의 LocalStorage를 활용한 데이터 영속성:
309
+ SDK는 `StorageFactory`를 통해 환경에 최적화된 스토리지를 자동 선택합니다:
226
310
 
311
+ #### IndexedDB 어댑터 (권장)
312
+ - **대용량 지원**: LocalStorage 5MB 제한 없이 대량 이벤트 저장
313
+ - **비동기 처리**: UI 블로킹 없는 백그라운드 저장
314
+ - **트랜잭션 지원**: 데이터 무결성 보장
315
+ - **LRU 정리**: 타임스탬프 인덱스 기반 오래된 데이터 자동 정리
316
+
317
+ #### LocalStorage 어댑터
227
318
  - **자동 용량 관리**: QuotaExceededError 시 LRU 캐시 정리
228
319
  - **접두사 격리**: `vircle_` 접두사로 다른 데이터와 분리
229
320
  - **타임스탬프 추적**: 데이터 저장 시간 기록
230
321
  - **안전한 폴백**: LocalStorage 불가 시 경고 후 계속 작동
231
322
 
323
+ #### 스토리지 타입 선택
324
+
325
+ ```typescript
326
+ const vircle = new VircleWeb({
327
+ apiKey: 'your-api-key',
328
+ storageType: 'auto', // 'auto' | 'localStorage' | 'indexedDB'
329
+ });
330
+ ```
331
+
332
+ | 타입 | 설명 |
333
+ |------|------|
334
+ | `auto` (기본값) | IndexedDB 우선, 미지원 시 LocalStorage 폴백 |
335
+ | `localStorage` | 항상 LocalStorage 사용 |
336
+ | `indexedDB` | 항상 IndexedDB 사용 |
337
+
338
+ `StorageFactory`는 동기(`create`) 및 비동기(`createAsync`) 생성을 모두 지원합니다. 비동기 방식은 Safari Private 모드 등에서 IndexedDB 실제 사용 가능 여부를 테스트한 후 안전하게 폴백합니다.
339
+
340
+ ### 암호화 (WebExtendedCryptoAdapter)
341
+
342
+ Web Crypto API 기반의 하이브리드 암호화를 제공합니다:
343
+
344
+ ```
345
+ 데이터 (JSON) → AES-256-GCM 암호화 → 암호문 + Auth Tag
346
+ AES 키 → RSA-OAEP (서버 공개키) → 암호화된 키
347
+ 결과: { data, key, iv, authTag, metadata }
348
+ ```
349
+
350
+ #### 암호화 활성화
351
+
352
+ ```typescript
353
+ const vircle = new VircleWeb({
354
+ apiKey: 'your-api-key',
355
+ enableEncryption: true, // Web Crypto API 기반 암호화 활성화
356
+ });
357
+ ```
358
+
359
+ #### 제공 기능
360
+ - **하이브리드 암호화**: AES-256-GCM (데이터) + RSA-OAEP (키 래핑)
361
+ - **UUID v4 생성**: `crypto.randomUUID` 또는 `getRandomValues` 폴백
362
+ - **HMAC-SHA256 서명**: 페이로드 무결성 검증
363
+ - **자동 가용성 감지**: Web Crypto API 미지원 시 암호화 자동 비활성화
364
+
365
+ #### 암호화된 페이로드 구조
366
+
367
+ ```typescript
368
+ interface EncryptedPayload {
369
+ data: string; // Base64 인코딩된 AES-GCM 암호문
370
+ key: string; // Base64 인코딩된 RSA-OAEP 암호화 AES 키
371
+ iv: string; // Base64 인코딩된 초기화 벡터 (96-bit)
372
+ authTag: string; // Base64 인코딩된 GCM 인증 태그 (128-bit)
373
+ metadata: {
374
+ algorithm: 'AES-256-GCM';
375
+ keyAlgorithm: 'RSA-OAEP';
376
+ timestamp: string;
377
+ };
378
+ }
379
+ ```
380
+
381
+ ### 세션 타임아웃
382
+
383
+ SDK는 사용자 비활성 상태를 감지하여 자동으로 새 세션을 시작합니다:
384
+
385
+ - **기본 타임아웃**: 30분 (설정 가능)
386
+ - **동작 방식**: `visibilitychange → visible` 이벤트 시 마지막 활동 시간과 비교
387
+ - **세션 로테이션**: 타임아웃 경과 시 새 세션 ID 생성 + `session_start` 이벤트 자동 추적
388
+ - **활동 갱신**: `track()` 호출 시마다 마지막 활동 시간 갱신
389
+
390
+ ```typescript
391
+ const vircle = new VircleWeb({
392
+ apiKey: 'your-api-key',
393
+ sessionTimeout: 15 * 60 * 1000, // 15분
394
+ });
395
+ ```
396
+
397
+ ### sendBeacon (페이지 이탈 보호)
398
+
399
+ 페이지 종료 시 `navigator.sendBeacon`을 사용하여 대기 중인 이벤트를 안전하게 전송합니다:
400
+
401
+ - **beforeunload**: 페이지 닫기/새로고침 시 큐의 이벤트를 beacon으로 전송
402
+ - **visibilitychange → hidden**: 탭 전환/최소화 시 beacon으로 전송
403
+ - **제약사항**: sendBeacon은 커스텀 헤더를 설정할 수 없으므로 API 키를 요청 body에 포함하며, 암호화는 스킵됩니다
404
+ - **폴백**: `navigator.sendBeacon` 미지원 환경에서는 자동으로 무시됩니다
405
+
406
+ ```typescript
407
+ const vircle = new VircleWeb(
408
+ { apiKey: 'your-api-key' },
409
+ { flushOnUnload: true }, // 기본값: true
410
+ );
411
+ ```
412
+
413
+ ### 에러 클래스
414
+
415
+ SDK는 디버깅을 위한 구조화된 에러 클래스를 제공합니다:
416
+
417
+ | 에러 클래스 | 코드 | 설명 |
418
+ |------------|------|------|
419
+ | `VircleWebError` | - | 기본 에러 클래스 (코드, 상세정보 포함) |
420
+ | `VircleConfigError` | `CONFIG_ERROR` | 잘못된 설정 (API 키 누락 등) |
421
+ | `VircleInitializationError` | `INITIALIZATION_ERROR` | 초기화 실패 |
422
+ | `VircleStorageError` | `STORAGE_ERROR` | 스토리지 작업 실패 |
423
+ | `VircleBrowserCompatibilityError` | `BROWSER_COMPATIBILITY_ERROR` | 브라우저 미지원 기능 |
424
+
425
+ ```typescript
426
+ import { VircleWebError, VircleConfigError } from '@vircle/sdk-web';
427
+
428
+ try {
429
+ const vircle = new VircleWeb({ apiKey: '' });
430
+ await vircle.initialize();
431
+ } catch (error) {
432
+ if (error instanceof VircleConfigError) {
433
+ console.error('설정 오류:', error.code, error.details);
434
+ }
435
+ }
436
+ ```
437
+
232
438
  ### 성능 최적화 기술
233
439
 
234
- - **requestIdleCallback 활용**: UI 블로킹 없는 백그라운드 이벤트 처리
235
- - **컨텍스트 캐싱**: 브라우저 정보 캐싱으로 반복적 DOM 접근 방지
440
+ - **requestIdleCallback 활용**: TaskScheduler를 통한 UI 블로킹 없는 백그라운드 이벤트 처리
441
+ - **컨텍스트 캐싱**: ContextCache를 통한 브라우저 정보 캐싱 (기본 5분), 중복 수집 방지를 위한 Promise 재사용
236
442
  - **이벤트 배칭**: TransportService의 큐를 통한 네트워크 요청 최소화
237
- - **메모리 관리**: cleanup 시 모든 이벤트 리스너 제거로 누수 방지
443
+ - **메모리 관리**: cleanup 시 모든 이벤트 리스너 제거 및 History API 원복으로 누수 방지
444
+ - **페이지 이탈 대응**: `navigator.sendBeacon`을 사용한 페이지 종료 시 안정적 이벤트 전송
445
+ - **큐 크기 제한**: TaskScheduler의 `maxQueueSize`(기본 1000)로 메모리 오버플로우 방지
238
446
 
239
447
  ### 추적 제외
240
448
 
@@ -255,26 +463,43 @@ SDK는 `setupAutoTracking` 메서드를 통해 다양한 브라우저 이벤트
255
463
 
256
464
  ### VircleWebConfig
257
465
 
466
+ `VircleConfig` (코어)를 상속하며 웹 전용 옵션을 추가합니다.
467
+
258
468
  ```typescript
259
- interface VircleWebConfig {
260
- apiKey: string; // 필수: API
261
- endpoint?: string; // API 엔드포인트 (선택)
262
- trackPageViews?: boolean; // 페이지뷰 자동 추적 (기본: false)
263
- trackClicks?: boolean; // 클릭 자동 추적 (기본: false)
264
- trackForms?: boolean; // 제출 자동 추적 (기본: false)
265
- trackErrors?: boolean; // 에러 자동 추적 (기본: false)
266
- singlePageApp?: boolean; // SPA 모드 (기본: false)
267
- storagePrefix?: string; // 스토리지 접두사 (기본: 'vircle_')
469
+ interface VircleWebConfig extends VircleConfig {
470
+ trackPageViews?: boolean; // 자동 페이지뷰 추적 (기본: false)
471
+ trackClicks?: boolean; // 자동 클릭 추적 (기본: false)
472
+ trackForms?: boolean; // 자동 폼 제출 추적 (기본: false)
473
+ trackErrors?: boolean; // 자동 에러 추적 (기본: false)
474
+ singlePageApp?: boolean; // SPA 모드 (기본: false)
475
+ storagePrefix?: string; // 스토리지 접두사 (기본: 'vircle_')
476
+ storageType?: StorageType; // 스토리지 타입: 'auto' | 'localStorage' | 'indexedDB' (기본: 'auto')
477
+ enableEncryption?: boolean; // Web Crypto API 암호화 활성화 (기본: false)
478
+ sessionTimeout?: number; // 세션 타임아웃(ms) (기본: 1800000 = 30분)
268
479
  }
269
480
  ```
270
481
 
482
+ #### VircleConfig (코어) 주요 옵션
483
+
484
+ | 옵션 | 타입 | 기본값 | 설명 |
485
+ |------|------|--------|------|
486
+ | `apiKey` | `string` | (필수) | API 인증 키 |
487
+ | `environment` | `'development' \| 'production'` | `'production'` | 환경 설정 |
488
+ | `debug` | `boolean` | `false` | 디버그 모드 |
489
+ | `batch.size` | `number` | `50` | 배치당 최대 이벤트 수 |
490
+ | `batch.timeout` | `number` | `5000` | 배치 대기 시간(ms) |
491
+ | `batch.flushInterval` | `number` | `10000` | 자동 플러시 간격(ms) |
492
+ | `retry.maxAttempts` | `number` | `3` | 최대 재시도 횟수 |
493
+ | `retry.initialDelay` | `number` | `500` | 초기 백오프 지연(ms) |
494
+ | `privacy.respectDoNotTrack` | `boolean` | `true` | DNT 헤더 존중 |
495
+ | `headers` | `Record<string, string>` | - | 커스텀 HTTP 헤더 |
496
+
271
497
  ### VircleWebOptions
272
498
 
273
499
  ```typescript
274
- interface VircleWebOptions {
275
- flushOnUnload?: boolean; // 페이지 언로드 시 이벤트 전송 (기본: true)
276
- contextCacheTime?: number; // 컨텍스트 캐시 시간(ms) (기본: 300000)
277
- enablePerformance?: boolean; // 성능 측정 활성화 (기본: true)
500
+ interface VircleWebOptions extends Partial<VircleCoreOptions> {
501
+ flushOnUnload?: boolean; // 페이지 언로드 시 sendBeacon 전송 (기본: true)
502
+ contextCacheTime?: number; // 컨텍스트 캐시 시간(ms) (기본: 300000 = 5분)
278
503
  }
279
504
  ```
280
505
 
@@ -291,14 +516,14 @@ SDK를 초기화하고 자동 추적을 시작합니다.
291
516
  ### 이벤트 추적
292
517
 
293
518
  ```typescript
294
- await vircle.track(
295
- eventName: string,
296
- properties?: Record<string, unknown>,
519
+ await vircle.track<TProperties>(
520
+ name: string,
521
+ properties?: TProperties,
297
522
  context?: EventContext
298
523
  ): Promise<void>
299
524
  ```
300
525
 
301
- 커스텀 이벤트를 추적합니다.
526
+ 커스텀 이벤트를 추적합니다. `requestIdleCallback`을 활용하여 UI를 블로킹하지 않으며, 호출 시마다 세션의 마지막 활동 시간이 갱신됩니다.
302
527
 
303
528
  ### 페이지뷰 추적
304
529
 
@@ -309,18 +534,18 @@ await vircle.trackPageView(
309
534
  ): Promise<void>
310
535
  ```
311
536
 
312
- 페이지뷰를 수동으로 추적합니다.
537
+ 페이지뷰를 수동으로 추적합니다. URL, 제목, 리퍼러 등의 페이지 컨텍스트를 자동 수집합니다.
313
538
 
314
539
  ### 사용자 식별
315
540
 
316
541
  ```typescript
317
- await vircle.identify(
542
+ await vircle.identify<TTraits>(
318
543
  userId: string,
319
- traits?: UserProperties
544
+ traits?: TTraits
320
545
  ): Promise<void>
321
546
  ```
322
547
 
323
- 사용자를 식별하고 속성을 설정합니다.
548
+ 사용자를 식별하고 속성을 설정합니다. VircleWeb에서는 호출 시 컨텍스트 캐시를 자동으로 무효화합니다.
324
549
 
325
550
  ### 사용자 리셋
326
551
 
@@ -328,7 +553,38 @@ await vircle.identify(
328
553
  await vircle.reset(): Promise<void>
329
554
  ```
330
555
 
331
- 사용자 정보를 초기화합니다.
556
+ 사용자 정보와 세션을 초기화합니다.
557
+
558
+ ### 유틸리티
559
+
560
+ ```typescript
561
+ // 현재 페이지 URL
562
+ vircle.getCurrentUrl(): string
563
+
564
+ // 네트워크 연결 상태
565
+ vircle.isOnline(): boolean
566
+
567
+ // SDK 상태 확인
568
+ vircle.getStatus(): {
569
+ isInitialized: boolean;
570
+ sessionId?: string;
571
+ userId?: string;
572
+ pluginCount: number;
573
+ queueSize: number;
574
+ }
575
+
576
+ // 런타임 메트릭 조회
577
+ vircle.getMetrics(): {
578
+ queueSize: number;
579
+ isProcessing: boolean;
580
+ }
581
+
582
+ // 이벤트 수동 플러시
583
+ await vircle.flush(): Promise<void>
584
+
585
+ // 이벤트 수동 플러시 (타임아웃 지정)
586
+ await vircle.flush(timeoutMs?: number): Promise<void>
587
+ ```
332
588
 
333
589
  ### 정리
334
590
 
@@ -336,18 +592,64 @@ await vircle.reset(): Promise<void>
336
592
  await vircle.cleanup(): Promise<void>
337
593
  ```
338
594
 
339
- 모든 이벤트 리스너를 제거하고 리소스를 정리합니다.
595
+ SDK가 사용하는 모든 리소스를 정리합니다:
596
+ 1. 이벤트 리스너 제거 (`beforeunload`, `error`, `click`, `submit`, `popstate`, `visibilitychange`)
597
+ 2. History API 원복 (`pushState`, `replaceState`)
598
+ 3. 대기 중인 이벤트 전송 (`flush()`)
599
+ 4. 코어 서비스 정리 (`super.cleanup()`)
600
+
601
+ ## Exports
602
+
603
+ ```typescript
604
+ // 메인 SDK 클래스
605
+ import { VircleWeb } from '@vircle/sdk-web';
606
+ import VircleWeb from '@vircle/sdk-web'; // default export
607
+
608
+ // 설정 타입
609
+ import type { VircleWebConfig, VircleWebOptions } from '@vircle/sdk-web';
610
+
611
+ // 컨텍스트 수집기
612
+ import { WebContextCollector } from '@vircle/sdk-web';
613
+
614
+ // 스토리지
615
+ import { LocalStorageAdapter, IndexedDBAdapter, StorageFactory } from '@vircle/sdk-web';
616
+ import type { StorageType } from '@vircle/sdk-web'; // 'auto' | 'localStorage' | 'indexedDB'
617
+
618
+ // 암호화 어댑터
619
+ import { WebExtendedCryptoAdapter } from '@vircle/sdk-web';
620
+
621
+ // 에러 클래스
622
+ import {
623
+ VircleWebError,
624
+ VircleConfigError,
625
+ VircleInitializationError,
626
+ VircleStorageError,
627
+ VircleBrowserCompatibilityError,
628
+ } from '@vircle/sdk-web';
629
+
630
+ // Core 타입 재수출
631
+ import type {
632
+ VircleConfig,
633
+ EventContext,
634
+ DeviceContext,
635
+ PageContext,
636
+ AppContext,
637
+ UserTraits,
638
+ StorageAdapter,
639
+ } from '@vircle/sdk-web';
640
+ ```
340
641
 
341
642
  ## 빌드 파일 설명
342
643
 
343
- | 파일 | 용도 | 의존성 | 사용 방법 |
344
- |------|------|--------|-----------|
345
- | `dist/index.js` | CommonJS (Node.js/번들러) | 별도 설치 필요 | `require('@vircle/sdk-web')` |
346
- | `dist/index.esm.js` | ES Modules (번들러) | 별도 설치 필요 | `import { VircleWeb } from '@vircle/sdk-web'` |
347
- | `dist/vircle-web-sdk.standalone.esm.js` | ES Modules (브라우저) | 모두 포함 | `<script type="module">` |
348
- | `dist/vircle-web-sdk.min.js` | UMD (레거시 브라우저) | 모두 포함 | `<script>` |
644
+ | 파일 | 포맷 | 용도 | `@vircle/sdk-core-ts` | 사용 방법 |
645
+ |------|------|------|----------------------|-----------|
646
+ | `dist/index.js` | CJS | 번들러 (Webpack 등) | 외부 의존성 | `require('@vircle/sdk-web')` |
647
+ | `dist/index.esm.js` | ESM | 번들러 (Vite, Rollup 등) | 외부 의존성 | `import { VircleWeb } from '@vircle/sdk-web'` |
648
+ | `dist/vircle-web-sdk.standalone.esm.js` | ESM | 브라우저 직접 사용 | 번들에 포함 | `<script type="module">` |
649
+ | `dist/vircle-web-sdk.min.js` | UMD | 레거시 브라우저 | 번들에 포함 | `<script src="...">` |
650
+ | `dist/index.d.ts` | DTS | TypeScript 타입 정의 | - | 자동 적용 |
349
651
 
350
- ⚠️ **중요**: 브라우저에서 직접 사용할 때는 반드시 `standalone` 또는 `min` 빌드를 사용하세요.
652
+ **중요**: 브라우저에서 직접 사용할 때는 반드시 `standalone` 또는 `min` 빌드를 사용하세요. npm 패키지로 사용 시 번들러가 `@vircle/sdk-core-ts`를 함께 번들링합니다.
351
653
 
352
654
  ## 브라우저 지원
353
655
 
@@ -357,51 +659,49 @@ await vircle.cleanup(): Promise<void>
357
659
  - iOS Safari: iOS 12+
358
660
  - Chrome for Android: Android 6+
359
661
 
662
+ > 암호화 기능(`enableEncryption`)은 Web Crypto API를 지원하는 브라우저에서만 동작합니다. 미지원 시 자동으로 비활성화됩니다.
663
+
360
664
  ## 보안 고려사항
361
665
 
362
666
  1. **클라이언트 측 API 키**: 브라우저에서 사용되는 API 키는 공개됩니다. 데이터 수집(Write) 권한만 가진 키를 사용하세요.
363
667
 
364
668
  2. **민감한 데이터**: 민감한 데이터가 수집되지 않도록 `data-vircle-ignore` 속성을 적절히 사용하세요.
365
669
 
366
- 3. **LocalStorage**: SDK는 LocalStorage를 사용하여 데이터를 임시 저장합니다. 민감한 정보는 저장하지 않습니다.
670
+ 3. **스토리지**: SDK는 LocalStorage/IndexedDB를 사용하여 데이터를 임시 저장합니다. 민감한 정보는 저장하지 않습니다.
671
+
672
+ 4. **암호화**: `enableEncryption: true` 설정 시 서버 공개키로 이벤트 페이로드를 암호화합니다. 공개키는 CDN(`RemoteConfigService`)에서 자동 fetch됩니다.
673
+
674
+ 5. **sendBeacon 전송**: 페이지 종료 시 `sendBeacon`으로 전송되는 이벤트는 커스텀 헤더 설정이 불가하여 암호화/HMAC 서명 없이 전송됩니다. API 키는 요청 body에 포함됩니다. 정상 동작 중에는 `fetch()` 기반의 암호화된 전송이 사용됩니다.
367
675
 
368
676
  ## 성능 영향
369
677
 
370
- - **번들 크기**: ~13.5KB (minified), ~4.5KB (gzipped)
371
- - **초기화 시간**: < 10ms
372
- - **이벤트 추적**: < 1ms (requestIdleCallback 사용)
678
+ - **번들 크기 (standalone)**: ~106KB (minified, core 포함), npm 패키지는 ~70KB (core 외부 의존성)
679
+ - **이벤트 추적**: requestIdleCallback 기반으로 UI 블로킹 없음
680
+ - **컨텍스트 수집**: 캐싱으로 반복적인 DOM 접근 방지 (기본 5분)
373
681
 
374
682
  ## 고급 사용 예제
375
683
 
376
684
  ### SPA (Single Page Application) 설정
377
685
 
378
686
  ```typescript
687
+ // singlePageApp: true 설정 시 History API (pushState, replaceState)를 래핑하여
688
+ // 라우트 변경을 자동으로 감지하고 페이지뷰를 추적합니다.
689
+ // React Router, Vue Router, Next.js 등 대부분의 SPA 프레임워크와 호환됩니다.
379
690
  const vircle = new VircleWeb({
380
- apiKey: 'your-api-key',
381
- singlePageApp: true, // SPA 모드 활성화
382
- trackPageViews: true
383
- })
384
-
385
- await vircle.initialize()
386
-
387
- // React Router 예제
388
- import { useEffect } from 'react'
389
- import { useLocation } from 'react-router-dom'
390
-
391
- function App() {
392
- const location = useLocation()
393
-
394
- useEffect(() => {
395
- // 라우트 변경 시 자동으로 페이지뷰가 추적됩니다
396
- }, [location])
691
+ apiKey: 'your-api-key',
692
+ singlePageApp: true, // History API 래핑
693
+ trackPageViews: true, // 자동 페이지뷰 추적
694
+ });
397
695
 
398
- return <Routes>...</Routes>
399
- }
696
+ await vircle.initialize();
697
+ // 이후 별도의 설정 없이 라우트 변경 시 자동으로 page_view 이벤트가 추적됩니다.
400
698
  ```
401
699
 
402
700
  ### 에러 처리와 함께 사용
403
701
 
404
702
  ```typescript
703
+ import { VircleWeb, VircleInitializationError } from '@vircle/sdk-web';
704
+
405
705
  try {
406
706
  const vircle = new VircleWeb({
407
707
  apiKey: 'your-api-key',
@@ -422,10 +722,27 @@ try {
422
722
  throw error;
423
723
  }
424
724
  } catch (error) {
425
- console.error('SDK 초기화 실패:', error);
725
+ if (error instanceof VircleInitializationError) {
726
+ console.error('SDK 초기화 실패:', error.code, error.details);
727
+ }
426
728
  }
427
729
  ```
428
730
 
731
+ ### 암호화 활성화
732
+
733
+ ```typescript
734
+ const vircle = new VircleWeb({
735
+ apiKey: 'your-api-key',
736
+ enableEncryption: true, // Web Crypto API 가용 시 자동 활성화
737
+ trackPageViews: true,
738
+ });
739
+
740
+ await vircle.initialize();
741
+
742
+ // 이후 모든 이벤트가 하이브리드 암호화되어 전송됩니다
743
+ await vircle.track('purchase', { amount: 99.99 });
744
+ ```
745
+
429
746
  ### 성능 최적화 설정
430
747
 
431
748
  ```typescript
@@ -434,18 +751,16 @@ const vircle = new VircleWeb(
434
751
  apiKey: 'your-api-key',
435
752
  trackPageViews: true,
436
753
  trackClicks: true,
754
+ sessionTimeout: 15 * 60 * 1000, // 15분 세션 타임아웃
437
755
  },
438
756
  {
439
- contextCacheTime: 600000, // 10분 캐싱
440
- flushOnUnload: false, // 수동으로 flush 관리
441
- enablePerformance: false, // 성능 추적 비활성화
757
+ contextCacheTime: 600000, // 10분 캐싱 (기본 5분)
758
+ flushOnUnload: true, // sendBeacon으로 페이지 이탈 시 자동 전송 (기본값)
442
759
  },
443
760
  );
444
761
 
445
- // 중요한 시점에 수동으로 flush
446
- window.addEventListener('beforeunload', async () => {
447
- await vircle.flush();
448
- });
762
+ // 중요한 시점에 수동으로 flush (예: 결제 완료 후)
763
+ await vircle.flush();
449
764
  ```
450
765
 
451
766
  ### 민감한 정보가 있는 페이지
@@ -531,30 +846,51 @@ await vircle.flush();
531
846
  ### 메모리 누수 방지
532
847
 
533
848
  ```typescript
534
- // 컴포넌트 언마운트 시 반드시 cleanup 호출
535
- // React 예제
849
+ // React 예제 - 컴포넌트 언마운트 시 반드시 cleanup 호출
850
+ // 일반적으로 SDK는 앱 레벨에서 한 번만 초기화합니다
536
851
  useEffect(() => {
537
- const vircle = new VircleWeb({ apiKey: 'your-api-key' });
852
+ const vircle = new VircleWeb({
853
+ apiKey: 'your-api-key',
854
+ trackPageViews: true,
855
+ });
538
856
  vircle.initialize();
539
857
 
540
858
  return () => {
859
+ // cleanup()은 이벤트 리스너 제거, History API 원복, 큐 플러시를 수행합니다
541
860
  vircle.cleanup();
542
861
  };
543
862
  }, []);
544
863
  ```
545
864
 
865
+ ### 암호화 관련
866
+
867
+ ```typescript
868
+ // 암호화 미지원 환경에서 enableEncryption: true로 설정한 경우
869
+ // 콘솔에 경고 메시지가 출력되고 암호화 없이 동작합니다
870
+ // "[Vircle] Web Crypto API를 사용할 수 없습니다. 암호화를 비활성화합니다."
871
+ ```
872
+
546
873
  ## TypeScript 지원
547
874
 
548
875
  SDK는 완전한 TypeScript 타입 정의를 제공합니다:
549
876
 
550
877
  ```typescript
551
- import { VircleWeb, VircleWebConfig, VircleWebOptions, EventContext, UserProperties } from '@vircle/sdk-web';
878
+ import {
879
+ VircleWeb,
880
+ VircleWebConfig,
881
+ VircleWebOptions,
882
+ WebExtendedCryptoAdapter,
883
+ VircleWebError,
884
+ } from '@vircle/sdk-web';
885
+ import type { EventContext, UserTraits, StorageType } from '@vircle/sdk-web';
552
886
 
553
887
  // 타입 안전한 설정
554
888
  const config: VircleWebConfig = {
555
889
  apiKey: process.env.VIRCLE_API_KEY!,
556
890
  trackPageViews: true,
557
891
  trackErrors: true,
892
+ enableEncryption: true,
893
+ storageType: 'auto',
558
894
  };
559
895
 
560
896
  const options: VircleWebOptions = {
@@ -565,14 +901,15 @@ const options: VircleWebOptions = {
565
901
  const vircle = new VircleWeb(config, options);
566
902
 
567
903
  // 타입 안전한 이벤트 추적
568
- interface PurchaseEvent {
904
+ // track()의 제네릭 파라미터는 Record<string, unknown>을 확장해야 합니다
905
+ interface PurchaseProperties {
569
906
  product_id: string;
570
907
  price: number;
571
908
  currency: string;
572
909
  quantity: number;
573
910
  }
574
911
 
575
- await vircle.track<PurchaseEvent>('purchase', {
912
+ await vircle.track<PurchaseProperties>('purchase', {
576
913
  product_id: 'sku_123',
577
914
  price: 49.99,
578
915
  currency: 'USD',
@@ -580,13 +917,14 @@ await vircle.track<PurchaseEvent>('purchase', {
580
917
  });
581
918
 
582
919
  // 타입 안전한 사용자 속성
583
- interface UserTraits extends UserProperties {
920
+ // identify()의 제네릭 파라미터는 Record<string, unknown>을 확장해야 합니다
921
+ interface CustomUserTraits {
584
922
  email: string;
585
923
  plan: 'free' | 'pro' | 'enterprise';
586
924
  created_at: string;
587
925
  }
588
926
 
589
- await vircle.identify<UserTraits>('user-123', {
927
+ await vircle.identify<CustomUserTraits>('user-123', {
590
928
  email: 'user@example.com',
591
929
  plan: 'pro',
592
930
  created_at: new Date().toISOString(),
@@ -595,4 +933,4 @@ await vircle.identify<UserTraits>('user-123', {
595
933
 
596
934
  ## 라이선스
597
935
 
598
- MIT
936
+ MIT