티스토리 뷰
글이 전하려고 하는 정보
커스텀 엘리먼트와 쉐도우 돔을 이용하여 메서드 형식으로 사용하는 방법, CSS 속성을 주는 방법, 타입스크립트에서 타입 지정하는 방법 등등 사용하며 배운 지식을 전하려고 합니다.
Custom-element를 사용하는 방법
https://developer.mozilla.org/ko/docs/Web/Web_Components/Using_custom_elements
Shadow Dom을 사용하는 방법
https://htmlwithsuperpowers.netlify.app/get-started/shadow-dom.html
https://ui.toast.com/posts/ko_20170721
읽기 전
모든 정보는 웹팩 환경에서 이루어져 있습니다. 웹팩을 사용하지 않았을 경우 다르게 작동할 수 있습니다.
custom-elements 선언하는 방법
customElements.define( 선언할 html 태그 이름, 해당하는 Javascript 코드)
주의할 점으로는 선언할 html 태그 이름은 무조건 'dash(-) ' 가 들어가야 합니다. html의 이름 규칙을 따라야 하기 때문입니다.
import Header from './Header';
import SearchInput from './SearchInput';
import MoviesContainer from './MoviesContainer';
import MovieListItem from './MovieListItem';
import Modal from './Modal';
import MovieScore from './MovieScore';
import Image from './Image';
import Button from './Button';
import Skeleton from './Skeleton';
customElements.define('movie-header', Header);
customElements.define('search-input', SearchInput);
customElements.define('movies-container', MoviesContainer);
customElements.define('movie-item', MovieListItem);
customElements.define('movie-modal', Modal);
customElements.define('movie-score', MovieScore);
customElements.define('movie-image', Image);
customElements.define('common-button', Button);
customElements.define('skeleton-item', Skeleton);
shadowDom 없는 custom-elements CSS 적용 방법
커스텀 엘리먼트 파일에서 CSS 파일을 불러오면 사용이 가능합니다.
웹팩의 css-loader가 없다면 안될 수 있습니다
import './Button.css';
class Button extends HTMLElement {
connectedCallback(): void {
this.render();
}
render(): void {
const text = this.getAttribute('text');
const color = this.getAttribute('color');
this.innerHTML = /*html*/ `
<button class="btn ${color}">${text}</button>
`;
}
}
export default Button;
Button.css
.btn {
width: 100%;
height: 100%;
border: 0;
border-radius: 8px;
color: var(--white);
}
.primary {
background-color: var(--red);
}
.darken {
background-color: var(--black);
}
shadowDom과 custom-elements를 같이 사용했을 때 CSS 적용 방법
쉐도우 돔에서는 CSS를 적용시킬 때 내부에서 적용을 시켜줘야 잘 작동이 되었습니다. 먼저 HTML 코드를 렌더 시키고 그 이후에 CSS 파일을 쉐도우 돔 내부에 적용시키는 방법을 사용했습니다.
Button.ts
class Button extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.render();
this.setComponentStyle();
}
static get observedAttributes() {
return ['name', 'color', 'id'];
}
render() {
const name = this.getAttribute('name');
const id = this.getAttribute('id');
const color = this.getAttribute('color');
this.shadowRoot!.innerHTML = `
<button type="button" id="${id}" alt="${id}" class="button--${color} text-caption">${name}</button>
`;
}
setComponentStyle() {
const componentStyle = document.createElement('style');
componentStyle.textContent = `
.text-caption {
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
button {
width: 171px;
height: 44px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s;
}
.button--white {
border: 1px solid var(--grey-300);
background: transparent;
color: var(--grey-300);
}
.button--white:hover{
background: var(--grey-400);
color: var(--grey-100);
}
.button--orange {
background: var(--primary-color);
color: var(--grey-100);
}
.button--orange:hover{
background: var(--yellow-color);
color: var(--grey-400);
}
@media (max-width: 500px) {
button {
width: 150px;
height: 44px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
}`;
this.shadowRoot!.append(componentStyle);
}
}
export default Button;
custom-elements 메서드 사용하는 방법
querySelector를 이용하여 원하는 커스텀엘리먼트를 선택한다. 그리고 메서드를 실행한다.
export const $ = <E extends Element>(selector: string): E | null => document.querySelector(selector);
index.ts
movies-container를 dom에서 찾은 후 reset 메서드를 실행시키는 코드입니다.
const setDisconnectedError = (): void => {
const movieContainer = $('movies-container') as HTMLMovieContainerElement;
const modal = $('#modal') as HTMLDialogElement;
movieContainer.setSearchWord('OFFLINE_ERROR');
movieContainer.reset();
$('body')?.classList.remove(SCROLL_HIDDEN_CLASSNAME);
modal.close();
$('#more-button')?.classList.add('hide');
$('#more-button')?.classList.add('hide-button');
$('#skeleton-container')?.classList.add('skeleton-hide');
movieContainer.setErrorMessage('인터넷 연결이 끊겼습니다.');
};
MoviesContainer.ts
class MoviesContainer extends HTMLElement {
reset(): void {
const movieListWrapper = $('#movie-list-wrapper') as HTMLElement;
movieListWrapper.innerHTML = '';
if ($('#no-result-message')) {
$('#no-result-message')?.remove();
}
this.#movies.resetPageIndex();
}
}
export default MoviesContainer;
안 좋은 예
외부와 격리시켜서 캡슐화하기 위해 사용한 shadowDom을 무력화시키는 코드를 사용하기.
이런 코드를 사용하지 않고 외부에 노출되어 있는 <movie-header></movie-header>와 같은 커스텀 엘리먼트를 찾아 메서드를 실행시키는 것이 좋습니다.
const $$$ = (rootNode, selector) => {
const arr = [];
const traverser = (node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches(selector)) {
arr.push(node);
}
const children = node.children;
if (children.length) {
for (const child of children) {
traverser(child);
}
}
const shadowRoot = node.shadowRoot;
if (shadowRoot) {
const shadowChildren = shadowRoot.children;
for (const shadowChild of shadowChildren) {
traverser(shadowChild);
}
}
};
traverser($(rootNode));
return arr[0];
};
다음과 같이 직접 값을 조정하지 않고 add-restaurant-modal에서 reset 메서드를 만들어서 사용할 수 있을 것입니다.
$$$('add-restaurant-modal', '#categoryList').value = '';
$$$('add-restaurant-modal', '#nameInput').value = '';
$$$('add-restaurant-modal', '#distanceList').value = '';
$$$('add-restaurant-modal', '#descriptionInput').value = '';
$$$('add-restaurant-modal', '#linkInput').value = '';
shadow Dom, custom-elements 타입 지정하기
? 혹은! 붙이기
$('#skeleton-container')?.classList.add('skeleton-hide');
getSelectValue(): string {
const id = this.getAttribute('id');
const select = this.shadowRoot!.querySelector(
`#${id}`
) as HTMLSelectElement;
return select.value;
}
custom-elements interface 만들기
타입을 내보낼 때 클래스를 기반으로 타입을 내보내면 타입뿐만 아니라 메서드 코드, 클래스 그 자체를 내보낸다고 봐서 따로 인터페이스를 만들어주었습니다.
네이밍은 HTML Element와 관련 있는 코드라고 알려주기 위해 HTML ~ Element로 짓는 컨벤션을 만들어보았습니다.
export interface HTMLMovieContainerElement extends HTMLElement {
reset: () => void;
setSearchWord: (searchWord: string) => void;
setErrorMessage: (errorMessage: string) => void;
}
export interface HTMLMovieListItemElement extends HTMLElement {
updateReviewedElement: () => void;
}
this를 사용하여 여러 개의 컴포넌트 관리하기
setLoadingEvent 메서드에서 this.querySelector을 사용하여 커스텀 엘리먼트 내부에서 img 태그를 찾아서 기능을 만들었습니다. this를 사용하지 않았더라면 id를 따로 부여해서 id를 찾은 후 동작시켰어야 했을 텐데 this를 이용하면 쉽게 만들 수 있습니다.
import './Image.css';
import DEFAULT_IMAGE from '../image/default-movie-image.png';
class Image extends HTMLElement {
connectedCallback(): void {
this.render();
this.setLoadingEvent();
}
render(): void {
const EMPTY = 'null';
const imgUrl = this.getAttribute('imgUrl');
const title = this.getAttribute('title');
const width = this.getAttribute('width');
const URL = imgUrl !== EMPTY ? `https://image.tmdb.org/t/p/w${width}${imgUrl}` : DEFAULT_IMAGE;
this.innerHTML = /*html*/ `
<img
class="movie-image skeleton-image"
src="${URL}"
loading="lazy"
alt="${title}"
title="${title}"
/>`;
}
setLoadingEvent(): void {
this.querySelector('img')?.addEventListener('load', () => {
this.querySelector('img')?.classList.remove('skeleton-image');
});
}
}
export default Image;
custom-elements에서 attribute 사용하기
observedAttributes을 사용하지 않아도 attribute를 사용할 수 있습니다.
static get observedAttributes() {
return ['name', 'color', 'id'];
}
<movie-image imgUrl="${imgUrl}" title="${title}" width="200" class="movie-list-image-wrapper"></movie-image>
Shadow Dom에서 Jest, Cypress 테스트 편하게 하기 위한 방법
Shadow Dom안에 Shadow Dom을 넣어서 Form, input 이벤트가 안될 때
검색 키워드 : Form Associated
사용하며 느낀 점
리액트가 아닌 자바스크립트로 컴포넌트를 처음 만들었을 때 custom-elements를 사용했습니다. 원하는 기능을 동작하려고 할 때 querySelector로 해당하는 커스텀 엘리먼트를 찾아서 메서드를 사용할 수 있다는 점이 가장 큰 장점으로 다가왔습니다.
또한 HTMLElement의 메서드를 사용할 수 있어서 유용했습니다.
shadow Dom을 이용했을 때엔 HTML, CSS, Javascript 모두 캡슐화되어 있는 하나의 돔처럼 이용할 수 있는 것이 좋았고, 진정한 컴포넌트처럼 생각했었습니다. 그렇지만 Shadow Dom을 사용하는 순간부터 골치 아픈 일들이 많이 생깁니다. 예를 들어 CSS를 Javascript에서 작성해서 적용시켜야 하고, 테스트를 해야 한다면 따로 라이브러리를 또 설치해서 사용해야 하기에 외부 코드에 의존성이 높아지게 됩니다. 그리고 Form을 이용해서 submit 이벤트를 이용해야 할 때가 있었는 데 form으로 이루어진 Shadow Dom 안에 다른 input Shadow Dom이 있을 경우 required 혹은 form 이벤트가 작동하지 않았었습니다. 이를 해결하기 위해 Form Associated를 이용해야 해결할 수 있었습니다. 저는 이 방법 말고 일일이 input값, form에 관한 것을 javascript 를 통해 관리하는 방법으로 해결했었습니다.
결론
custom-elements는 유용했습니다. Shadow Dom은 커스텀 엘리먼트가 너무 많아서 CSS에서 사용하는 클래스를 중복되지 않게 관리해야 한다 했을 때 유용할 것 같고, 그 외에 장점은 못 느꼈습니다. 따라서 custom-elements만 사용하는 것을 추천합니다.
하지만 저는 Shadow Dom과 custom-elements를 같이 사용할 때 더욱 빛을 발하는 slot, host와 같은 기능은 사용하지 않았습니다. 제가 모르는 다른 장점이 있을 수 있습니다.
'자바스크립트' 카테고리의 다른 글
외부에 의존하는 코드 줄이기 + API를 그대로 사용하면 안 좋은 이유는? (2) | 2023.04.07 |
---|---|
function 함수와 화살표 함수를 사용하는 기준 + ES6 클래스 메서드 기준 (0) | 2023.04.06 |
타입스크립트 Type 과 Interface의 공통점과 차이점 (0) | 2023.03.01 |
Node.js와 ws로 직접 WebSocket 서버 만들기 (0) | 2023.01.15 |
HTTP vs WebSockets 의 특징 및 차이점 (0) | 2023.01.14 |
- Total
- Today
- Yesterday
- 프론트앤드
- WSL2
- error
- React
- 위코드
- 우아한테크코스
- 노마드코더
- 초보
- import/order
- 프리온보딩
- createPortal
- electron
- 노개북
- javascript
- TopLayer
- NextApiRequest
- 윤성우 열혈C프로그래밍
- CLASS
- C언어
- nextjs
- env
- 아차산
- 원티드
- jest
- Storybook
- 북클럽
- NextRequest
- nodejs
- 스토리 북
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |