ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • '2022 Dev-Matching: 웹 프론트엔드 개발자(상반기)' 과제 테스트 해설
    취업 이야기/데브매칭 문제 해설 2022. 4. 1. 09:23

    프론트엔드 개발자의 이직/구직을 위한 데브매칭! 지난 2022년 3월 12일 토요일 오후 2시부터 5시까지 3시간 동안 '2022 Dev-Matching: 웹 프론트엔드 개발자(상반기)'의 과제 테스트가 진행되었습니다. 이 과제 테스트는 라이브러리나 프레임워크 없이 오직 Vanilla JavaScript로 문제를 해결해야 했는데요, 다른 개발자들은 이 문제를 어떻게 해결했을까요? 출제자의 해설과 함께 하나씩 짚어보겠습니다.


    문제 소개

    프로그래밍 언어 검색

    • 관련 직무 : 프론트엔드
    • 기술 태그 : HTML, JavaScript, CSS
    여러분은 자신이 좋아하는 프로그래밍 언어를 검색할 수 있는 서비스를 만드려고 합니다. 검색어를 입력하면 해당 검색어를 기준으로 서버에 요청을 하고, 서버에서 받은 검색어 목록을 렌더링 합니다. 검색어를 선택하거나 엔터키를 누르면 아래 요구사항에 맞게 동작하도록 만들어 봅시다.

    * 문제는 프로그래머스에서 다시 풀어볼 수 있습니다.


     

    문제해설

    생각해보기

    이번 데브매칭, 재미있게 즐기셨나요?

    자동완성을 구현하는 것은 실제 비즈니스에서도 자주 등장하는 요구사항입니다. 일반적으로 자동완성 기능은 라이브러리나 프레임워크 등을 이용해 구현하지만, 이렇게 JS만으로 구현해보는 경험이 자동완성 기능을 만들기 위해 어떤 것들을 해야 하는지, 그리고 그 원리가 무엇인지 파악하고 이해하는데 도움이 될 것입니다.

    이 문제를 어떤 식으로 구현하면 좋을지, 차근차근 설명을 해보겠습니다.

     

    구현하기에 앞서서

    컴포넌트의 구조 생각해보기

    화면에 구현해야하는 부분들을 아래와 같이 쪼개서 생각해볼 수 있을겁니다.

     

    각 컴포넌트는 아래와 같은 관계를 가지게 됩니다.

    App 컴포넌트가 세 컴포넌트를 제어하는 형태이고, SelectedLanguages, SearchInput, Suggestion 각각의 컴포넌트는 서로의 의존성을 띄지 않는 형태로 작성해야 추후 재사용 가능한 컴포넌트가 됩니다.

     

    코드 작성하기

    App 컴포넌트 작성

    제일 먼저, App 컴포넌트를 작성합니다.

    // App.js
    export default function App({ $target }) {
      this.state = {
        fetchedLanguages: [],
        selectedLanguages: []  
      }
    
      this.setState = (nextState) => {
        // TODO: 구현해야함
      }
    }

    그 후, App 컴포넌트를 생성하는 index.js를 선언하고 이를 index.html에서 불러오게 합니다.

    // index.js
    import App from './App.js';
    
    new App({ $target: document.querySelector('.App')})
    // index.html
    <html>
      <head>
        <title>2022 FE 데브매칭</title>
        <link rel="stylesheet" href="./style.css">
      </head>
      <body>
        <main class="App">
          <div class="SelectedLanguage"></div>
        </main>
        <script src="./index.js" type="module"></script>
      </body>
    </html>

     

    SearchInput 컴포넌트 구현하기

    기초 구현

    이제 나머지 컴포넌트들을 구현하고, App에서 이를 제어하는 형태로 만듭니다.

    // SearchInput.js
    export default function SearchInput({
      $target,
      initialState
    }) {
      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()
    
    }

     

    이제, 1차적으로 구현한 SearchInput 컴포넌트를 App 컴포넌트를 이용해 렌더링 합니다.

    import SearchInput from './SearchInput.js'
    
    export default function App({ $target }) {
      // 이전 코드 생략
    
      const searchInput = new SearchInput({
        $target,
        initialState: ''
      })
    }

     

    이제 SearchInput이 아래와 같이 표시됩니다.

     

    API 연동하기

    아직은 키 입력을 해도 아무 일도 일어나지 않는데, 키 입력 이벤트를 이용해 API를 호출해보도록 합니다.

    api.js 를 만듭니다.

    export const API_END_POINT = 'API END POINT'
    
    const request = async (url) => {
      const res = await fetch(url)
    
      if (res.ok) {
        const json = await res.json()
        return json
      }
    
      throw new Error('요청에 실패함')
    }
    
    export const fetchLanguages = async (keyword) => request(`${API_END_POINT}/languages?keyword=${keyword}`)

     

    재사용성을 생각하여 fetch를 호출하는 request 함수를 별도로 정의하고, 이 함수를 이용해 언어 목록을 조회하는 fetchLanguages 함수를 만듭니다.

    fetch 사용 시, response의 ok를 꼭 검사해야 올바른 호출이 됐는지 체크할 수 있습니다. 아래의 문서를 참고하세요.
    https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch
    https://developer.mozilla.org/en-US/docs/Web/API/Response/ok

     

    이제 SearchInput 컴포넌트의 생성자 파라메터로 onChange를 추가하고, 입력 이벤트 발생 시 해당 이벤트를 발생시키게 합니다.

    // SearchInput.js
    export default function SearchInput({
      $target,
      initialState,
      onChange
    }) {
      // 코드 생략
      this.render()
    
      // 이벤트 핸들러 구현부분
      this.$element.addEventListener('keyup', (e) => {
        onChange(e.target.value)
      })
    } 
    // App.js
    import SearchInput from './SearchInput.js'
    import { fetchLanguages } from './api.js'
    export default function App({ $target }) {
      // 코드 생략
    
      const searchInput = new SearchInput({
        $target,
        initialState: '',
        onChange: async (keyword) => {
          const languages = await fetchLanguages(keyword)
          console.log(languages)
        }
      })
    }

     

    지금은 API를 통해 데이터를 불러온 결과를 console.log로 찍게 해두었기 때문에, SearchInput 에서 언어를 검색하면 console을 통해 확인할 수 있습니다.

    이제 위에서 얻어온 데이터를 통해 추천 검색어를 렌더링하고 키보드 화살표 키로 선택할 수 있도록 합시다.

     

    Suggestion 구현하기

    렌더링 먼저 해보기

    우선 현재 상태 기반으로 추천 검색어를 노출하고, 이후 setState를 통해 다시 렌더링하는 구조로 컴포넌트를 작성합니다.

    // Suggestion.js
    export default function Suggestion ({
      $target,
      initialState
    }) {
      this.$element = document.createElement('div')
      this.$element.className = 'Suggestion'
      $target.appendChild(this.$element)
    
      this.state = initialState
    
      this.setState = (nextState) => {
        this.state = nextState
        this.render()
      }
    
      this.render = () => {
        const { items = []} = this.state
        if (items.length > 0) {
          this.$element.style.display = 'block'
          this.$element.innerHTML = `
            <ul>
              ${items.map((item, index) => `
                <li data-index="${index}">${item}</li>
                </li>
              `).join('')}
            </ul>
          `
        } else {
          this.$element.style.display = 'none'
          this.$element.innerHTML = ''
        }
      }
    
    
      this.render()
    }

     

    Suggestion 컴포넌트도 준비 되었으니, App 컴포넌트에서 Suggestion 컴포넌트를 생성하도록 하고, App 컴포넌트의 setState 함수를 아래와 같이 구현합니다.

    // App.js
    import SearchInput from './SearchInput.js'
    import Suggestion from './Suggestion.js'
    
    import { fetchLanguages } from './api.js'
    
    export default function App({ $target }) {
      this.state = {
        fetchedLanguages: [],
        selectedLanguages: []
      }
    
      this.setState = (nextState) => {
        this.state = {
          ...this.state,
          ...nextState
        }
        suggestion.setState({
          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: {
          items: []
        }
      })
    }

     

    이제 SearchInput 에 검색을 해보면, API를 통해 불러온 언어 목록이 렌더링 되는 것을 확인할 수 있습니다.

     

    키보드로 추천 검색어의 커서 옮기기

    이제 화살표 위, 아래 키로 추천 언어 목록의 커서를 움직이게 하는 처리를 합니다.

    현재 키가 어디를 순회하고 있는지를 위해 selectedIndex 라는 값을 Suggestion 컴포넌트의 state에 추가합니다.

    // 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()
      }
    
      // 이후 코드 생략
    }
    

     

    App 컴포넌트 내에서도 Suggesstion 의 상태를 변경할 시에 selectedIndex를 넣어주도록 합니다.

    // App.js
    import SearchInput from './SearchInput.js'
    import Suggestion from './Suggestion.js'
    
    import { fetchLanguages } from './api.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 suggestion = new Suggestion({
        $target,
        initialState: {
          cursor: 0,
          items: []
        }
      })
    }

     

    selectedIndex를 이용해 강조 처리하기

    문제의 지문을 보면 Suggestion__item--selected 클래스를 이용해 커서 처리를 하도록 나와있습니다.

    Suggestion 컴포넌트의 render 함수를 아래처럼 변경합니다.

    // Suggestion.js
    export default function Suggestion ({
      $target,
      initialState
    }) {
      // 이전 코드 생략
      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>
                </li>
              `).join('')}
            </ul>
          `
        } else {
          this.$element.style.display = 'none'
          this.$element.innerHTML = ''
        }
      }
    
      this.render()
    } 

     

    그리고 검색을 해보면 아래 이미지처럼, 첫번째 추천언어 위치에 커서 처리가 되는 것을 볼 수 있습니다.

     

    화살표 위, 아래 키 입력으로 selectedIndex 변경하기

    이제 selectedIndex만 변경하면 추천 검색어 목록 중 강조할 검색어를 변경할 수 있게 되었습니다.

    이제 키보드 이벤트를 통해, selectedIndex 값을 바꿀 수 있도록 하면 화살표 위, 아래키로 추천 검색어를 순회해볼 수 있을 것입니다.

    // Suggestion.js
    export default function Suggestion ({
      $target,
      initialState,
      onSelect
    }) {
      // 코드 생략
    
    
      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
            })
          }
        }
      })
    }

     

    버그 발생1: 커서 초기화 문제

    그런데 순회가 잘 안 되고, 자꾸 0번째로 돌아오는 현상이 생깁니다.

    왜 이런 일이 벌어지는 걸까요?

     

    화살표 키 입력 시 검색 안 되게 하기

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

    // SearchInput.js
    export function SearchInput({ $target, initialState, onChange }) {
      // 코드 생략
      this.$element.addEventListener('keyup', (e) => {
        // ArrowUp, ArrowDown 등에도 무조건 onChange가 호출되기 때문에
        // API를 다시 호출하고 다시 렌더링을 하게 됨    
        onChange(e.target.value)
      })
    }
    

    그래서 화살표 키를 입력하면 SearchInput 에 있는 keyup 이벤트가 반응하여, onChange 를 호출하게 되고 그러면 다시 검색이 일어나면서 Suggestion 이 다시 렌더링 되는 문제가 생기는 것입니다.

    이는 이번 과제의 필수 요구사항이었기 때문에, 반드시 처리해줘야 하는 부분입니다.

    여러가지 방법이 있겠지만 이 게시글에서는 SearchInputkeyup 이벤트에서, 화살표키를 입력했을 때는 onChange 이벤트를 발생시키지 않는 방법으로 하겠습니다.

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

    // SearchInput.js
    export default function SearchInput({
      $target,
      initialState,
      onChange
    }) {
      // 이전 코드 생략
    
      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 // 추가된 onSelect
    }) {
      // 코드 생략
    }

     

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

    // Suggestion.js
    export default function Suggestion ({
      $target,
      initialState,
      onSelect
    }) {
      // 코드 생략
      window.addEventListener('keyup', (e) => {
        if (this.state.items.length > 0) {
          // 코드 생략
          if (navigationKeys.includes(e.key)) {
            // 코드 생략
    
            // 엔터키 입력시 현재 커서의 위치의 추천검색어를 
            // 파라메터로 하여 onSelect 호출
          } else if (e.key === 'Enter') {
                    onSelect(this.state.items[this.state.selectedIndex])
          }
        }
    }

     

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

    // App.js
    export default function App({ $target }) {
      // 코드 생략
      const suggestion = new Suggestion({
        $target,
        initialState: {
          selectedIndex: 0,
          items: [],
        },
        onSelect: (language) => {
          alert(language)
        }
      })
    }

     

    버그발생 2: 화면 새로고침 문제

    그런데 의도대로 alert이 뜨지 않고 화면이 새로고침이 됩니다.

    왜냐하면 SearchInput 컴포넌트의 inputform 태그로 감싸져있는데요. form 태그 내 input에 focus가 있는 상태에서 엔터키를 입력하면, form의 action에 지정된 url로 화면이동을 하려고 하는 습성이 있기 때문입니다.

    이는 SearchInputform에서 submit 이벤트 발생 시 preventDefault를 호출해주는 것으로 간단하게 해결할 수 있습니다.

     

    SearchInput의 form에서 submit 이벤트 처리하기

    // SearchInput.js
    export default function SearchInput({
      $target,
      initialState,
      onChange
    }) {
      // 코드 생략
      // submit 이벤트 무시하기
    
      this.$element.addEventListener('submit', (e) => {
        e.preventDefault()
      })
    } 

    혹은 SearchInputform 대신 div로 감싸거나 혹은 this.$element 자체를 input으로 만들어서 해결하는 방법도 있긴 한데, input을 쓸 때는 form으로 감싸는 습관을 들이는 것이 좋습니다.

    위의 코드를 넣고, 추천 언어 아무곳이나 커서를 옮긴 후에 엔터키를 누르면 커서가 가리키는 언어가 alert으로 잘 뜰 것입니다.

     

    마우스 클릭으로 선택처리 하기

    다음으로 마우스로 추천 검색어를 직접 클릭했을 때에도 onSelect 를 호출하는 처리를 합니다.

    키보드 화살표 키로 처리하는 것과 비교해보면, 커서가 어디에 있고 무엇을 가리키는지 처리할 필요가 없고 단순히 어느 위치의 추천 검색어를 클릭했느냐를 알아내는 것이 핵심입니다.

    이벤트 델리게이션을 이용해 하나의 이벤트로 처리합니다.

    이벤트 델리게이션(이벤트 위임)에 대한 내용은 https://developer.mozilla.org/ko/docs/Learn/JavaScript/Building_blocks/Events 문서를 참고하세요.

     

    Suggestion 의 추천 검색어들을 li로 렌더링하면서 data-index=”${index}” 값을 넣어줬었는데, 이 값을 이용해 클릭한 li의 index를 얻어옵니다.

    data-* attribute에 대해선 https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/Use_data_attributes 를 참고하세요.

     

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

    index는 마크업 상 string 형태로 들어가기 때문에 이 값은 문자열입니다. parseInt를 통해 숫자로 바꿔주는 것이 좋겠죠?

    숫자로 바꿔주지 않아도 자바스크립트 특성상 동작은 하겠지만, 이런 부분은 명확하게 하는 것이 좋을 것입니다.

    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('무언가 잘못되었습니다! 선택할 수 없습니다!')
          }
        }
      })
    }

    이제 Suggestion 관련 작업도 거의 끝나갑니다.

     

    App의 state에 선택한 언어 추가하기

    App 컴포넌트의 Suggestion 컴포넌트 생성하는 쪽에 선언해둔 onSelect 코드에 selectedLanguages를 자신의 state에 추가하는 코드를 추가합니다.

    // App.js
    export default function App({ $target }) {
      this.state = {
        fetchedLanguages: [],
        selectedLanguages: []
      }
    
      // 코드 생략
    
      const suggestion = new Suggestion({
        $target,
        initialState: {
          selectedIndex: 0,
          items: [],
        },
        onSelect: (language) => {
          alert(language)
    
          // 이미 선택된 언어인 경우, 맨 뒤로 보내버리는 처리
          const nextSelectedLanguages = [...this.state.selectedLanguages]
    
          const index = nextSelectedLanguages.findIndex((selectedLanguage) => selectedLanguage === language)
    
          if (index > -1) {
            nextSelectedLanguages.splice(index, 1)
          }
          nextSelectedLanguages.push(language)
    
          this.setState({
            ...this.state,
            selectedLanguages: nextSelectedLanguages
          })
        }
      })
    }

     

    SelectedLanguages 구현하기

    여기까지 잘 따라오셨다면 App 컴포넌트의 state 중 selectedLanguages 를 이용해서 선택된 언어가 무엇인지 렌더링하는 컴포넌트를 만들 수 있습니다.

    작업하기에 앞서, index.html 의 내용 중 div class="SelectedLanguage"></div> 코드를 제거합니다.

    해당 마크업은 SelectedLanguages 컴포넌트에서 넣어줄 것이기 때문이죠.

    <!-- index.html -->
    <html>
      <head>
        <title>2022 FE 데브매칭</title>
        <link rel="stylesheet" href="./style.css">
      </head>
      <body>
        <main class="App"></main>
        <script src="./index.js" type="module"></script>
      </body>
    </html>

     

    아래와 같이 컴포넌트를 정의합니다.

    // SelectedLanguages.js
    export default function SelectedLanguage({
      $target,
      initialState
    }) {
      this.$element = document.createElement('div')
      this.$element.className = 'SelectedLanguage'
      this.state = initialState
    
      $target.appendChild(this.$element)
    
      this.setState = (nextState) => {
        this.state = nextState
        this.render()
      }
    
      this.render = () => {
        this.$element.innerHTML = `
          <ul>
            ${this.state.map((item) => `
              <li>${item}</li>
            `).join('')}
          </ul>
        `
      }
    
      this.render()
    }

     

    이후 App 컴포넌트에서 SelectedLanguages 컴포넌트를 생성하고, App 컴포넌트의 setState 함수 내에서 SelectedLanguages의 setState를 호출하여 상태를 변경하도록 추가합니다.

    // App.js
    import SelectedLanguages from './SelectedLanguages.js'
    import SearchInput from './SearchInput.js'
    import Suggestion from './Suggestion.js'
    
    import { fetchLanguages } from './api.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
        })
        selectedLanguges.setState(this.state.selectedLanguages)
      }
    
      const selectedLanguages = new SelectedLanguages({
        $target,
        initialState: []
      })
    
      const searchInput = new SearchInput({
        $target,
        initialState: '',
        onChange: async (keyword) => {
          // 입력한 검색어가 다 지워진 경우에는 fetchLanguages를 초기화 한다.
          if (keyword.length === 0) {
            this.setState({
              fetchedLanguages: [],
            })
          } else {
            const languages = await fetchLanguages(keyword)
            this.setState({
              fetchedLanguages: languages
            })
          }
        }
      })
    
      const suggestion = new Suggestion({
        $target,
        initialState: {
          selectedIndex: 0,
          items: []
        }
      })
    }

     

    5개까지만 선택할 수 있게 하기

    요구사항에 보면, 5개까지만 선택이 가능하도록 되어있습니다.

    현재 구현된 컴포넌트에서는 그러한 제한이 없어서 계속 추가가 되는데, 요구사항대로 제약사항을 구현하도록 하겠습니다.

    다양한 방법이 있겠지만, 여기에서는 SelectedLanguages 에서 state로 받은 selectedLanguages 배열의 값이 5개를 초과하면 앞부분을 잘라버리는 것으로 처리하겠습니다.

    // SelectedLanguages.js
    const MAX_DISPLAY_COUNT = 5
    export default function SelectedLanguage({
      $target,
      initialState
    }) {
      // 코드 생략
    
      this.setState = (nextState) => {
        this.state = nextState
    
        if (this.state.length > MAX_DISPLAY_COUNT) {
          const startPosition = this.state.length - MAX_DISPLAY_COUNT
          this.state = this.state.slice(startPosition, MAX_DISPLAY_COUNT + startPosition)      
        }
        this.render()
      }
    
      // 코드 생략
    }

    이렇게 기본적인 구현사항들은 끝났습니다.

    여기까지 잘 따라오셨다면, 아래와 같이 동작하는 애플리케이션이 완성되어 있을 것입니다.

     

     

     

    수고하셨습니다!

     

     


    추가 구현사항들

    필수 구현사항들 외에도 몇가지 보너스 점수가 주어지는 추가 구현사항들이 있는데, 이 추가구현 사항들은 몇 가지 힌트만 남겨두겠습니다. 힌트를 토대로 직접 풀어보시는 것이 좋겠지요?

     

    화면 들어오자마자 SearchInput에 focus 가게 하기

    이것은 SearchInput 컴포넌트 어딘가에 this.$element.focus() 한줄만 넣어주면 됩니다.

    사소한 부분이지만 이런 사소한 부분들이 하나씩 쌓이고 쌓여서, 사용성 좋은 웹 애플리케이션의 밑거름이 됩니다.

     

    API 캐시하기

    현재 코드에서 api.js의 request 함수 부분을 조금 고치면, 키워드 별로 온 응답에 대해 캐시할 수 있습니다.

    간단하게 메모리를 통해 캐시를 하면 아래와 같습니다.

     // api.js
    
    // 여기에 캐시하기
    const cache = {}
    
    const request = async (url) => {
      if (cache[url]) {
        return cache[url]
      }
    
      const res = await fetch(url)
    
      if (res.ok) {
        const json = await res.json()
        cache[url] = json
        return json
      }
    
      throw new Error('요청에 실패함')
    }

    그외에도 local storage나 session storage, 혹은 쿠키 등을 사용하는 방법도 있습니다만 캐시는 어떻게 만료시킬 것인가를 항상 고민해야 합니다.

    무엇보다 중요한 것은 잘못된 응답값이나 에러값을 캐시하면 안 되는 부분인데, 이렇게 될 경우 해당 캐시를 날리기 전까지 치명적인 에러가 발생하게 될 것입니다. 이를 방지하려면 fetch 후의 response의 ok값을 검사하고, response의 데이터를 잘 검증한 후 캐시하는 작업이 필요할 것입니다.

     

    검색어 입력하는 동안 API 호출 지연하기

    이것은 debounce 기법을 통해 처리하면 됩니다.

     

    새로고침 시에도 현재 상태 유지하게 하기

    이것도 여러가지 방법이 있겠지만, SearchInput 의 현재 입력값과 SuggestionselectedIndex 이 변경될 경우에 App의 state에도 바로바로 반영이 되게 한다음, App 컴포넌트의 this.setState 코드 내에서 this.state를 통째로 local storage나 session storage에 넣게 하면 됩니다.

    이후 App 컴포넌트 코드에 storage에서 저장된 상태가 있다면 불러와서 초기 state에 할당하는 등의 코드를 넣으면 됩니다.

     

    추천 검색어와 입력한 검색어가 일치하는 부분 강조처리 하기

    이것을 구현하기 위해서는 Suggestion 에 setState 할 때, 현재 SearchInput에 어떤 키워드가 입력이 되어있는지도 같이 내려주어야 합니다. 물론 Suggestion 컴포넌트에서 생성된 SearchInput 컴포넌트에 직접 접근해서 가져오는 방법도 있겠지만, 이렇게 하면 두 컴포넌트가 강결합 되기 때문에 그 상위 컴포넌트인 App에서 setState를 통해 공유 받는 방법이 좋지 않을까 싶습니다.

    렌더링 코드는 아래의 코드를 참고하세요.

    // Suggestion.js
    export default function Suggestion({ $target, initialState, onSelect }) {
    // 코드 생략
    
      this.renderMatchedItem = (keyword, item) => {
        if (!item.includes(keyword)) {
          return 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 { selectedIndex, keyword, items } = 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}">${this.renderMatchedItem(keyword, item)}</li>
                </li>
              `).join('')}
            </ul>
          `
        } else {
          this.$element.style.display = 'none'
          this.$element.innerHTML = ''
        }
      }
    }

     

     

     


    프로그래머스 데브매칭이 궁금하다면?

    Dev-Mathing은 개발자들과 유수의 기업을 이어주는 프로그래머스의 채용 프로그램입니다. 지원자는 하나의 이력서로 다수의 관심 있는 포지션을 선택하여 지원할 수 있으며, 이력서와 테스트 점수가 함께 기업에 전달되어 개발자를 더욱 주목받게 도와주는 프로그램입니다. 자세한 내용은 프로그래머스에서 확인하세요!

    프로그래머스 데브매칭 보기

     

     

    댓글

Programmers