본문 바로가기

카테고리 없음

프레임워크 없는 프론트엔드 개발 | 웹 컴포넌트

오늘날의 최신 브라우저에서 웹 컴포넌트(Web Component)라고 하는 네이티브 API를 사용해 웹 애플리케이션에서 컴포넌트를 작성할 수 있다.

API

웹 컴포넌트는 세가지 중요 기술로 구성된다. 이 기술은 개발자가 재사용할 수 있는 UI 컴포넌트를 작성하고 게시할 수 있게 해준다.

- HTML 템플릿: <template> 태그는 콘텐츠가 렌더링되지는 않지만 자바스크립트 코드에서 동적인 콘텐츠를 생성하는데 '스탬프'로 사용되도록 하려는 경우에 유용하다.

- 사용자 정의 요소: 이 API를 통해 개발자는 완전한 기능을 갖춘 자신만의 DOM 요소를 작성할 수 있다. 

- 새도우(Shadow) DOM: 이 기술은 웹 컴포넌트가 컴포넌트 외부의 DOM에 영향을 받지 않아야 하는 경우에 유용하다. 다른 사람들과 공유할 수 있도록 컴포넌트 라이브러리나 위젯을 작성하려는 경우 매우 유용하다. 

사용자 정의 요소

사용자 정의 요소는 웹 컴포넌트의 핵심 요소이다. 간단히 말해 다음과 같이 사용자 정의 HTML 태그를 작성할 수 있다.(<app-calendar />)

우연히 app-calendar라는 이름을 사용한 것이 아니다. 사용자 정의 요소 API를 사용해 사용자 정의 태그를 작성할 때는 대시로 구분된 두 단어 이상의 태그를 사용해야 한다. 한 단어 태그는 W3C에서만 단독으로 사용할 수 있다.

 

사용자 정의 요소는 HTML 요소를 확장하는 자바스크립트 클래스일 뿐이다.

 

export default class HelloWorld extends HTMLElement {
  connectedCallback() {
    window.requestAnimationFrame(() => {
      this.innerHTML = '<div>Hello World!</div>`
    })
  }
}

 

connectedCallback은 사용자 정의 요소의 라이프 사이클 메서드 중 하나이다. 이 메서드는 컴포넌트가 DOM에 연결될 때 호출된다. 리액트의 componentDidMount 메서드와 매우 유사하다. 예제처럼 컴포넌트의 콘텐츠를 렌더링하거나 타이머를 시작하거나 또는 네트워크에서 데이터를 가져올 때 좋은 장소이다. 마찬가지로 컴포넌트가 DOM에서 삭제될 때 disconnectedC

input.value = "Frameworkless"

allback이 호출되는데, 정리작업에서 유용한 메서드이다. 새로 생성한 이 컴포넌트를 사용하려면 브라우저 컴포넌트 레지스트리에 추가해야 한다.

import HelloWorld from './components/HelloWorld.js'

window.customElements.define('hello-world', HelloWorld)

 

브라우저 컴포넌트 레지스트리에 컴포넌트ㄹ르 추가하는 것은 태그 이름을 사용자 정의 요소 클래스에 연결하는 것을 의미한다. 그런 다음에 생성한 사용자 정의 태그(<hello-world/>)를 컴포넌트로 사용할 수 있다.

 

웹 컴포넌트의 가장 중요한 기능은 개발자가 어떤 프레임워크와도 호환되는 새로운 컴포넌트를 만들 수 있다는 것이다. 여기에는 리액트나 앵귤러뿐만 아니라 자바스크립트 페이지나 기타 어떤 도구로 빌드된 레거시 애플리케이션을 포함한 모든 웹 애플리케이션이 해당된다. 그러나 이 목적을 달성하려면 컴포넌트에 다른 표준 HTML 요소와 동일한 공용 API가 있어야 한다. 따라서 사용자 정의 요소에 속성을 추가하려면 동일한 방식으로 이 속성을 관리할 수 있어야 한다. <input>과 같은 표준 요소의 세 가지 방법으로 속성을 설정할 수 있다. 가장 직관적인 방법은 속성을 HTML 마크업에 직접 추가하는 것이다.

<input type="text value="Frameworkless">

 

자바스크립트에서는 세터(setter)를 사용해 value 속성을 조작할 수 있습니다.

input.value = 'Frameworkless'

 

또는 setAttribute 메서드를 사용할 수도 있다.

input.setAttribute('value', 'Frameworkless')

 

이 세가지 방법은 모두 동일한 결과, 즉 input 요소의 value 속성을 변경한다. 또한 동기화된다. 마크업을 통해 값을 입력하면 getter나 getAttribute 메서드로 동일한 값을 읽을 수 있다. 세터나 setAttribute 메서드로 값을 변경하면 마크업이 새 속성과 동기화된다. 사용자 정의 속성을 생성하려면 HTML 요소의 이 특성을 잘 기억하고 있어야 한다. 아래 코드는 HelloWorld 컴포넌트에 color 속성을 추가하는데, 레이블 콘텐츠의 색상을 변경하는 데 사용한다.

const DEFAULT_COLOR = 'black'

export default class HelloWorld extends HTMLElement {
  get color() {
    return this.getAttribute('color') || DEFAULT_COLOR
  }
  
  set color(value) {
    return this.setAttribute('color', value)
  }
  ... 생략
}

 

보다시피 색상 게터/세터는 getAttribute/setAttribute에 대한 래퍼일 뿐이다. 따라서 속성을 설정하는 세 가지 방법이 자동으로 동기화된다. 

가상 DOM 통합

가상 DOM 알고맂므은 모든 사용자 정의 컴포넌트에 완벽하게 플러그인 될 수 있다.

import applyDiff from './applyDiff.js'

const DEFAULT_COLOR = 'black'

const createDOMElement = color => {
  const div = document.createElement('div')
  div.textContent = 'Hello World'
  div.style.color =color
  return div
}

export default class HelloWorld extends HTMLElement {
  static get observedAttributes() {
    return ['color']
  }
  
  get color() {
    return this.getAttribute('color') || DEFAULT_COLOR
  }
  
  set color(value) {
    this.setAttribute('color', value)
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (!this.hasChildNodes()) {
  	  return
    }
    
    applyDiff(this, thisfirstElementChild, createDomElement(newValue)
  }
  
  connectedCallback() {
    window.requestAnimationFrame(() => {
      this.appendChild(createDomElement(this.color))
    })
  }
}

 

가상 DOM을 이 시나리오에 사용하는 것은 조금 과하긴 하지만 컴포넌트가 많은 속성을 가진 경우라면 매우 유용하다. 

 

사용자 정의 이벤트

이번 예제에서는 GithubAvatar라는 컴포넌트를 분석해본다. 이 컴포넌트의 목적은 깃허브 사용자의 아바타를 보여주는 것이다. 이 컴포넌트를 사용하려면 user 속성을 설정해야 한다.

 

<github-avatar user="hyunjinee"></github-avatar>

 

컴포넌트가 DOM에 연결되면 'loading'이라는 자리 표시자(placeholder)가 표시된다. 그런 다음 깃허브 REST API를 사용해 아바타 이미지 URL을 가져온다. 요청이 성공하면 아바타가 표시되고 그렇지 않으면 오류를 보여준다.

const ERROR_IMAGE = 'https://~~~'
const LOADING_IMAGE = 'https://~~'

const getGitHubAvatarUrl = async (user) => {
  if (!user) {
  	return
  }
  
  const url = `https://api.github.com/users/${user}`
  
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(response.statusText)
  }
  
  const data = await response.json()
  return data.avatar_url
}

export default class GitHubAvatar extends HTMLElement {
  constructor() {
    super()
    this.url = LOADING_IMAGE
  }
  
  get user() {
    return this.getAttribute('user')
  }
  
  set user(value) {
    this.setAttribute('user', value)
  }
  
  render() {
    window.requestAnimationFrame(() => {
      this.innerHTML = ''
      const img = document.createElement('img')
      img.src =this.url
      this.append(img)
    })
  }
  
  async loadNewAvatar () {
    const {user } = this
    if (!user) {
      return
    }
    try {
      this.url = await getGitHubAvatarUrl(user)
    } catch(e) {
      this.url = ERROR_IMAGE
    }
    
    this.render()
  }
  
  connectedCallback() {
    this.render()
    this.loadNewAvatar()
  }
}

 

컴포넌트 외부의 HTTP 요청 결과에 반응하려면 어떻게 해야할까? 사용자 정의 요소는 가능한 한 표준 DOM 요소와 동이랗게 동작해야 한다는 사실을 명심하자. 앞에서는 다른 요소와 마찬가지로 속성을 사용해 정보를 컴포넌트에 전달했다. 컴포넌트에서 정보를 얻는 동일한 방법을 따라 DOM 이벤트를 사용한다.

 

웹 컴포넌트와 렌더링 함수

웹 컴포넌트를 작성하려면 HTML 요소를 확장해야 하므로 클래스 작업이 필요합니다. 함수형 프로그래밍을 선호한다면 이런 방식으로 작업하는 것이 불편할 수도 있습니다. 반면에 자바나 C#같은 클래스를 기반으로 하는 언어에 익숙하다면 함수보다 웹 컴포넌트에 더 자신감이 생길 수 있다. 둘 중에 승자는 없다. 여러분이 어떤것을 좋아하는지에 달려있다. 렌더링 함수를 가져와 웹 컴포넌트로 래핑하면 디자인을 시나리오에 맞출 수 있다.

 

웹 컴포넌트는 휴대성(portable)이 좋아야 한다. 다른 DOM 요소와 동일하게 동작한다는 사실은 다른 애플리케이션 간에 동이한 컴포넌트를 사용해야 하는 경우 핵심 기능이 된다. 컴포넌트 클래스는 대부분의 프레임워크에서 DOM 요소를 작성하는 표준 방법이다. 대규모 팀이나 빠르게 성장하는 팀이라면 명심해야 할 아주 중요한 기능이다. 사람들에게 익숙한 코드가 더 읽기 쉬운 코드가 된다. 

 

웹 컴포넌트의 출현으로 인한 흥미로운 부작용은 사라지는 프레임워크(frameworks)라고 불리는 여러 도구의 탄생이다. 기본 아이디어는 리액트 같은 다른 UI 프레임워크와 마찬가지로 동일하게 코드를 작성하는 것이다. 기본 아이디어는 리액트 같은 다른 UI 프레임워크와 마찬가지로 동일하게 코드를 작성하는 것이다. 제품 번들을 제작할 때 출력은 표준 웹 컴포넌트가 된다. 즉 컴파일 타임에 프레임워크는 사라진다.