바닐라 JS로 autocomplete 만들기

Rocky·2022년 11월 13일
0

프로그래머스 '2022 Dev-Matching: 웹 프론트엔드 개발자(상반기)'
기출 문제 해설을 참고하여 작성하였습니다.

컴포넌트 구조

  • index.js : App.js의 로직을 index.html에 보여주는 역할
  • App.js : 전역 상태를 관리하면서 아래 3개의 컴포넌트를 연결시켜주는 역할
  • SelectedLanguages.js : 선택된 값을 태그로 보여주는 영역
  • SearchInput.js : 검색 영역
  • Suggestion.js : 결과 영역

주요 로직

  1. 인풋에 값을 입력했을 경우, api 호출
  2. 인풋에 포커스 되어있을 경우, 화살표로 검색된 값을 선택 할 수 있게 처리
  3. 인풋의 입력값과 동일한 텍스트는 음영처리
  1. 인풋에 값을 입력했을 경우, api 호출
src/api/common.js
const cache = {};

// api 호출 로직을 분리하여 캐싱 및 에러처리가 되도록 처리
export const request = async (url, errorMessage = "요청에 실패했습니다.") => {
  if (cache[url]) {
    return cache[url];
  }
  const res = await fetch(url);
  if (res.ok) {
    const data = await res.json();
    cache[url] = data;
    return data;
  }

  throw new Error(errorMessage);
};
src/js/SearchInput.js
import { debounce } from "../utils/debounce.js";

export default function SearchInput({ target, initialState, onChange }) {
  /* (...생략) */ 
  // 방향키 위, 아래, 엔터는 예외 처리 후, onChange를 props로 App.js에서 로직 처리 
  this.element.addEventListener(
    "keyup",
    debounce(({ key, target: { value } }) => {
      const ignoreKeys = ["ArrowUp", "ArrowDown", "Enter"];
      if (ignoreKeys.includes(key)) return;
      onChange(value);
    }, 500)
  );
}
src/js/App.js
export default function App({ target }) {
  /* (...생략) */ 
  const searchInput = new SearchInput({
    target,
    initialState: "",
    onChange: async (keyword) => {
      // 검색 키워드가 없을 경우 검색결과 값을 초기화하고,
      // 값이 있을 경우 api 호출
      if (!keyword) {
        this.setState({
          fetchedLanguages: [],
        });
      } else {
        const languages = await fetchLanguages(keyword);
        this.setState({
          fetchedLanguages: languages,
          keyword,
        });
      }
    },
  });
}
  1. 인풋에 포커스 되어있을 경우, 화살표로 검색된 값을 선택 할 수 있게 처리
src/js/Suggestion.js
export default function Suggestion({ target, initialState, onSelect }) {
  /* (...생략) */
  window.addEventListener("keyup", ({ key }) => {
    const { selectedIndex } = this.state;
    const lastIndex = this.state.items.length - 1;
    const keys = ["ArrowUp", "ArrowDown", "Enter"];
    let nextIndex = selectedIndex;

    if (keys.includes(key)) {
      switch (key) {
        case "ArrowUp":
          nextIndex = selectedIndex === 0 ? lastIndex : selectedIndex - 1;
          break;
        case "ArrowDown":
          nextIndex = selectedIndex === lastIndex ? 0 : selectedIndex + 1;
          break;
        case "Enter":
          onSelect(this.state.items[this.state.selectedIndex]);
          break;
        default:
      }
      this.setState({ ...this.state, selectedIndex: nextIndex });
    }
  });

  window.addEventListener("click", (e) => {
    const idx = e.target.dataset.index;
    if (!idx) return;
    this.setState({ ...this.state, selectedIndex: idx });
    onSelect(this.state.items[idx]);
  });
}
src/js/App.js
export default function App({ target }) {
  /* (...생략) */ 
  // 검색결과 컴포넌트
  const suggestion = new Suggestion({
    target,
    initialState: {
      items: [],
      selectedIndex: 0,
      keyword: "",
    },
    onSelect: (language) => {
      this.setState({
        selectedLanguages: [...this.state.selectedLanguages, language],
      });
    },
  });
}
  1. 인풋의 입력값과 동일한 텍스트는 음영처리
src/js/Suggestion.js
export default function Suggestion({ target, initialState, onSelect }) {
  /* (...생략) */
  this.renderMatchItem = (keyword, item) => {
    const matchedText = item.match(new RegExp(keyword, "gi"))[0];
    return item.replace(
      new RegExp(matchedText, "gi"),
      `<span class="Suggestion__item--matched">${matchedText}</span>`
    );
  };

  this.render = () => {
    const { items = [], selectedIndex, keyword } = this.state;

    if (items.length > 0) {
      this.element.style.display = "block";
      this.element.innerHTML = `
      <ul>
        ${items
          .map(
            (item, idx) =>
              `<li class="${
                Number(idx) === selectedIndex
                  ? "Suggestion__item--selected"
                  : ""
              }" data-index="${idx}">${this.renderMatchItem(
                keyword,
                item
              )}</li>`
          )
          .join("")}
      </ul>
      `;
    } else {
      this.element.style.display = "none";
      this.element.innerHTML = "";
    }
  };

  this.render();
}

참고 레포지토리 : https://github.com/rrrrrrrrrrrocky/vanilla-js-autocomplete

profile
r이 열한개!

0개의 댓글