Codeit Weekly Mission [Week 4] 웹 컴포넌트화

0

Weekly Mission

목록 보기
2/10
post-thumbnail

기존에 만들었던 내브바, 푸터를 모두 웹 컴포넌트 기술을 사용하여 커스텀 엘리먼드로 만드는 작업을 진행하였다.

Web component화

커스텀 navbar 만들기

먼저 커스텀 엘리먼트들을 만들어 사용하기 위해 폴더 구조를 다음과 같이 바꿔준다.

  기존의 네브바는 다음과 같이 html태그들을 중첩하여 만들고, 클래스를 부여하고 css파일에서 스타일링하여 만들었다.
<!-- index.html -->
<div id="header-wrapper">
  <nav>
    <a id="logo" href="/">
      <img src="images/logo.svg" />
    </a>
    <a id="login-btn" href="signin.html"> 로그인 </a>
  </nav>
</div>	
/* index.css */
#header-wrapper {
  width: 100%;
  max-width: 120rem;
  height: 5.813rem;
  padding: 1.25rem 12.5rem;
  margin: 0 auto;
  background-color: #f0f6ff;
}

nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

nav > #login-btn {
  width: 8rem;
  height: 3.313rem;
  border-radius: 0.5rem;
  text-decoration: none;
  padding: 1rem 2.531rem;
  font-size: 1.125rem;
  font-weight: 600;
  color: #f5f5f5;
  background-image: linear-gradient(90.99deg, #6d6afe 0.12%, #6ae3fe 101.84%);
}
....

이제는 이 네브바를 웹 컴포넌트 기술을 이용하여 커스텀 태그로 다음과 같이 만들어야 한다.

<custom-gnb></custom-gnb>

그러기 위해 우선 class문법을 이용하여 나만의 컴포넌트를 만들기 위한 작업을 해 주어야 한다. 자세한 방법은 여기에 정리해두었다.
다음과 같이 custom-elements라는 디렉토리를 만들고 안에 gnb.js파일에서 Gnb라는 클래스를 만들고, customElements.define메서드를 이용하여 우리의 커스텀 gnb 태그를 작명해주었다.

// src/custom-elements/gnb.js
export class Gnb extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.render();
    this._isLoggedIn = false;
  }

  connectedCallback() {
    console.log("connected");
  }

  static get observedAttributes() {
    return ["isLoggedIn"];
  }

  get styles() {
    return `
    * {
      box-sizing: border-box;
    }
  
    #header-wrapper {
      width: 100%;
      max-width: 120rem;
      height: 5.813rem;
      padding: 1.25rem 12.5rem;
      margin: 0 auto;
      background-color: #f0f6ff;
    }
    
    nav {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    #login-btn {
      width: 8rem;
      height: 3.313rem;
      border-radius: 0.5rem;
      text-decoration: none;
      padding: 1rem 2.531rem;
      font-size: 1.125rem;
      font-weight: 600;
      color: #f5f5f5;
      background-image: linear-gradient(90.99deg, #6d6afe 0.12%, #6ae3fe 101.84%);
    }
    
    #logo img:hover {
      cursor: pointer;
    }
    
    @media only screen and (max-width: 1200px) {
      #header-wrapper {
        display: flex;
        justify-content: center;
        padding: 1.5rem 0;
      }
    
      nav {
        width: 49.028rem;
      }
    }
    
    @media only screen and (max-width: 868px) {
      #header-wrapper {
        padding: 1.5rem 2rem;
      }
    }
    
    @media only screen and (max-width: 767px) {
      #header-wrapper {
        width: 100%;
        height: 3.938rem;
        padding: 0.813rem 2rem;
        display: flex;
      }
    
      #logo img {
        width: 4.849rem;
        height: 0.875rem;
      }
    
      #login-btn {
        width: 5rem;
        height: 2.313rem;
        padding: 0.625rem 1.344rem;
        font-weight: 600;
        font-size: 0.875rem;
        line-height: 1.063rem;
      }
    }`;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    switch (name) {
      case "isLoggedIn":
        this._isLoggedIn = newValue;
        break;
      default:
        break;
    }
    this.render();
  }

  render() {
    const style = document.createElement("style");
    style.textContent = this.styles;
    this.shadowRoot.appendChild(style);

    const template = document.createElement("template");
    template.innerHTML = this.template;
    this.shadowRoot.appendChild(template.content.cloneNode(true));

  }

  get template() {
    return `
      <div id="header-wrapper">
        <nav>
          <a id="logo" href="/">
            <img src="images/logo.svg" />
          </a>
          <a id="login-btn" href="signin.html"> 로그인 </a>
        </nav>
      </div>
    `;
  }
}

customElements.define("custom-gnb", Gnb);

이 custom-gnb를 실제로 사용하는 곳에서 위 파일을 불러와야 한다. 귀찮으니 app.js라는 파일을 만들고 그 안에서 모든 커스텀 태그들을 import해온 뒤, 커스텀 태그를 사용하는 html파일에서는 app.js만 가져오는 방식을 일단 택하였다. 커스텀 엘리먼트들이 많아지면 비효율적일 것 같긴 한데, 우선은 app.js를 만들어 Gnb를 import하였다.

// src/app.js
import { Gnb } from "./custom-elements/gnb/gnb.js";

그리고 index.html에서 다음과 같이 app.js를 모듈로서 불러와준다.
type="module"이 없으면 안되니 주의하자.

<!-- index.html -->
<script src="/src/app.js" type="module"></script>

이렇게 만들고 나니 우선은 쓸 수 있게 되었다. 아래처럼 잘 동작하는 것을 볼 수 있다.

gnb에 데이터 전달하기

현재 최종적으로 원하는 것은 단순히 컴포넌트화만 하는 것이 아니다. 이제 우리가 만든 커스텀 엘리먼트에 데이터를 전달하고, 데이터에 따라 다른 스타일을 보여줄 장치가 필요하다.

커스텀 엘리먼트에 데이터를 전달하는 세 가지 방법

  • 애트리뷰트
    • 애트리뷰트를 이용해서 일반 html처럼 전달하는 것이다.
    • 단점은 스트링 값만 전달할 수 있고, 클래스 안에서 hasAttribute나 getAttribute, setAttribute로 다룰 수 있다.
  • 프로퍼티
    • 프로퍼티로 데이터를 전달하는 방법도 있다. 이 방법으로는 어떠한 타입도 넘겨줄 수 있어 편리하다. 클래스의 프로퍼티를 다루듯이 다루면 된다.
  • 슬롯
    • 슬롯을 통해 자식 html태그들을 넘겨줄 수도 있다. 슬롯이란 우리의 커스텀 엘리먼트에 정의해둔 일종의 html태그를 위한 placeholder역할을 하는 태그이다.

우리가 원하는 것은 다음처럼 로그인 여부에 따라 달라지는 네브바이다.

일단은 애트리뷰트와 프로퍼티를 함께 활용하는 방법을 택해보자. 웹 컴포너늩 클래스에서는 attributeChangedCallback이라는 함수가 내장되어 있다. 내가 변화를 감지하고 싶은 애트리뷰트들이 변화하면 자동으로 호출되는 콜백함수이다. 우리가 원하는 gnb는 로그인 여부에 따라서 UI가 바뀌므로, 어딘가에서 로그인 여부에 대한 데이터를 우리 커스텀 엘리먼트로 보내주면 된다. 일단 그 데이터를 prop으로 전달할 것이며, prop이 외부에서 세팅되었을 때 호출되는 setter함수에서 attribute는 set해준다. 그럼 자동으로 attributeChangedCallback함수가 호출되고, 그 함수에서 render메서드를 재호출함으로써 보여줄 UI를 재렌더링하는 방식으로 데이터를 처리해주자.

prop설정, setter getter정의

일단 다음과 같이 prop이라는 클래스 프로퍼티를 설정해주고, getter와 setter 메서드 또한 정의해준다. setter메서드에서는 prop이 값을 받을 때 setAttribute메서드를 이용하여 애트리뷰트를 설정한다. 애트리뷰트 이름은 직관적으로 isloggedin으로 해주었다.

export class Gnb extends HTMLElement {
  // 컴포넌트 정보를 담고 있는 프로퍼티
  #prop = { isLoggedIn: false, profileImgSrc: "", username: "" };
  ...
  
  get prop() {
    return this.#prop;
  }

  set prop(value) {
    this.#prop = value;
    this.setAttribute("isloggedin", `${value.isLoggedIn}`);
  }
  ...
}

변화를 감지할 애트리뷰트 등록, attributeChangedCallback 정의

이제 prop이 세팅될 때 애트리뷰트가 변화하니, 우리가 감지할 애트리뷰트를 observedAttributes함수에서 배열 형태로 리턴해준다. 이것이 일종의 등록과정이다. 그리고 attributeChangedCallback에서는 name, oldValue, newValue라는 세 가지 파라미터를 받는데, name에 해당하는 것이 애트리뷰트 이름이다. 따라서 어떤 애트리뷰트가 변화하였는지 switch문으로 구분한 뒤 원하는 작업을 해준다.

export class Gnb extends HTMLElement {
  ...

  /**
   * 변화를 감지할 attribute 이름이 담긴 배열을 리턴
   */
  static get observedAttributes() {
    return ["isloggedin"];
  }

  /**
   * 변화를 감지하는 중인 attribute들의 변화가 있을 시 호출되는 콜백함수
   */
  attributeChangedCallback(name, oldValue, newValue) {
    switch (name) {
      case "isloggedin":
        this._isLoggedIn = newValue;
        break;
      default:
        break;
    }
    this.render();
  }
  ...
}

attributeChangedCallback에 this.render()를 마지막에 해 주어야 우리가 원하는대로 바뀐 애트리뷰트에 대한 작업 결과가 반영된 새로운 UI를 재렌더링 할 수 있다.

render 메서드 수정

이제 애트리뷰트가 변화하면 재렌더링이 일어난다. render() 메서드에서도 prop을 검사하여 로그인 되어있다면 showUserInfo라는 함수를 호출하게 한다. showUserInfo에서는 단순하게 gnb의 로그인 버튼을 제거하고, 그 자리에 로그인된 계정 정보가 담긴 html요소들을 UI적으로 만들어서 shadowRoot의 nav요소의 자식으로 append한다.

export class Gnb extends HTMLElement {
  ...
  showUserInfo() {
    this.shadowRoot.querySelector("#login-btn").remove();
    const myAccountDiv = document.createElement("div");
    myAccountDiv.classList.add("my-account");

    const profileDiv = document.createElement("div");
    profileDiv.classList.add("profile-container");
    myAccountDiv.prepend(profileDiv);

    const profileImg = document.createElement("img");
    profileImg.classList.add("profile-img");
    profileImg.setAttribute("src", this.#prop.profileImgSrc);
    profileDiv.append(profileImg);

    const usernameDiv = document.createElement("div");
    usernameDiv.classList.add("username-container");
    usernameDiv.textContent = this.#prop.username;
    myAccountDiv.append(usernameDiv);

    this.shadowRoot.querySelector("nav").append(myAccountDiv);
  }

  /**
   * shadow root 노드에 자식 노드들을 추가하여 화면에 렌더링하는 함수
   */
  render() {
    this.shadowRoot.innerHTML = "";

    const style = document.createElement("style");
    style.innerHTML = this.styles;
    this.shadowRoot.appendChild(style);

    const template = document.createElement("template");
    template.innerHTML = this.template;
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    /* prop을 읽어 로그인된 상태라면 showUserInfo 호출 */
    if (this.prop.isLoggedIn) {
      this.showUserInfo();
    }
  }
  ...
}

이제 우리가 만든 커스텀 gnb는 받는 prop에 따라 다른 UI를 보여줄 수 있는 준비를 마쳤다. 역할에 맞게 웹 컴포넌트화를 하였다고 볼 수 있다.

데이터를 외부에서 gnb로 전달하기

이제 남은 작업은 gnb태그에 실제로 데이터를 전달하여 로그인시켜보는 것이다. 데이터를 다음과 같이 수동으로 전달하여 테스트해본다.

export class Gnb extends HTMLElement { ... }

customElements.define("custom-gnb", Gnb);

{
  const gnb = document.querySelector("custom-gnb");
  let isLoggedIn = true; // 로그인 구현 후 업데이트 예정

  gnb.prop = { // 여기서 Gnb의 prop setter 호출 -> 애트리뷰트 변화 -> attributeChangedCallback호출 -> 조건에 맞는 재렌더링
    isLoggedIn,
    profileImgSrc: isLoggedIn ? "/images/profile_img.png" : "",
    username: isLoggedIn ? "Codeit@codeit.com" : "",
  };
}

그리고 화면을 보면 다음과 같이 잘 나오는 것을 확인할 수 있다.

추후에 로그인을 구현하고 나면 로그인이 되었다는 사실을 gnb엘리먼트에 어떻게 전달할 수 있을 지 고민을 해 봐야한다. html태그에 애트리뷰트에 전달할 수도 있고, 위와 같이 isLoggedIn이라는 변수를 활용할 수 있겠다. 일단은 true로 하드코딩하여 확인하였다.

0개의 댓글