개발 알다가도 모르겠네요

프로그래밍 언어 검색 -2 본문

웹/프론트엔드 직군 과제

프로그래밍 언어 검색 -2

이재빵 2022. 8. 26. 22:23
728x90

화살표 방향키 입력으로 seleectedIndex 변경

 

키보드 이벤트를 통해, selectedIndex 값을 바꿀 수 있도록 해보자.

//Suggestion.js
export default function Suggestion({ $target, initialState }) {
  this.$element = document.createElement("div");
  this.$element.className = "Suggestion";
  $target.appendChild(this.$element);

  this.state = {
    selectedIndex: 0,
    items: initialState.items,
  };

  this.setState = (nextState) => {
    this.state = {
      ...this.state,
      ...nextState,
    };
    this.render();
  };

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

  this.render();

//추가된 부분
  window.addEventListener("keyup", (e) => {
    if (this.state.items.length > 0) {
      const { selectedIndex } = this.state;
      const lastIndex = this.state.items.length - 1;
      const navigationKeys = ["ArrowUp", "ArrowDown"];
      let nextIndex = selectedIndex;

      if (navigationKeys.includes(e.key)) {
        if (e.key === "ArrowUp") {
          nextIndex = selectedIndex === 0 ? lastIndex : nextIndex - 1;
        } else if (e.key === "ArrowDown") {
          nextIndex = selectedIndex === lastIndex ? 0 : nextIndex + 1;
        }

        this.setState({
          ...this.state,
          selectedIndex: nextIndex,
        });
      }
    }
  });
}

 

버그: 커서 초기화 문제

 

화살표 키 입력시 검색 안되게 처리

 

현재 SearchInput의 구현에서는 keyup 이벤트가 발생하면 무조건 onChange를 호출하도록 되어 있다.

export default function SearchInput({ $target, initialState, onChange }) {
  //코드 생략
  
  //이벤트 핸들러 구현부분
  this.$element.addEventListener("keyup", (e) => {
  //ArrowUp, ArrowDown 등에도 무조건 onChange 호출됨
  //API를 재호출 -> 재렌더링
    onChange(e.target.value);
  });
}

 

그래서 화살표 키를 입력하면 SearchInput에 있는 keyup 이벤트가 반응해, onChange를 호출하게 되고

다시 검색이 일어나면서 Suggestion이 재렌더링 되는 것이다.

 

따라서 해결방법으로 SearchInput keyup 이벤트에서, 화살표 키를 입력했을 때는 onChange 이벤트를 발생시키지 않도록 할 것이다.

 

엔터키로 선택해야 하기 때문에 엔터키도 같이 검색이 일어나지 않도록 하는 처리를 추가한다.

export default function SearchInput({ $target, initialState, onChange }) {
  this.$element = document.createElement("form");
  this.$element.className = "SearchInput";
  this.state = initialState;

  $target.appendChild(this.$element);

  this.render = () => {
    this.$element.innerHTML = `<input class="SearchInput__input" type="text" placeholder="프로그래밍 언어를 입력하세요." value="${this.state}">`;
  };

  this.render();

  //이벤트 핸들러 구현부분
  this.$element.addEventListener("keyup", (e) => {
    const actionIgnoreKeys = [
      "Enter",
      "ArrowUp",
      "ArrowDown",
      "ArrowLeft",
      "ArrowRight",
    ];
    if (!actionIgnoreKeys.includes(e.key)) {
      onChange(e.target.value);
    }
  });
}

 

 

엔터키 눌러서 선택처리

다음으로 엔터키를 입력했을 때, 현재 커서가 가리키는 언어를 App의 state 중 selectedLanguages에 넣는 작업을 해야한다.

 

먼저 Suggestion에 생성자 함수 파라미터로 onSelect를 추가한다.

//Suggestion.js
export default function Suggestion({ $target, initialState, onSelect })

 

그 다음 keyup 이벤트를 처리하는 곳에서 엔터키가 눌렸을 경우 onSelect를 호출한다.

//Suggestion.js
export default function Suggestion({ $target, initialState, onSelect }) {
  this.$element = document.createElement("div");
  this.$element.className = "Suggestion";
  $target.appendChild(this.$element);

  this.state = {
    selectedIndex: 0,
    items: initialState.items,
  };

  this.setState = (nextState) => {
    this.state = {
      ...this.state,
      ...nextState,
    };
    this.render();
  };

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

  this.render();

  window.addEventListener("keyup", (e) => {
    if (this.state.items.length > 0) {
      const { selectedIndex } = this.state;
      const lastIndex = this.state.items.length - 1;
      const navigationKeys = ["ArrowUp", "ArrowDown"];
      let nextIndex = selectedIndex;

      if (navigationKeys.includes(e.key)) {
        if (e.key === "ArrowUp") {
          nextIndex = selectedIndex === 0 ? lastIndex : nextIndex - 1;
        } else if (e.key === "ArrowDown") {
          nextIndex = selectedIndex === lastIndex ? 0 : nextIndex + 1;
        } 
        //추가된 부분 
        //엔터키 입력시 현재 커서의 위치의 추천검색어를 파라미터로 하여 onSelect 호출
        else if (e.key === "Enter") {
          onSelect(this.state.items[this.state.selectedIndex]);
        }

        this.setState({
          ...this.state,
          selectedIndex: nextIndex,
        });
      }
    }
  });
}

 

이제 App 컴포넌트에서 Suggestion 컴포넌트를 생성할 때, onSelect 함수를 정의한다.

//App.js
import { fetchLanguages } from "./api.js";
import SearchInput from "./SearchInput.js";
import Suggestion from "./Suggestion.js";

export default function App({ $target }) {
  this.state = {
    fetchedLanguages: [],
    selectedLanguages: [],
  };

  this.setState = (nextState) => {
    this.state = {
      ...this.state,
      ...nextState,
    };
    suggestion.setState({
      selectedIndex: 0,
      items: this.state.fetchedLanguages,
    });
  };

  const searchInput = new SearchInput({
    $target,
    initialState: "",
    onChange: async (keyword) => {
      if (keyword.length === 0) {
        this.setState({
          fetchedLanguages: [],
        });
      } else {
        const languages = await fetchLanguages(keyword);
        this.setState({
          fetchedLanguages: languages,
        });
      }
    },
  });

  const suggestion = new Suggestion({
    $target,
    initialState: {
      cursor: 0,
      items: [],
    },
    
    //추가된 부분
    onSelect: (language) => {
      alert(language);
    },
  });
}

 

버그: 화면 새로고침 문제

alert가 뜨지 않고 화면이 새로고침되는 문제가 발생한다.

SearchInput 컴포넌트의 inputform태그로 감싸져있기 때문인데, form태그input에 focus가 있는 상태에서 엔터키를 입력하면,

form의 action에 지정된 url로 화면이동을 하려는 습성이 있기 때문이다.

 

따라서 SearchInputform에서 submit 이벤트 발생 시 preventDefault를 호출해주는 것으로 해결할 수 있다.

 

 

SearchInput의 form에서 submit 이벤트 처리

//SearchInput.js
export default function SearchInput({ $target, initialState, onChange }) {
  this.$element = document.createElement("form");
  this.$element.className = "SearchInput";
  this.state = initialState;

  $target.appendChild(this.$element);

  this.render = () => {
    this.$element.innerHTML = `<input class="SearchInput__input" type="text" placeholder="프로그래밍 언어를 입력하세요." value="${this.state}">`;
  };

  this.render();

  //이벤트 핸들러 구현부분
  this.$element.addEventListener("keyup", (e) => {
    const actionIgnoreKeys = [
      "Enter",
      "ArrowUp",
      "ArrowDown",
      "ArrowLeft",
      "ArrowRight",
    ];
    if (!actionIgnoreKeys.includes(e.key)) {
      onChange(e.target.value);
    }
  });

  //submit 이벤트 무시
  this.$element.addEventListener("submit", (e) => {
    e.preventDefault();
  });
}
SearchInputform 대신에 div로 감싸거나 this.$element 자체를 input으로 만들어서 해결할 수 있지만 위의 방법 추천.

 

 

 

마우스 클릭으로 선택처리

다음으로 마우스로 추천 검색어를 직접 클릭했을 때 onSelect를 호출하는 처리를 구현해보자.

마우스 클릭의 경우 단순히 어느 위치의 추천 검색어를 클릭했느냐를 알아내는 것이 핵심이다.

 

따라서 event delegation(이벤트 위임)을 이용해 하나의 이벤트로 처리한다.

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Building_blocks/Events

 

Suggestion의 추천 검색어들을 li로 렌더링하면서 넣은 data-index="${index}" 값을 이용해 클릭한 li의 index를 가져온다.

 https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/Use_data_attributes

 

dataset에는 해당 DOM의 data- 접두어로 붙어있는 모든 값이 들어있기 때문에 이것을 통해 index값을 꺼내온다.

//Suggestion.js
export default function Suggestion({ $target, initialState, onSelect }) {
  //코드 생략

//추가된 부분
  this.$element.addEventListener("click", (e) => {
    const $li = e.target.closest("li");
    if ($li) {
      const { index } = $li.dataset;
      try {
        onSelect(this.state.items[parseInt(index)]);
      } catch (e) {
        alert("무언가 잘못됐습니다! 선택할 수 없습니다!");
      }
    }
  });
}

index는 마크업 상 string 형태로 들어가기 때문에 문자열이다.

자바스크립트 특성상 숫자로 바꾸지 않아도 돌아가지만 명확하게 하기 위해 숫자로 바꿔준다.

' > 프론트엔드 직군 과제' 카테고리의 다른 글

프로그래밍 언어 검색 -3  (1) 2022.08.27
프로그래밍 언어 검색 -1  (0) 2022.08.26