웹 컴포넌트 성능과 API의 도전 과제
단 하나의 속성만을 가진 웹 컴포넌트를 만들기 위해 Custom Elements API를 사용하면 상당한 양의 코드가 필요합니다. 기본 구현은 문자열 속성만 지원하므로, 다른 데이터 유형은 직렬화를 통해 처리해야 합니다.
최소한의 컴포넌트 예시:
class MyFoo extends HTMLElement {
_bar = 0
get bar() {
return this._bar
}
set bar( next ) {
this.setAttribute( 'bar', next )
}
static observedAttributes = [ 'bar' ]
attributeChangedCallback( name, prev, next ) {
this[ '_' + name ] = next
}
}
globalThis.customElements.define( 'my-foo', MyFoo )
숫자 타입을 지원하기 위해 JSON 직렬화를 추가하고, 재귀 호출을 방지하기 위해 _muted_ 플래그를 사용합니다:
class MyFoo extends HTMLElement {
_bar = 0
get bar() {
return this._bar
}
set bar( next ) {
this._bar = next
this._muted_ = true; try {
this.setAttribute( 'bar', JSON.stringify( next ) )
} finally { this._muted_ = false }
}
_muted_ = false
static observedAttributes = [ 'bar' ]
attributeChangedCallback( name, prev, next ) {
if( this._muted_ ) return
this[ '_' + name ] = JSON.parse( next )
}
}
이 방식은 속성 문자열의 이스케이프 문제를 야기하며, 날짜, 객체 등 다양한 타입을 다루기 위해 추가적인 추상화 계층이 필요합니다.
성능: 마이크로초가 프레임을 죽인다
웹 컴포넌트는 런타임 프록시를 통해 정규 JS 객체보다 수십 배에서 수백 배 느립니다. 생성 시간 비교:
- 정규 JS 객체: ~0.01 μs
- 웹 컴포넌트: ~1–3 μs
천 개의 컴포넌트 생성에 1–3ms가 소요되며, 이는 16ms 프레임 예산의 상당 부분을 차지합니다. 속성 변경도 마이크로초 단위의 오버헤드를 동반합니다:
- 간단한 JS 할당: 나노초
- 웹 컴포넌트 세터: 마이크로초
근본 원인은 JS와 브라우저의 C++ 런타임 사이의 지속적인 데이터 교환으로, JIT 인라인을 방해합니다. 메모리 사용량은 단순 웹 컴포넌트당 124바이트, JS 객체는 16바이트입니다.
전역 등록과 이름 충돌
컴포넌트 등록은 전역적이며 탭이 닫힐 때까지 취소할 수 없습니다:
customElements.define( 'ya-button', YandexButton )
// ...
customElements.define( 'ya-button', YahooButton ) // 💥 오류: 이름이 이미 사용됨
이름 충돌은 모듈성을 해칩니다. Shadow DOM의 CustomElementRegistry는 일부 해결책이지만, 폴리필은 제공되지 않으며, 레지스트리 병합에는 네임스페이스 관리가 필요합니다.
대안:
- DNS처럼 전역 분할 레지스트리
- VerLess — 호환성 깨짐 없이 업데이트 가능
라이프사이클 훅과 Shadow DOM
connectedCallback/disconnectedCallback은 컴포넌트의 위치 이동 시 항상 실행되며, 같은 위치로 이동하는 경우에도 트리거됩니다. 이로 인해 하위 트리 전체에 걸쳐 불필요한 비동기 작업이 발생합니다.
Shadow DOM은 격리성을 제공하지만 스타일링을 복잡하게 만듭니다: adoptedStyleSheets는 외부에서 동적 임포트를 통해 스타일을 가져와야 합니다. 요소 이름은 반드시 kebab-case 형식이어야 하며, 이는 표준 DOM 요소와 충돌할 수 있습니다.
핵심은 무엇인가?
- 성능: 웹 컴포넌트는 호스트 프록시로 인해 JS 객체보다 100~1000배 느림
- 보일러플레이트: 문자열이 아닌 속성은 직렬화/역직렬화 필수
- 등록: 전역적이고 수정 불가능, 이름 충돌 위험 있음
- 라이프사이클: 이동 시 훅이 트리거되어 계단식 효과 발생
- Shadow DOM: 스타일 커스터마이징과 폴리필이 어렵다
웹 컴포넌트는 TypeScript 정적 타이핑이나 내장 반응성 기능을 지원하지 않으며, 실용적인 앱에서는 프레임워크가 필수입니다.
— Editorial Team
아직 댓글이 없습니다.