4 분 소요

서론

오늘은 JSON파일에서 정보를 빼 와 원하는 대로 사용하는 법을 배웠다.

구현

링크

Type Ahead

Task

  1. fetch로 정보 받아오기
  2. 유저 입력받기
  3. 입력에 해당하는 정보 filter
  4. 화면에 결과 출력

1. fetch로 정보 받아오기

fetch는 서버에 네트워크 요청을 보내 새로운 정보를 받아오는 API다. 여기선 주어진 JSON파일의 정보를 받아와 그 정보들을 cities라는 배열에 저장하려고 한다.

fetch(접근하려고 하는 url)로 url에 접근한 뒤 Promise 객체를 반환한다. 이것을 Request라고 하는데, Request에 성공하든 실패하든 해당 통신에 대한 Response 객체가 취득된다. 이 단계에서 아직 본문이 전달되기 이전이지만, Response Header를 보고서 요청이 성공 여부를 알 수 있다.

만약 네트워크 문제가 발생하거나 존재하지 않는 url에 접근하려고 하면, HTTP 요청을 보낼 수 없는 상태이기에 Promise 거부 상태가 된다.

그리고 HTTP 상태 코드도 Response Property로 반환된다. 위의 상태라면 HTTP 상태 코드는 404로 설정되어 화면에 404 Not Found가 뜰 것이다.

응답을 받은 뒤, .then(blob => blob.json())으로 응답을 JSON형태로 파싱하는 것이다. 여기서 blob은 Binary Large OBject을 의미하는 것 같다. 파싱을 정확히 이해하진 못 했는데, parse - 문장을 문법대로 분석하다 라는 단어 뜻대로, 데이터를 분해하고 분석하여 원하는 형태로 조립하는 것을 parsing이라 이해했다.

fetch(endpoint)
  .then(blob => blob.json())
  .then(data => cities.push(...data))

그리하여 위의 코드는 아래와 같이 해석할 수 있는 것이다. 웹상에서 내가 가져온 정보를 원하는 형태로 가공하여 활용하기 위해서 JSON으로 파싱한 뒤, 그것을 .then(data => cities.push(...data))cities배열에 저장하는 것이다.

만약 여기서 구조 분해 할당(...)을 사용하지 않는다면, 배열의 길이는 1이다. 왜냐면 한 뭉치의 정보들을 한 번에 push하기 때문이다. 그래서 [{정보 1}, {정보 2}...] 이런 형태의 배열이 된다. 그러나 구조 분해 할당을 사용한다면 {정보 1}, {정보 2}... 이런 식으로 배열에 값들이 push된다.

2. 유저 입력받기

정보를 받아왔으니 이제 유저의 입력을 받을 차례이다. querySelectorinput을 가져온 뒤, addEventListener로 변경 사항을 확인하는 것이다.

change 이벤트는 input type에 상관없이, input에 어떠한 변경이 있을 때마다 뒤에 주어진 함수를 실행시킨다.

keyup 이벤트는 유저가 키를 눌렀다가 키를 놓을 때마다 함수를 실행시킨다.

input에 변화가 있을 때마다 결과가 달라지기 때문에 이렇게 실시간으로 변경을 확인하는 것이다.

3. 입력에 해당하는 정보 filter

입력에 따라서 정보들이 filter될 수 있게 findMatches라는 함수를 만든다.

여기서 match()를 사용하는데, 처음엔 왜 includes()를 안 사용하는지 의문이었다. 그래서 includes()를 사용해 봤는데, match()의 결과가 훨씬 정확했다. 왜 그런가 나름대로 이유를 생각해보니, 아무래도 includes()는 대소문자를 구별하여 차이가 나는 것이 아닐까 추측해본다. match()includes()는 유사해 보이지만 반환 값이 다르다. [includes()는 인수로 전달받은 문자나 문자열이 포함되어 있는지를 검사한 후 그 결과를 불리언 값으로 반환한다. match()`는 인수로 전달받은 정규 표현식에 맞는 문자열을 찾아서 하나의 배열로 반환한다.](http://www.tcpschool.com/javascript/js_standard_stringMethod)

match() 설명에 명시되어 있듯이, 이 메서드를 사용하기 위해선 정규식 개체가 필요하다. 정규식 개체를 생성하려면 RegExp() 생성자를 사용해야 한다. 뒤에 "gi"flags라고 한다. g는 global로, 처음 일치에서 중단하지 않고 문자열 전체를 판단하고, i는 ignore case로, 대소문자를 무시하겠다는 뜻이다. 그리하여 placecitystate든 대소문자 무시하고 문자열 전체를 살폈을 때 조건과 일치하는 것들만 반환하는 것이다.

4. 화면에 결과 출력

결과를 화면으로 보기 위해 displayMatches()라는 함수를 만들었다. findMatches()로 배열을 반환받은 뒤, replace()를 사용하여 유저의 입력과 일치하는 문자들을 <span class="hl">${this.value}</span>를 사용해 하이라이트 효과를 준다. 그리고 배열 내의 모든 요소를 HTML코드 형식으로 반환하여 suggestions.innerHTML = html;로 화면에 출력하는 것이다.

인구수는 numberWithCommas()라는 함수를 이용해 기존에 00000으로 보이던 상태에서 00,000 형태로 보이게 만들었다. 출처 우선 인자 x를 받고, 그걸 문자열로 반환한 뒤(x는 문자열로 변환되지 않는다), replace()를 이용해 변경하는 것이다.

여기서 사소한 오류가 하나 있는데, 애초에 place.population이 문자열이기 때문에 toString()을 사용할 필요가 없는데 영상에선 사용했다. 그래서 나는 toString()을 빼버렸다. 아까 정규식을 만들려면 RegExp()를 사용해야 한다고 했는데, 이거 외에도 앞뒤를 /로 감싸는 패턴도 있다. 예) /문자열/

정규식 패턴을 하나하나 찬찬히 뜯어보도록 하자. 우선 \B, (?=()), (\d), {3}, +, (?!\d)로 분리하자.

  • \B는 문자열의 첫 번째/마지막 문자가 단어 문자가 아닌 경우, 해당 문자의 앞부분/뒷부분을 의미한다. 두 단어/비 단어 문자 사이를 의미하는 것이다.
  • x(?=y)는 오직 y가 뒤따라오는 x만 의미한다. 여기에서는 처음과 끝 문자를 제외한 모든 문자 중, = 바로 뒤에 적혀있는 조건에 해당하는 문자만 의미한다.
  • \d는 문자열 내의 숫자 문자를 찾는다.
  • {n}는 앞 표현식이 n번 나타나는 부분을 의미한다. (n번 반복되는 X)
  • \d{n}은 숫자 문자가 3번 나타나는 부분 전체를 의미하는 것이다. 예를 들자면 “1234456”이면 “123”, “456” 이러는 것이 아니라, “123”, “234”, “345”, “456”을 의미한다.
  • +는 앞의 표현식이 1회 이상 연속으로 반복되는 부분을 의미한다. {1,} 과 같은 의미.
  • x(?!y)x(?=y)와는 반대로 x뒤에 y가 없는 x를 의미한다. 여기선 \B(?=(\d{3}))이 반복될 때, 뒤에 숫자 문자가 없는 부분을 의미한다. 정리하자면, 숫자 문자들 사이 뒤에, 숫자 문자가 3번 나타나는지 확인하고, 만약 숫자 문자가 3번 나타나는 것이 반복되고 반복 부분 바로 뒤에 문자가 숫자 문자가 아니라면, 거기를 “,”로 변경하는 것이다.

이 방법 외에, Number.prototype.toLocaleString()를 사용해도 된다. 우선 const populationNumber = parseInt(place.population);으로 정수형으로 파싱한 뒤, <span class="population">👤${populationNumber.toLocaleString()}</span>html에 추가하는 것이다.

TLD

fetch는 사용해 보았으나 정확히 이것이 무엇인지, 어떻게 작동되는지 몰랐는데, 앞으로 더 자세히 공부해 봐야겠다. 현재로서는 잘 이해하지 못하겠다.

그리고 RegExp는 처음 알았다! JS도 정규식이 있다는 걸 처음 알았다.

또한 찾다 보니 toLocaleString이라는 것도 알게 되었는데 앞으로 요긴하게 써먹을 것 같다.

최종 코드

const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
const searchInput = document.querySelector(".search");
const suggestions = document.querySelector(".suggestions");

const cities = [];
fetch(endpoint)
  .then(blob => blob.json())
  .then(data => cities.push(...data));
  
function findMatches(wordToMatch, cities) {
  return cities.filter((place) => {
    const regex = new RegExp(wordToMatch, "gi");
    return place.city.match(regex) || place.state.match(regex);
  });
}

function numberWithCommas(x) {
  return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

function displayMatches() {
  const matchArray = findMatches(this.value, cities);
  const html = matchArray.map((place) => {
    const regex = new RegExp(this.value, "gi");
    const cityName = place.city.replace(regex, `<span class="hl">${this.value}</span>`);
    const stateName = place.state.replace(regex, `<span class="hl">${this.value}</span>`);
    // const populationNumber = parseInt(place.population);
    // console.log(typeof(populationNumber));
    // <span class="population">👤${populationNumber.toLocaleString()}</span>
    return `
    <li>
      <span class="name">${cityName}, ${stateName}</span>
      <span class="population">👤${numberWithCommas(place.population)}</span>
      </li>
      `;
  }).join("");
  suggestions.innerHTML = html;
}

searchInput.addEventListener("change", displayMatches);
searchInput.addEventListener("keyup", displayMatches);

댓글남기기