[JavaScript 시리즈 14편] 재사용 가능한 UI 패턴과 렌더 함수 설계

English version

접근성과 상태 흐름을 익혔다면, 자주 등장하는 UI를 패턴 단위로 묶어 중복을 줄여야 합니다. 패턴은 “상태를 받아 DOM을 갱신하는 렌더 함수”로 정의할 수 있습니다.

이번 글에서 새로 나오는 용어

  1. 패턴 카탈로그: 반복적으로 쓰이는 카드·리스트 같은 UI 조각을 문서로 정리한 목록입니다.
  2. 부분 갱신: 전체 리스트 대신 바뀐 항목만 찾아 교체해 성능을 지키는 업데이트 방식입니다.
  3. dataset: data-id처럼 HTML에 메타 정보를 저장해 JS에서 element.dataset.id로 읽는 속성입니다.
  4. 템플릿 헬퍼: 공통 렌더 규칙(마운트, 이벤트 연결 등)을 묶어 반복을 줄여 주는 함수입니다.

핵심 개념

  • 렌더 함수(Render Function, 상태 → 뷰 변환 함수): 상태를 입력으로 받아 HTML 문자열이나 DOM 조작을 출력하는 순수 함수입니다. 입력이 같으면 출력도 같아야 합니다.
  • 패턴 카탈로그(Pattern Catalog, 반복 UI 목록): 카드, 리스트, 배너, 모달 등 반복되는 구조를 목록으로 정리한 문서입니다. 필요한 상태, 이벤트, 접근성 속성을 함께 기록합니다.
  • 부분 갱신(Partial Update, 필요한 조각만 갱신): 리스트 전체를 다시 그리는 대신 변경된 아이템만 업데이트하는 패턴입니다. dataset이나 id를 활용합니다.
  • 뷰/상태 분리(View-State Separation, 역할 분리): 렌더 함수에는 비즈니스 로직이 없어야 유지보수가 쉽습니다. 데이터 가공은 다른 함수에서 담당합니다.

코드로 확인하기

먼저 카드와 리스트 패턴을 순수 함수로 정의합니다.

function renderCard({ title, summary, actions = [] }) {
  return `
    <article class="card">
      <h3>${title}</h3>
      <p>${summary}</p>
      <div class="card-actions">
        ${actions.map((action) => `<button data-action="${action.id}">${action.label}</button>`).join("")}
      </div>
    </article>
  `;
}

function renderList({ items, emptyMessage = "항목이 없습니다." }) {
  if (items.length === 0) {
    return `<p class="list-empty" role="status">${emptyMessage}</p>`;
  }
  return `
    <ul class="list" role="list">
      ${items.map((item) => `<li data-id="${item.id}">${renderCard(item)}</li>`).join("")}
    </ul>
  `;
}
const listRoot = document.querySelector("#project-list");

function updateProjectList(projects) {
  if (!listRoot) return;
  listRoot.innerHTML = renderList({ items: projects });
}

function updateSingleProject(project) {
  const target = listRoot?.querySelector(`[data-id="${project.id}"]`);
  if (!target) return;
  target.outerHTML = `<li data-id="${project.id}">${renderCard(project)}</li>`;
}

updateProjectList는 전체를 렌더링하고 updateSingleProject는 부분 갱신을 담당합니다.

이제 패턴 렌더러 헬퍼를 만들어 템플릿과 마운트 규칙을 통일합니다.

function createPatternRenderer({ root, template, onMount }) {
  let currentState = null;

  function render(state) {
    currentState = state;
    root.innerHTML = template(state);
    onMount?.({ root, state: currentState });
  }

  return { render, getState: () => currentState };
}

const banner = createPatternRenderer({
  root: document.querySelector(".alert-banner"),
  template: ({ type, message }) => `
    <div class="alert alert--${type}" role="alert">
      ${message}
      <button class="alert-close" aria-label="배너 닫기">×</button>
    </div>
  `,
  onMount: ({ root }) => {
    root.querySelector(".alert-close")?.addEventListener("click", () => {
      root.innerHTML = "";
    });
  },
});

banner.render({ type: "info", message: "저장이 완료되었습니다." });

왜 중요한가

  • 패턴 단위로 코드를 나누면 팀원이 합류했을 때 “이 UI는 renderList 패턴을 따라야 한다”라고 설명하기 쉬워집니다.
  • 부분 갱신은 대량의 데이터를 다룰 때 성능을 안정적으로 유지하는 비결입니다.
  • 패턴 카탈로그는 디자인 시스템으로 확장할 기초가 됩니다. Figma 컴포넌트와 코드가 같은 이름을 사용하도록 맞출 수 있습니다.

실습

  • 따라 하기: 카드, 리스트, 배너 패턴을 각각 렌더 함수로 만들고 동일한 상태를 넣어 결과를 비교합니다.
  • 확장하기: createPatternRenderer와 같은 헬퍼를 만들어 render, onMount 구조를 통일합니다.
  • 디버깅: 렌더 함수 안에서 상태를 직접 수정하도록 일부러 실수를 만들고, 왜 버그가 생기는지 기록한 뒤 순수 함수로 되돌립니다.
  • 완료 기준: 최소 세 가지 패턴이 같은 상태 구조를 받아 렌더링하고, 한 패턴에서 부분 갱신 함수가 정상 동작하면 실습이 끝납니다.

마무리

재사용 가능한 패턴을 갖추면 UI를 확장할 때 속도가 붙습니다. 다음 글에서는 API 에러 상태와 네트워크 메시지를 이 패턴 위에 어떻게 올릴지 살펴보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요