본문 바로가기

Front-End

프로그래머스 과제 테스트-좋아하는 언어 검색기

728x90

INDEX

    Stack

    #VanillaJS

    문제와 풀이(모범답안..)는 링크로 걸어두겠습니다.

    https://school.programmers.co.kr/skill_check_assignments/298

     

    프로그래머스

    코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

    programmers.co.kr

    https://prgms.tistory.com/139

     

    '2022 Dev-Matching: 웹 프론트엔드 개발자(상반기)' 과제 테스트 해설

    프론트엔드 개발자의 이직/구직을 위한 데브매칭! 지난 2022년 3월 12일 토요일 오후 2시부터 5시까지 3시간 동안 '2022 Dev-Matching: 웹 프론트엔드 개발자(상반기)'의 과제 테스트가 진행되었습니다.

    prgms.tistory.com

    Preview

    프로그래머스에서 세 시간 동안 주어지는 과제테스트를 연습한 코드를 기록해보려 합니다. 비록 잘한 건 아니지만 그래도 저만의 접근법을 

    공유하고 싶었습니다.

     

    vanillaJS로 개발했고 두시간 반 정도 소요됐습니다.

     

    문제를 간단히 요약하면 좋아하는 언어를 검색하면 api를 통해 해당 검색어가 포함된 언어 리스트를 받아옵니다.

     

    받아온 언어리스트를 뿌려주고 선택하면 alert와 함께 상단에 좋아하는 항목으로 쌓아둡니다. 

    구현 조건에는 다음과 같은 조건들이 있었습니다.

    1. 화살표 키(위, 아래) 를 통해 검색목록을 순회할 수 있도록 할 것. 단, 제일 아래에서 한번 더 아래키를 누르면 가장 처음으로 돌아올 것.

    2. 화면을 껐다 켜도 기존 검색어와 좋아하는 언어 목록 데이터를 유지하고 있을 것.

    3. 화면을 처음 렌더링할 때 input에 focus가 될 것.

    4. 검색어와 동일한 text에 class를 추가해 하이라이트(색칠)가 될 것. [구현 X]

     

    index.html

    -주어진 파일입니다. 기존 주어진 코드는 건드리지 않았습니다.

    <html>
      <head>
        <title>2022 FE 데브매칭</title>
        <link rel="stylesheet" href="./style.css" />
      </head>
      <body>
        <main class="App">
          <div class="SelectedLanguage">
            <ul>
              <li>JavaScript</li>
              <li>Python</li>
              <li>Elixir</li>
              <li>Java</li>
              <li>PHP</li>
            </ul>
          </div>
          <form class="SearchInput">
            <input
              class="SearchInput__input"
              type="text"
              placeholder="프로그램 언어를 입력하세요."
              value="Script"
            />
          </form>
          <div class="Suggestion">
            <ul>
              <li class="Suggestion__item--selected">
                Action<span class="Suggestion__item--matched">Script</span>
              </li>
              <li>Java<span class="Suggestion__item--matched">Script</span></li>
              <li>Type<span class="Suggestion__item--matched">Script</span></li>
              <li>Pure<span class="Suggestion__item--matched">Script</span></li>
            </ul>
          </div>
        </main>
      </body>
      <script type="text/javascript">
      /*소스*/
      <script/>
     </html>

     

    InitRender

    제일 처음 실행되는 함수입니다. 이 함수는 화면이 처음 켜질 때 실행되어야 하는 코드들을 담고 있습니다. 

    input에 focusing을 주고 ul목록을 초기화하는 등의 작업이 이루어집니다.

    const initiRender = () => {
          const inputField = document.querySelector("input"); //input element
          if (inputField) inputField.focus(); //focusing 구현
    
          inputField.addEventListener("input", updateValue);  // updateValue 함수 다음 코드블럭에 첨부
          inputField.value = ""; //input value 초기화
    
          //ul 초기화 
          ulInit(getUlByClassName("SelectedLanguage")); //ulInit함수. 이후 코드블럭에 첨부
          ulInit(getUlByClassName("Suggestion")); //getUlByClassName 함수. 이후 코드블럭에 첨부
          divGone("Suggestion"); //divGone함수 이후 코드블럭에 첨부
    
          if (localStorage.getItem("selectedList")) { //화면을 껐다켜도 검색목록 정보를 가져오기 위해 localstorage 활용
            var list = localStorage.getItem("selectedList").split(",");
            for (var i = 0; i < list.length; i++) {
              var li = document.createElement("li");
              li.appendChild(document.createTextNode(list[i]));
              getUlByClassName("SelectedLanguage").appendChild(li);
            }
          }
     };
     
     initiRender();

     

     

    updateValue

    Input에 Input 이벤트를 할당했습니다

    *change 이벤트를 할당했더니 input에 포커싱이 가있는 동안에는 텍스트를 입력하거나 지워도 change이벤트가 발생하지 않더군요.

    빈칸이면 서버로 전송하면 안됩니다. (에러 발생) 그래서 검색어가 빈 칸이면 ul 목록 초기화와 div를 숨김처리 해줬습니다.

    div를 숨기지 않으면 아무 내용이 없는 경우 아래 이미지처럼 div가 영역을 차지하고 있었습니다. 급한 마음에 그냥 검색 내용이 없으면 숨김 처리하자..라는 마인드로 바로 display = none

    const updateValue = (event) => {
          inputTxt = event.target.value;
          localStorage.setItem("inputText", inputTxt); //검색어를 localstorage에 저장.
          if (inputTxt === "") { //빈 칸이면 ul초기화 및 div hide처리(divGone함수)
            ulInit(getUlByClassName("Suggestion"));
            divGone("Suggestion");
            return;
          }
          getLangList(inputTxt); //api호출 함수
        };

    divGone & divShow

    위에서 말했듯 className을 인자로 받아 해당 div를 숨김처리 및 보임처리해 주는 함수입니다.

    const divGone = (divClassName) => {
      var div = document.getElementsByClassName(divClassName)[0];
      div.style.display = "none";
    };
    const divShow = (divClassName) => {
      var div = document.getElementsByClassName(divClassName)[0];
      div.style.display = "block";
    };

     

    uiInit

    ul을 인자로 받아 해당 ul에 포함된 li들을 초기화해 주는 함수입니다. 검색어가 바뀔 때마다 검색목록을 초기화해주거나 처음 렌더링 시에 초기화해주는 역할을 합니다.

    const ulInit = (ul) => {
      var fc = ul.firstChild;
      while (fc) {
        ul.removeChild(fc);
        fc = ul.firstChild;
      }
    };

     

    getUlByClassName

    처음 html 파일을 보면 ul태그에 className이 할당되지 않았습니다. 있었으면 굳이 이런 함수 안 만들어도 됐는데 아쉬웠습니다. 이 함수는 Ul을 가지고 있는 Div의 className을 인자로 받아 하위 child인 ul 태그를 return 합니다.

    const getUlByClassName = (className) => {
      var ulComp = document.getElementsByClassName(className)[0].children[0];
      return ulComp;
    };

    getLangList

    fetch를 사용해 api 호출을 담당하는 함수입니다. 검색목록의 길이가 0보다 크면 (검색 목록이 존재하면) renderSearchList 함수를 호출하고 없으면 ul을 초기화하고 div를 숨겨줍니다.

    const getLangList = (inputTxt) => {
      fetch(
        `https://wr4a6p937i.execute-api.ap-northeast-2.amazonaws.com/dev/languages?keyword=${inputTxt}`
      )
        .then((response) => response.json())
        .then((response) => {
          var suggestionUl = getUlByClassName('Suggestion')
          this.cursorState = 0; //화살표로 검색목록을 순회하기 위해 만든 cursor Index 값
          if (response.length > 0) {
            renderSearchList(suggestionUl, response);
            divShow("Suggestion");
          } else {
            //초기화
            ulInit(suggestionUl);
            divGone("Suggestion");
          }
        });
    };

     

    renderSearchList

    ul과 검색목록 array를 인자로 받아 해당 ul에 검색 목록 li를 렌더링 하는 함수입니다.

    const renderSearchList = (ul, list, searchTxt) => {
      if (list.length === 0 || !ul) return; //방어코딩
      ulInit(ul); //Ul초기화
      
      const listItemClicked = (e) => {
        const idx = e.target.getAttribute("itemIdx");
        const item = list[parseInt(idx)];
        alert(item);
    
        addItemToSelectedLang(getUlByClassName("SelectedLanguage"), item); 클릭한 아이템을 좋아하는 언어 목록에 추가
      };
      
      for (var i = 0; i < list.length; i++) {
        var li = document.createElement("li");
        li.appendChild(document.createTextNode(list[i]));
        li.setAttribute("itemIdx", i);
        li.addEventListener("click", listItemClicked); 
        ul.appendChild(li); //동적으로 Li를 생성해 Ul에 렌더링
      }
    };

    addItemToSelectedLang

    클릭 및 엔터이벤트로 선택된 언어 아이템을 좋아하는 목록(인자로 받은 Ul)에 rendering 하는 함수입니다.

    좋아하는 언어 목록에 추가하는 알고리즘은 다음과 같습니다.

    1. 최대 다섯 개일 것. 넘으면 FIFO(First In First Out) 규칙을 지킬 것.

    2. 이미 있는 언어이면 제일 마지막으로 이동시킬 것.

    const addItemToSelectedLang = (ul, text) => {
      if (!ul || !text) return; //방어코딩
    
      const MAX_SIZE = 5; //최대 5개만 보여준다. (구현조건)
      
      var existingList = Array.prototype.slice.call(ul.children);
      existingList = existingList.map((item) => item.innerHTML); //기존에 인자로 받은 Ul에 들어있는 li 아이템들의 value만 추출한 array
      
      var newList = [];
      if (existingList.includes(text)) { 
        //순서바꾸기
        newList = existingList.filter((item) => item !== text);
        newList.push(text);
      } else if (existingList.length === MAX_SIZE) {
        //FIFO
        newList = [...existingList];
        newList.shift();
        newList.push(text);
      } else {
        newList = [...existingList];
        newList.push(text);
      }
      
      ulInit(ul); //초기화 후 렌더링
      for (var i = 0; i < newList.length; i++) {
        var li = document.createElement("li");
        li.appendChild(document.createTextNode(newList[i]));
        ul.appendChild(li);
      }
      localStorage.setItem("selectedList", newList);
    };

     

    키보드 이벤트

    키보드의 화살표(위, 아래) 이벤트를 받아 검색 목록을 순회하도록 만든 함수입니다. 전역에서 cursor Index를 참조하기 위해 this.cursorState 변수에 현재 index값을 넣습니다. 검색어가 변경되면 해당 index값은 0으로 초기화됩니다. *getLangList함수 참고.

    window.onkeydown = (e) => {
      var suggestionUl = getUlByClassName("Suggestion");
      const listCount = suggestionUl.children.length;
      if (listCount === 0) return; //검색목록이 없으면 return
    
      if (e.code === "ArrowDown") {
        if (this.cursorState >= listCount - 1) {
          this.cursorState = 0;
        } else this.cursorState += 1;
        renderCursoredList(suggestionUl);
      } else if (e.code === "ArrowUp") {
        if (this.cursorState === 0) return;
        this.cursorState -= 1;
        renderCursoredList(suggestionUl);
      } else if (e.code === "Enter") {
        e.preventDefault();
        addItemToSelectedLang(
          getUlByClassName("SelectedLanguage"),
          suggestionUl.children[this.cursorState].innerHTML
        );
        alert(suggestionUl.children[this.cursorState].innerHTML);
      }
    };

     

    renderCursoredList

    this.cursorState 값으로 검색목록에서 해당 index에 있는 아이템에 class를 add 해줍니다.

    const renderCursoredList = (ul) => {
      Array.prototype.slice
        .call(ul.children)
        .map((item) => item.classList.remove("Suggestion__item--selected")); 
      var li = ul.children[this.cursorState];
      li.classList.add("Suggestion__item--selected");
    };

    이로써 vanillaJs로 개발한 테스트 과제 코드였습니다. 짧은 시간 동안 구현해야 하기 때문에 좀 억지로 개발한 코드들도 있지만 그래도 재밌게 참여했습니다. 시간이 촉박할 때 코드를 짜는 그 초집중 모드 시간이 좋았습니다.

    코드는 깃허브에 올려두겠습니다.

    'Front-End' 카테고리의 다른 글

    초성 검색 기능 구현하기  (0) 2023.08.23
    ReactJs에서 addEventListener 사용하기  (1) 2023.05.30
    OpenWeatherMap api by fetch and axios  (0) 2023.05.10
    Delaying of Function  (0) 2023.04.11
    [JS] Object 는 call by reference  (0) 2023.01.16