Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organize Component Writing Styles #44

Closed
yolophg opened this issue Jun 13, 2024 · 24 comments · Fixed by #70
Closed

Organize Component Writing Styles #44

yolophg opened this issue Jun 13, 2024 · 24 comments · Fixed by #70
Assignees

Comments

@yolophg
Copy link
Contributor

yolophg commented Jun 13, 2024

Background

@yolophg yolophg self-assigned this Jun 13, 2024
@yolophg yolophg converted this from a draft issue Jun 13, 2024
This was referenced Jun 13, 2024
@DaleSeo
Copy link
Contributor

DaleSeo commented Jun 15, 2024

코드를 리뷰하다보니 웹 컴포넌트 클래스에 여러 개의 메서드가 있을 때, 어떤 메서드가 HTMLElement 인터페이스를 구현하는 것이고 어떤 메서드가 아닌지 좀 더 쉽게 구분이 되면 좋겠다는 생각이 들었는데, (예를 들어, _ 접두사를 붙이기?) 이 부분에 대해서도 같이 논의 해보면 좋을 것 같습니다.

@yolophg
Copy link
Contributor Author

yolophg commented Jun 19, 2024

Web Component 작성 스타일 간략 정리

1. HTML 구조 정의 방식

1) 정적으로 생성

(1) template 사용

특징:

  • template을 사용하여 HTML 구조 정의.

예시 코드:

// template 생성
const template = document.createElement("template"); 
template.innerHTML = `
  <link rel="stylesheet" href="./components/footer-link/footer-link.css">
  <ul class="footer-link">
    <li><a id="link" href="#"><slot></slot></a></li>
  </ul>
`;

(2) render 함수로 분리하고 그 안에서 정적으로 생성

특징:

  • HTML 정적으로 생성.

예시 코드:

// render 내에서 정적 생성
render() {
    this.shadowRoot.innerHTML = `
      <article class="review-item">
        <section class="review-content">
          <figure><img src="${this.img}" alt="Reviewer"></figure>
          <blockquote>${this.text}</blockquote>
        </section>
        <footer class="review-footer"><figcaption>${this.name}</figcaption></footer>
      </article>
    `;
  }

2) 동적으로 생성

특징:

  • 동적으로 HTML 생성 및 설정.

예시 코드:

// HTML 동적 생성
this.shadowRoot.innerHTML = `<link rel="stylesheet" href="components/link-button/link-button.css">`;
const element = document.createElement("a");
element.setAttribute("href", this.getAttribute("href"));
element.classList.add("button", this.getAttribute("size"), this.getAttribute("variant"));
if (this.hasAttribute("outline")) element.classList.add("outline");
element.innerHTML = `<slot></slot>`;
this.shadowRoot.appendChild(element);

2. 속성 핸들링 방식

1) 속성 변경 감지 및 처리

특징:

  • observedAttributes를 사용하여 관찰할 속성을 정의.
  • attributeChangedCallback 메서드를 통해 속성 변경을 감지하고 처리.

예시 코드:

// 관찰할 속성 정의
static get observedAttributes() {
    return ["href"];
}
// 속성 변경 처리
attributeChangedCallback(name, oldValue, newValue) {
    if (name === "href") {
      this.updateLink(newValue);
    }
  }

updateLink(href) {
   this.linkElement.setAttribute("href", href);
 }

2) 속성 검증 및 기본 속성 설정

특징:

  • 필수 속성 검증과 기본 속성 설정을 위한 메서드 사용.
  • 생성자에서 속성을 검증하고 기본값을 설정.

예시 코드:

constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.validateAttributes(); // 속성 검증
    this.setDefaultAttributes(); // 기본 속성 설정
    this.render(); 
}
// 필수 속성 검증
validateAttributes() {
    if (!this.hasAttribute("href")) throw new Error('The "href" attribute is required.');
}
// 기본 속성 설정
setDefaultAttributes() {
    if (!this.hasAttribute("size")) this.setAttribute("size", "small");
    if (!this.hasAttribute("variant")) this.setAttribute("variant", "ghost");
}

3) 속성 접근을 위한 getter 사용

특징:

  • 속성 값을 가져오기 위해 getter 메서드를 사용.

예시 코드:

// getter 메서드 사용
get text() { return this.getAttribute("text") || ""; }
get name() { return this.getAttribute("name") || ""; }
get img() { return this.getAttribute("img") || ""; }

3. 초기 렌더링 방식

1) 생성자 내에서 초기 렌더링

특징:

  • 컴포넌트가 생성될 때, 생성자 내에서 초기 렌더링을 수행.
  • 초기화 과정에서 속성 검증과 기본값 설정을 포함.

예시 코드:

constructor() {
    super();
    this.attachShadow({ mode: "open" }); 
    this.validateAttributes(); 
    this.setDefaultAttributes(); 
    this.render(); // 초기 렌더링
  }

2) connectedCallback 내에서 초기 렌더링

특징:

  • 컴포넌트가 DOM에 연결될 때, connectedCallback 메서드에서 초기 렌더링 수행.
  • 컴포넌트가 DOM에 완전히 연결된 후 속성 값을 접근하고 렌더링.

예시 코드:

connectedCallback() {
    this.attachShadow({ mode: "open" }); 
    this.render(); // 초기 렌더링
}

3) template을 통한 초기 렌더링

특징:

  • template을 사용하여 HTML 구조를 정의하고, 컴포넌트가 생성될 때 템플릿을 클론하여 사용.
  • 생성자에서 template을 shadow DOM에 추가.

예시 코드:

constructor() {
    super();
    this.attachShadow({ mode: "open" }); 
    this.shadowRoot.appendChild(template.content.cloneNode(true)); // 초기 렌더링
    this.linkElement = this.shadowRoot.getElementById("link");
    this.updateLink(this.getAttribute("href"));
  }

@yolophg
Copy link
Contributor Author

yolophg commented Jun 20, 2024

Example code of the best practice

class DsExample extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.validateAttributes();
      this.setDefaultAttributes();
  
      this.shadowRoot.innerHTML = `
        <style>
          /* Add your CSS styling here */
        </style>
        <!-- Add your HTML here -->
        <div>example</div>
      `;
    }
  
    validateAttributes() {
      /* Add validate Attributes */
    }
  
    setDefaultAttributes() {
      /* Add default attributes settings */
    }
  }
  
customElements.define('ds-example', DsExample);

@DaleSeo DaleSeo moved this from Todo to Done in 달레 스터디 웹사이트 Jun 20, 2024
@DaleSeo DaleSeo closed this as completed by moving to Done in 달레 스터디 웹사이트 Jun 20, 2024
@DaleSeo DaleSeo reopened this Jun 20, 2024
@DaleSeo DaleSeo closed this as completed by moving to Done in 달레 스터디 웹사이트 Jun 20, 2024
@sounmind
Copy link
Member

sounmind commented Jun 20, 2024

@yolophg

const html = `
	<style>
	  /* Add your CSS styling here */
	</style>
	<!-- Add your HTML here -->
	<div>example</div>
`;

class DsExample extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = html;

    this.validateAttributes();
    this.setDefaultAttributes();
  }

  validateAttributes() {
    /* Add validate Attributes */
  }

  setDefaultAttributes() {
    /* Add default attributes settings */
  }
}
  
customElements.define('ds-example', DsExample);

@DaleSeo 클래스 이름 앞에도 prefix 붙이는 거 맞을까요??

@DaleSeo DaleSeo moved this from Done to In Progress in 달레 스터디 웹사이트 Jun 20, 2024
@DaleSeo DaleSeo reopened this Jun 20, 2024
@DaleSeo
Copy link
Contributor

DaleSeo commented Jun 20, 2024

@DaleSeo 클래스 이름 앞에도 prefix 붙이는 거 맞을까요??

@sounmind 흠... 이건 미처 생각을 못해봤네요. 그닥 중요하지는 않지만 그래도 통일하면 좋겠네요. 그냥 심플하게 붙이지 마시죠? ㅋㅋ

@DaleSeo
Copy link
Contributor

DaleSeo commented Jun 20, 2024

한 단계 더 나아가, csshtml 변수에 스타일과 마크업을 아예 분리해서 저장하는 것은 어떻게 생각하세요?

const css = `
  <style>
    /* Add your CSS styling here */
  </style>
`

const html = `
  <!-- Add your HTML here -->
  <div>example</div>
`

class Example extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = css + html;

    this.validateAttributes();
    this.setDefaultAttributes();
  }

  validateAttributes() {
    /* Add validate Attributes */
  }

  setDefaultAttributes() {
    /* Add default attributes settings */
  }
}
  
customElements.define('ds-example', Example);

@SamTheKorean
Copy link
Contributor

오 이 방법도 가독성이 높아지고 수정할 때 편할 것 같습니다!

@sounmind
Copy link
Member

좋습니다ㅎㅎ 더 관리하기 편해질 것 같네요!

@SamTheKorean
Copy link
Contributor

그럼 이 스타일로 작업하겠습니다!

@sounmind
Copy link
Member

sounmind commented Jun 21, 2024

html, css 분리했을 때 코드 하이라이팅이 요상하게 되는 문제가 있긴 하군요...
@SamTheKorean 님, 별 문제 없었나요?

@SamTheKorean
Copy link
Contributor

저는 따로 인지는 못했는데 혹시 어떤 부분 말씀하시는 걸까요?

@sounmind
Copy link
Member

저는 따로 인지는 못했는데 혹시 어떤 부분 말씀하시는 걸까요?

/*html*/ 이것만 사용하셨을까요? /*css*/와 함께 사용하려니 문제가 생겨서요.
/*html*/만 사용하면 문제는 없네요..!

@SamTheKorean
Copy link
Contributor

네! /html/만 사용했습니다! /css/ 테그도 사용해야하는 지 몰랐습니다..!

@sounmind
Copy link
Member

@DaleSeo @yolophg @SamTheKorean @bhyun-kim 아래와 같은 패턴은 어떠실까요?

반복되는 불필요한 코드(style tag, global style)를 함수로 빼냈습니다.
그리고 이 형태면 따로 주석 작성할 필요 없이 syntax highlighting, formatting의 효과를 누릴 수 있습니다.

import {
  createCSS as css,
  createHTMLWithGlobalCSS as html,
} from "../html-css-utils";

const style = css`
  /* Default: Extra-small devices such as small phones (less than 640px) */
  a {
    display: flex;
	...
`;

const htmlContent = html`
  <a>
    <slot></slot>
  </a>
`;

class ButtonLink extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = htmlContent + style;
여기서 사용된 유틸 함수는 다음과 같습니다.
/**
 * Processes a template literal to combine strings and interpolated values into a single HTML string.
 *
 * @param {TemplateStringsArray} strings - An array of string literals.
 * @param {...string} values - Interpolated values within the template literal.
 * @returns {string} The combined HTML string.
 */
export function createHTMLWithGlobalCSS(strings, ...values) {
  const GlobalCSS = `<link rel="stylesheet" href="./global-styles.css" />`;

  let htmlString = strings[0];

  for (let i = 0; i < values.length; i++) {
    htmlString += values[i] + strings[i + 1];
  }

  return GlobalCSS + htmlString;
}

/**
 * Processes a template literal to combine strings and interpolated values into a single CSS string.
 *
 * @param {TemplateStringsArray} strings - An array of string literals.
 * @param {...string} values - Interpolated values within the template literal.
 * @returns {string} The combined CSS string.
 */
export function createCSS(strings, ...values) {
  let content = strings[0];

  for (let i = 0; i < values.length; i++) {
    content += values[i] + strings[i + 1];
  }

  return `
    <style>
      ${content}
    <style>
  `;
}

@SamTheKorean
Copy link
Contributor

저는 이 방향이 깔끔하고 좋은 것 같습니다!

@bhyun-kim
Copy link
Contributor

저도 좋습니다!

@DaleSeo
Copy link
Contributor

DaleSeo commented Jun 21, 2024

@DaleStudy/website 지난 번에 논의할 때 우리가 충분히 고려하지 않은 부분이 있습니다. 바로 declarative하게 프로그래밍을 하려면, 결국 어떻게 해서든 HTML 마크업 또는 CSS 스타일을 하기 전에 속성값이 접근할 수 있어야 한다는 것입니다. 이런 측면에서는 @yolophg 님께서 제안해주셨던 render() 함수를 사용하는 패턴에 대한 재평가가 필요할 것 같습니다.

아래 논의도 상당히 관련이 있으니 참고 바랍니다.
#51 (comment)

@sounmind
Copy link
Member

@DaleSeo 이거 될 수 있으면 빨리 논의해서 정하면 좋을 것 같은데요. 어떡할까요? 다음 정기 미팅까지 기다리기에는 코드 진행이 더딜 것 같습니다.

@yolophg
Copy link
Contributor Author

yolophg commented Jun 21, 2024

@DaleSeo @sounmind
컴포넌트 설계 패턴과 관련하여 간단히 제 의견 남깁니다.
declarative 방식으로 통일하는 것에 적극 공감하고, 개인적으로는 #51에서 두분이서 논의해주셨던 것들 중, render함수를 활용해서 css, html 각각 생성하는 것을 만들고, 최종적으로 shadowRoot에 삽입해주는 방식으로 가는 것이 좋을 것 같습니다.
여기서 속성 검증하는 부분도 함수로 분리하여 가면 좋을 것 같습니다!

@sounmind
Copy link
Member

sounmind commented Jun 23, 2024

@DaleStudy/website 현재 확정된 웹 컴포넌트 작성 패턴 예시입니다. 한 번 확인해주세요~

import { css, html } from "../html-css-utils.js";

class ButtonLink extends HTMLElement {
  constructor() {
    super();

    this.validateAttributes();
    this.render();
  }

  validateAttributes() {
    if (!this.hasAttribute("href")) {
      throw new Error('The "href" attribute is required.');
    }

    if (this.hasAttribute("size")) {
      const size = this.getAttribute("size");
      const validSizes = ["big", "small"];

      if (!validSizes.includes(size)) {
        throw new Error(
          `The "size" attribute must be one of ${validSizes.join(", ")}.`
        );
      }
    }

    if (this.hasAttribute("variant")) {
      const variant = this.getAttribute("variant");
      const validVariants = ["ghost", "primary"];

      if (!validVariants.includes(variant)) {
        throw new Error(
          `The "variant" attribute must be one of ${validVariants.join(", ")}.`
        );
      }
    }
  }

  render() {
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = this.createCss() + this.createHtml();
  }

  createCss() {
    return css`
      a {
        display: flex;
        justify-content: center;
        align-items: center;

        width: max-content;
        text-align: center;
      }
    `;
  }

  createHtml() {
    const href = this.getAttribute("href");
    const variant = this.getAttribute("variant") ?? "ghost";
    const size = this.getAttribute("size") ?? "small";

    return html`
      <a
        href="${href}"
        class="${size} ${variant}"
        target=${href.startsWith("#") ? "_self" : "_blank"}
      >
        <slot></slot>
      </a>
    `;
  }
}

customElements.define("ds-button-link", ButtonLink);

@DaleSeo
Copy link
Contributor

DaleSeo commented Jun 23, 2024

@sounmind 님, 예시 코드 정리해주셔서 감사합니다. 전반적으로 논의한 패턴과 부합합니다. 몇 가지 nitpicks을 드리자면,

  • createStyle() 함수는 createCss()로 함수 이름을 바꾸는 게 더 일관성 있지 않을까요?
  • createHTML() 함수는 createHtml()로 함수 이름을 바꾸면 좋겠습니다. 라이브러리를 써서 case 자동 변환할 때, edge case를 방지할 수 있어서, camelCase 사용할 때 일반적인 best practice 입니다.
  • render() 함수가 크게 하는 일이 없는데 굳이 별도 함수로 뺄 이유가 있는지 모르겠습니다. 그냥 constructor() 안에 풀어놓으셔도 무방할 것 같습니다.

위 의견 모두 어느정도 제 개인적인 선호가 반영되어 있으며, 대세에 전혀 영향을 주지 않는 사소한 디테일입니다. 따라서 최종 의사 결정은 테크리드에게 맡기도록 하겠습니다. 저는 현재 확정된 패턴으로도 충분히 만족스럽습니다. 😁 더 중요한 것은 프로젝트가 더 이상 패턴 때문에 지연되지 않아야 한다는 것입니다.

@sounmind
Copy link
Member

sounmind commented Jun 23, 2024

@DaleSeo 님 피드백 감사합니다. 네이밍 관련은 모두 동의해서 그대로 반영하면 좋겠습니다.

render 함수는 아래와 같은 이유로 유지하면 좋겠네요!

  • consturctor에서 엘리먼트 생성 시 render 관련 로직(shadow dom, html, css 생성)을 한 군데 모으는 역할
  • constructor에서 어떤 일을 하는지 빠르게 파악 가능

@SamTheKorean
Copy link
Contributor

예시 작성 감사합니다! 그럼 현재까지 작성된 방식으로 변경하도록 하겠습니다!

@sounmind sounmind linked a pull request Jun 24, 2024 that will close this issue
3 tasks
@sounmind sounmind mentioned this issue Jun 25, 2024
9 tasks
@DaleSeo DaleSeo moved this from In Progress to Done in 달레 스터디 웹사이트 Jun 25, 2024
@DaleSeo DaleSeo closed this as completed by moving to Done in 달레 스터디 웹사이트 Jun 25, 2024
@sounmind sounmind mentioned this issue Jul 4, 2024
3 tasks
@sounmind
Copy link
Member

sounmind commented Jul 4, 2024

다시 명확하게: 웹 컴포넌트 클래스에서 attribute를 가져오기 위해 getter, setter는 사용하지 않는다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Status: Done
Development

Successfully merging a pull request may close this issue.

5 participants