psj2867
alpine + web component 사용 본문
1. 개요
주로 벡엔드를 하면서 react는 과하고 주로 vue를 사용하고 있었습니다.
그러나 기본적으로 각 페이지마다 vue를 실행하기에 아무리 대부분 간단한 개발이지만 썩 마음에 들지는 않았습니다.
이후 svelte도 나왔지만 svelte도 따로 프로젝트 만들고 컴파일 하는 등의 일이 간단한 서버에 비해 너무 과했습니다.
이후 alpine을 발견하고 드디어 마음에 드는 라이브러리를 발견하고 공부 중이었습니다.
alpine 자체가 jquery를 대신한다는 말도 있어서 고전적인 것을 좋아하는 사람으로 특히 마음에 들었습니다.(jquery 느리지만 간단하게 편했는데..)
심지어 realdom으로 화려한 페이지는 당연히 생각에 없는 상황에서 성능과 가벼운 무게도 장점이었습니다.
하나 아쉬운 점으로는 vue에 익숙해져서 component가 없는 것인데 사실 벡엔드 입장에서는 없는게 당연하고 오랜만에 꺼내보는 template 엔진을 사용할 수 있기는 합니다.
그리고 라이브러리 제작자도 서버를 위해서 만든 것이니 서버에서 처리하라고 나와있습니다.
맞는 말이지만 vue 익숙해진 저는 방법을 찾았고 web component를 찾았습니다.
검색하면 당연히 관련 코드나 라이브러리가 나오지만 역시 참고해서 구현해봅시다.
있는 코드가 별로 마음에 안들기도 하고요
길지 않으니 cdn보다 그냥 직접 넣는 것도 나쁘지 않습니다.
2. 구현
https://github.com/cie/alpine-webcomponent/
https://github.com/markmead/alpinejs-component
내용은 위의 코드를 참고하였습니다.
다만 두 코드 모두 아쉬운 점이 있어 적절히 수정하여 쓰길 권장드립니다.
https://github.com/psj2867/alpinejs-webcomponent
간단하게 수정하여 배포해두었습니다.
webcomponent를 사용할 것이니 당연히 우선 등록을 해줬습니다.
간단하게 shadowDom 만들고 x-c로 등롭합니다.
--js
class ComponentWrapper extends HTMLElement {
async connectedCallback() {
this.shadowDom = this.attachShadow({ mode: 'open' });
this.shadowDom.innerHTML = '<span x-text="item">e</span>';
Alpine.initTree(this.shadowDom);
}
}
customElements.define('x-c', ComponentWrapper);
--html
<template x-if="true" x-data="{ item: 1 }">
<x-c></x-c>
</template>
이 코드를 실행 시켜보면 성공적으로 실행이 됩니다.
그러나
--html
<div x-data="{ item: 1 }">
<x-c></x-c>
</div>
template를 div를 변경하면 Alpine을 못 찾는 오류가 발생합니다.
template 밑의 노드일 경우 실제 화면에 구현되지 않고 넘어가지만 div 밑의 노드일 경우 defer로 불러온 Alpine이 실행되기 전에 x-c를 화면에 구현하려하고 connectedCallback이 실행됩니다.
그러면 alpine이 등록된 후에 initTree를 호출하기 위해 다음 코드를 작성합니다.
document.addEventListener('alpine:init', () => {Alpine.initTree(this.shadowDom)});
initTree 부분을 해당 코드로 바꾸면 이번에는 두가지의 새로운 오류 상황이 생깁니다.div를 사용, template을 사용
-div 사용
div 사용시에는 오류로 alpine이 item을 못 찾는다는 오류가 나옵니다.
이 오류는 alpine이 아직 데이터 초기화 작업을 하기 전에 x-c 먼저 초기화를 하여 데이터가 없는 상황입니다.
-template 사용
template 사용시에는 오류도 나오지 않습니다. 대신 적절한 값이 아닌 기본 값인 'e'만 출력됩니다.
이 상황은 오히려 template으로 나중에 초기화가 되어 alpine:init이 실행되지 않습니다.
이 두 상황 모두 initTree를 초기화가 다 끝난 후로 변경하면 해결됩니다.
document.addEventListener('alpine:initialized', () => {Alpine.initTree(this.shadowDom)});
실행이 잘 되는 것 같지만 동적으로 component가 추가되면 initialized도 실행된 후이기 때문에 initTree가 실행되지 않습니다.
자 그럼 상황은 두가지로 Alpine이 실행되기 전에는 initialized로 이후에는 즉각 실행되면 됩니다.
if(typeof Alpine === 'undefined'){
document.addEventListener('alpine:initialized', () => {Alpine.initTree(this.shadowDom)});
}else{
Alpine.initTree(this.shadowDom)
}
최종 코드는 아래와 같습니다.
저는 테스트용으로 만들었지만 위의 제가 참고한 github을 참고하여 수정하여 사용하시면 됩니다.
github코드들은 사용하기 쉽게 되어있습니다.
class ComponentWrapper extends HTMLElement {
async connectedCallback() {
this.shadowDom = this.attachShadow({ mode: 'open' });
this.shadowDom.innerHTML = '<span x-text="item">e</span>';
if(typeof Alpine === 'undefined')
document.addEventListener('alpine:initialized', () => {Alpine.initTree(this.shadowDom)});
else
Alpine.initTree(this.shadowDom)
}
}
customElements.define('x-c', ComponentWrapper);
3. 일반화
원래는 간단한 테스트 구현에서 마칠 예정이었지만 돌아다니는 코드가 아쉬워서 추가적으로 구현했습니다.
다른 것은 없고 위의 코드 기반으로 사용하기 편하게 만들었습니다.
다른 alpine 의 plugin같이 defer로 alpine앞에서 선언될 것을 가정하여 코드를 작성했습니다.
실제로 plugin 처럼 alpine에 뭔가를 등록하지는 않지만 일관적이게 만들었습니다.
plugin처럼 custom을 할까도 했는데 그러면 alpine에 관리가 되어야해서 그것보다 그냥 전역적으로 관리하는 것이 더 변수 범위도 혼동 안 되고 깔끔해보였습니다.
function component(Alpine) {
for (const tpl of document.querySelectorAll('template[x-webcomponent]')) {
customElements.define(
tpl.getAttribute('x-webcomponent'),
class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.appendChild(tpl.content.cloneNode(true))
Alpine.nextTick(() => Alpine.initTree(shadow));
}
}
);
}
}
document.addEventListener("alpine:initialized", () => window.Alpine.plugin(component));
https://github.com/psj2867/alpinejs-webcomponent
GitHub - psj2867/alpinejs-webcomponent
Contribute to psj2867/alpinejs-webcomponent development by creating an account on GitHub.
github.com
4. 후기
공부로 시작하다가 아쉬운 점이 보여 만들었는데 꽤 깔끔해서 마음에 들었습니다.
만드는 김에 배포까지 같이 해봤는데 결국에 저도 언제 쓸지는 모르겠네요
6. 추가
다시 찾아보던 중 따로 라이브러리 템플릿이 아닌 자체 방법이 더 적절해 보여 추가합니다.
<div x-data="dropdown" x-bind="bind"></div>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
show: false,
bind: {
['x-html']() { return `
<button @click="show = !show">Click me!</button>
<div x-show="show">Hello World</div>
`},
},
}));
})
'기타' 카테고리의 다른 글
webrtc 간단한 설명 (0) | 2023.11.16 |
---|---|
printf에 관하여 (0) | 2023.06.15 |
punycode encoding/decoding 알고리즘(bootstring) - 2 (0) | 2023.05.09 |
punycode encoding/decoding 알고리즘(bootstring) (0) | 2023.05.09 |
iptables 정리 (0) | 2023.04.18 |