브라우저 확장 프로그램이나 기업 포털의 임베디드 UI에서 호스트 앱의 디자인 시스템을 재현하는 것은 어렵습니다. 난독화된 클래스 이름이나 프레임워크 고유 스타일을 추적하는 대신, 기존 네이티브 요소를 복제하고 필요한 부분만 교체하면 시각적 일관성을 “무료로” 얻을 수 있습니다.
코드
interface CloneButtonOptions { /** 복제할 템플릿 요소의 선택자 */ templateSelector: string /** 교체할 새 텍스트 */ text: string /** 교체할 새 아이콘 (선택사항) */ icon?: string | HTMLElement /** 클릭 핸들러 */ onClick: (event: MouseEvent) => void}
/** * 호스트 앱의 기존 버튼을 복제하여 새 버튼 생성 */export const cloneNativeButton = ( options: CloneButtonOptions): HTMLElement | null => { const { templateSelector, text, icon, onClick } = options
// 템플릿 요소 가져오기 const template = document.querySelector<HTMLElement>(templateSelector) if (!template) { console.warn(`Template not found: ${templateSelector}`) return null }
// 깊은 복제 생성 (이벤트 핸들러 제외) const cloned = template.cloneNode(true) as HTMLElement
// 텍스트 노드 교체 const textNode = findTextNode(cloned) if (textNode) { textNode.textContent = text }
// 아이콘 교체 (지정된 경우) if (icon) { const iconElement = findIconElement(cloned) if (iconElement) { const newIcon = typeof icon === 'string' ? createIconFromString(icon) : icon iconElement.replaceWith(newIcon) } }
// 이벤트 리스너는 복제되지 않으므로 새로 설정 cloned.addEventListener('click', onClick)
// ID가 있으면 제거 (중복 방지) if (cloned.id) { cloned.removeAttribute('id') }
return cloned}
/** * 요소 내 첫 번째 텍스트 노드 찾기 */const findTextNode = (element: HTMLElement): Text | null => { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // 공백만 있는 노드 제외 return node.textContent?.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT } } ) return walker.nextNode() as Text | null}
/** * 요소 내 첫 번째 아이콘 요소 찾기 (svg, img, i 등) */const findIconElement = (element: HTMLElement): Element | null => { return ( element.querySelector('svg') || element.querySelector('img') || element.querySelector('i[class*="icon"]') || element.querySelector('[role="img"]') )}
/** * HTML 문자열에서 아이콘 요소 생성 */const createIconFromString = (html: string): HTMLElement => { const template = document.createElement('template') template.innerHTML = html.trim() return template.content.firstElementChild as HTMLElement}사용 예시
// GitHub의 기존 버튼 스타일 재사용const myButton = cloneNativeButton({ templateSelector: '.Button--primary', text: 'Deploy', icon: '<svg>...</svg>', onClick: () => { console.log('Deploying...') }})
// DOM에 추가document.querySelector('.toolbar')?.appendChild(myButton)작동 원리
querySelector로 복제할 템플릿 요소 가져오기cloneNode(true)로 깊은 복사 생성 (자식 요소 포함)TreeWalker로 텍스트 노드 탐색 및 교체querySelector로 아이콘 요소 찾아서 교체- 이벤트 리스너는 복제되지 않으므로
addEventListener로 새로 설정 id속성이 있으면 제거하여 중복 방지
cloneNode는 스타일, 클래스, data 속성을 보존하지만 이벤트 리스너는 복사되지 않는 점에 유의하세요.
장점
- 시각적 일관성: 호스트 앱의 디자인 시스템을 자연스럽게 상속
- CSS 불필요: 난독화된 클래스 이름이나 프레임워크 스타일을 추적할 필요 없음
- 유지보수성 높음: 호스트 디자인 업데이트에 자동으로 따라감
- 경량: DOM API만으로 구현되어 의존성 없음
주의사항
cloneNode는 DOM의 현재 상태를 복사하므로 동적으로 변하는 클래스나 스타일(호버 상태 등)은 복제되지 않습니다. 또한 템플릿 요소가 삭제되면 작동하지 않으므로 존재 확인을 포함해야 합니다.
응용 사례
- 브라우저 확장에서 시각적으로 일관된 버튼 추가
- 복잡한 기업 포털 디자인 시스템에 자연스럽게 녹아드는 기능 임베드
- 서드파티 대시보드에 커스텀 조작 패널 삽입
hsb.horse