UNPKG

@adstage/web-sdk

Version:

AdStage Web SDK - Production-ready marketing platform SDK with React Provider support for seamless integration

331 lines (288 loc) 8.93 kB
/** * SSR 안전한 DOM API 래퍼 클래스 * 서버사이드 렌더링 환경에서 DOM API 접근 시 오류를 방지합니다. */ export class DOMUtils { /** * 브라우저 환경 여부 체크 */ static isBrowser(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * SSR 환경 여부 체크 */ static isSSR(): boolean { return !this.isBrowser(); } /** * DOM 사용 가능 여부 체크 */ static canUseDOM(): boolean { return this.isBrowser() && document.readyState !== undefined; } /** * 안전한 getElementById */ static safeGetElementById(id: string): HTMLElement | null { if (!this.canUseDOM()) return null; return document.getElementById(id); } /** * 안전한 querySelector */ static safeQuerySelector(selector: string): HTMLElement | null { if (!this.canUseDOM()) return null; return document.querySelector(selector); } /** * 안전한 querySelectorAll */ static safeQuerySelectorAll(selector: string): HTMLElement[] { if (!this.canUseDOM()) return []; return Array.from(document.querySelectorAll(selector)) as HTMLElement[]; } /** * 안전한 createElement */ static safeCreateElement(tagName: string): HTMLElement | null { if (!this.canUseDOM()) return null; return document.createElement(tagName); } /** * 안전한 addEventListener */ static safeAddEventListener( element: HTMLElement | null, event: string, handler: EventListener, options?: boolean | AddEventListenerOptions ): void { if (!this.canUseDOM() || !element) return; element.addEventListener(event, handler, options); } /** * 안전한 removeEventListener */ static safeRemoveEventListener( element: HTMLElement | null, event: string, handler: EventListener, options?: boolean | EventListenerOptions ): void { if (!this.canUseDOM() || !element) return; element.removeEventListener(event, handler, options); } /** * 안전한 window 속성 접근 */ static getWindowProperty<T>(property: keyof Window, defaultValue: T): T { if (!this.isBrowser()) return defaultValue; return (window[property] as T) ?? defaultValue; } /** * 안전한 document 속성 접근 */ static getDocumentProperty<T>(property: keyof Document, defaultValue: T): T { if (!this.canUseDOM()) return defaultValue; return (document[property] as T) ?? defaultValue; } /** * 안전한 window.open */ static safeWindowOpen(url: string, target?: string, features?: string): Window | null { if (!this.isBrowser()) return null; return window.open(url, target, features); } /** * 안전한 getComputedStyle */ static safeGetComputedStyle(element: HTMLElement): CSSStyleDeclaration | null { if (!this.isBrowser() || !element) return null; return window.getComputedStyle(element); } /** * DOM Ready 상태 체크 */ static isDOMReady(): boolean { if (!this.canUseDOM()) return false; return document.readyState !== 'loading'; } /** * DOM Ready 대기 (SSR 안전) */ static waitForDOM(): Promise<void> { return new Promise((resolve) => { if (!this.canUseDOM()) { resolve(); // SSR 환경에서는 즉시 resolve return; } if (this.isDOMReady()) { resolve(); } else { this.safeAddEventListener(document as any, 'DOMContentLoaded', () => resolve()); } }); } /** * 안전한 스타일 적용 */ static safeApplyStyles(element: HTMLElement | null, styles: Record<string, string>): void { if (!this.canUseDOM() || !element) return; Object.entries(styles).forEach(([property, value]) => { element.style.setProperty(property, value); }); } /** * 안전한 클래스 추가 */ static safeAddClass(element: HTMLElement | null, className: string): void { if (!this.canUseDOM() || !element) return; element.classList.add(className); } /** * 안전한 클래스 제거 */ static safeRemoveClass(element: HTMLElement | null, className: string): void { if (!this.canUseDOM() || !element) return; element.classList.remove(className); } /** * 안전한 텍스트 콘텐츠 설정 */ static safeSetTextContent(element: HTMLElement | null, text: string): void { if (!this.canUseDOM() || !element) return; element.textContent = text; } /** * 안전한 HTML 콘텐츠 설정 */ static safeSetInnerHTML(element: HTMLElement | null, html: string): void { if (!this.canUseDOM() || !element) return; element.innerHTML = html; } /** * 안전한 자식 요소 추가 */ static safeAppendChild(parent: HTMLElement | null, child: HTMLElement | null): void { if (!this.canUseDOM() || !parent || !child) return; parent.appendChild(child); } /** * 안전한 자식 요소 제거 */ static safeRemoveChild(parent: HTMLElement | null, child: HTMLElement | null): void { if (!this.canUseDOM() || !parent || !child) return; parent.removeChild(child); } /** * 현재 페이지 정보 가져오기 (SSR 안전) */ static getPageInfo() { return { url: this.getWindowProperty('location', { href: '' }).href, title: this.getDocumentProperty('title', ''), referrer: this.getDocumentProperty('referrer', ''), }; } /** * 뷰포트 정보 가져오기 (SSR 안전) */ static getViewportInfo() { return { width: this.getWindowProperty('innerWidth', 0), height: this.getWindowProperty('innerHeight', 0), pixelRatio: this.getWindowProperty('devicePixelRatio', 1), }; } /** * 스크롤 정보 가져오기 (SSR 안전) */ static getScrollInfo() { const scrollTop = this.canUseDOM() ? (window.pageYOffset || document.documentElement.scrollTop) : 0; return { scrollTop, scrollLeft: this.getWindowProperty('pageXOffset', 0), }; } /** * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API) */ static async waitForElement( id: string, options: { timeout?: number; // 최대 대기 시간 (ms), 기본값: 3000 retryInterval?: number; // 재시도 간격 (ms), 기본값: 100 debug?: boolean; // 디버그 로그 출력 여부 } = {} ): Promise<HTMLElement> { const { timeout = 3000, retryInterval = 100, debug = false } = options; if (!this.canUseDOM()) { throw new Error('DOM을 사용할 수 없는 환경입니다.'); } // 즉시 찾을 수 있으면 바로 반환 const immediateElement = document.getElementById(id); if (immediateElement) { if (debug) { console.log(`✅ 컨테이너 즉시 발견: ${id}`); } return immediateElement; } if (debug) { console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`); } return new Promise((resolve, reject) => { let attempts = 0; const maxAttempts = Math.ceil(timeout / retryInterval); const checkElement = () => { attempts++; const element = document.getElementById(id); if (element) { if (debug) { console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`); } resolve(element); return; } if (attempts >= maxAttempts) { const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}" 다음을 확인해보세요: 1. HTML에 id="${id}" 요소가 있는지 확인 2. React 등에서 컴포넌트가 렌더링된 후 SDK 호출 3. 철자가 정확한지 확인 4. 중복된 ID가 없는지 확인 대기 시간: ${timeout}ms (${attempts}번 시도)`; if (debug) { console.error(errorMessage); } reject(new Error(errorMessage)); return; } if (debug && attempts % 10 === 0) { console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`); } // Exponential backoff: 처음엔 빠르게, 나중엔 느리게 const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500); setTimeout(checkElement, nextInterval); }; // 첫 번째 체크는 즉시 실행 setTimeout(checkElement, retryInterval); }); } /** * 여러 DOM 요소를 동시에 기다리기 */ static async waitForElements( ids: string[], options: { timeout?: number; retryInterval?: number; debug?: boolean; } = {} ): Promise<HTMLElement[]> { const promises = ids.map(id => this.waitForElement(id, options)); return Promise.all(promises); } }