Javascript_30_06

Derek·2020년 12월 21일
0

javascript_30

목록 보기
7/31
post-thumbnail

안녕하세요!

Derek입니다아. 😁

벌써 30개 프로젝트 중에 6번째 시간입니다. 벌써 20%나 했네요!

꼭 30개 모두 정리해서 포스팅 하는 날 까지.. 열심히 해보겠습니다. ✍
힘내자구요!🤘


오늘은 꽤나 길고 머나먼 포스팅이 될 것 같아요.

Day 06 project는 정말 모르는 것 투성이였고, 그 모든 걸! 보기 좋게 정리해보려고 합니다. 시작해볼게요!




06. Type ahead

목표

주어진 URL에서 json 파일을 기반으로 citystate 를 검색하여 우측에 population 정보를 표출한다.

이제껏 한 시리즈 중에서 가장 난도가 높았었습니다..!

URL에서 json 파일을 fetch 한 적도 없었고, 한글자 한글자 입력할때마다 하단에 일치하는 내용을 표시하는 것이 상당히 곤란했었습니다.

강의를 들으면 처음 알게된 내용 및 어떻게 구현이 되었는지 정리해볼게요.




Derek 및 Web Bos 구현 코드

const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';

const cities = [];

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

function findMatches(wordToMatch, cities){
  return cities.filter(place => {
    // here we need to figure out if the city or state matches what was searched.
    
    const regex = new RegExp(wordToMatch, 'gi');
    return place.city.match(regex) || place.state.match(regex);
  })
}

function numberWithCommas(x) {
    return x.toString().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.city.replace(regEx, `<span class="hl">${this.value}</span>`);
    return `
      <li>
        <span class = "name">${cityName}, ${stateName}</span>
        <span class = "population">${numberWithCommas(place.population)}</span>
      </li>
    `;
  });

  suggestions.innerHTML = html.join("");
}

const searchInput = document.querySelector(".search");
const suggestions = document.querySelector(".suggestions");

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

상당히 길죠..😅 걱정마세요! 찬찬히 하나하나 정리해볼거라구요!


1. Fetch 으로 데이터 추출

가장 선행된 작업은 제공된 URL의 데이터를 전역 변수에 저장하는 일이였습니다.

const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';

const cities = [];

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

이와 관련해서, fetch 문법을 정리해보겠습니다.

MDN 에 따르면, fetch 의 정의는 아래와 같습니다.

Fetch API는 네트워크 통신을 포함한 리소스 취득을 위한 인터페이스가 정의되어 있습니다. XMLHttpRequest와 같은 비슷한 API가 존재합니다만, 새로운 Fetch API는 좀더 강력하고 유연한 조작이 가능합니다.

너무 사전적인 정의만 기술한 듯 하여, 더 직관적인 정의 및 예제를 보여드릴게요.

fetch()를 호출하면 브라우저는 네트워크 요청을 보내고 promise가 반환됩니다. 반환되는 프라미스는 fetch()를 호출하는 코드에서 사용됩니다.

기본적인 문법은,

let promise = fetch(url, [options])

형식입니다.

사실 promise 에 대해서 아직 정확하게 알지 못해서, 실질적으로 쓸 수 있는 예제를 들어볼까해요. promise 에 관한 공부는 추후에 따로 하여 업로드하겠습니다.

let url = 'https://api.github.com/repos/javascript-tutorial/ko.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // 응답 본문을 읽고 JSON 형태로 파싱함

alert(commits[0].author.login);

위 예제를 단순히 crtl+shift+i 눌러서 console 창에 입력하면, 바로 결과가 alert 로 뜨는 것을 확인 할 수 있습니다.

비동기 패턴 처리인 await 으로 fetch 를 기다렸다가 결과를 response 에 받고, 이를 .json() 함수로 파싱을 합니다. 이를 commit 에 집어넣습니다.

이는 더 간략하게 구현할 수 있습니다.

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

promisethen 을 쓰듯이 사용하는 느낌입니다.
<이 모든 내용은 모던 자바스크립트 튜토리얼을 참고했습니다.>

이를 참고하면, 제가 fetch 한 코드가 한결 잘 보일거에요 :)

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

상단에서 정의된 endpoint 에는 URL 이 담겨져 있고, 이를 fetch 하고, .then 으로 .json() 을 통해 파싱을 하고, dataspread 해서 cities 라는 배열에 집어넣었습니다.

많이.. 복잡합니다! 사실 저도 promise 및 비동기적인 처리가 익숙하지 않아서 떠듬떠듬 이해되는 방식이라, 완벽하지 못한 설명인것 같아 죄송해요. 🥴


여기까지 진행되었다면, 이제 cities 배열에는 1000개의 정보가 들어가 있을거에요! 대략 보여드리면,
이런 형식의 데이터가 객체 배열 형식으로 저장되어 있을겁니다 :)

그럼 계속해볼게요.

2. addEventListener 함수 구현

다음은 입력창에 타자로 입력시 발생하는 이벤트 처리입니다.

const searchInput = document.querySelector(".search");
const suggestions = document.querySelector(".suggestions");

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

이번 프로젝트에선 addEventListener 부분은 그나마 간단해요.

제가 필요한 부분을 querySelector 로 골라오고, 이를 changekeyup 으로 등록하였습니다.

change 이벤트는 요소 변경이 끝나면 발생합니다. 특히, 이번 프로젝트에서 input 같은 경우에는 요소 변경이 끝날 때가 아니라 포커스를 잃을 때 이벤트가 발생합니다.

즉, 검색창에 냅다 다 쓰고 마우스로 다른곳을 눌렀을 때 이벤트가 발생하는 원리에요.

keyup 이벤트는 말 그대로 타자를 입력할때마다 이벤트가 발생하는 것입니다. 이를 보시면 한 글자 한 글자 쓸때마다 계속 검색이되는, 이벤트가 발생함을 확인 할 수 있어요 :)

이렇게, 두 가지 changekeyup 이벤트는 displayMatches 함수를 호출해요!

displayMatches 함수는 바로 findMatches 함수를 호출하는데, 이 두 함수를 같이 살펴볼게요. 😳

3. displayMatchesfindMatches 함수

displayMatchesfindMatches 는 같이 설명드리는게 좋을 것 같아요.

먼저 displayMatches 는 처음부터 findMatches 함수를 호출하게 됩니다.

function displayMatches(){
  const matchArray = findMatches(this.value, cities);
  ...
}

이런식으로 이벤트가 발생하여 displayMatches 함수를 실행하면 이벤트의 주체인 searchInputthis 를 가지고 findMatches 를 호출해요.

findMatches 함수를 먼저 살펴볼게요.

function findMatches(wordToMatch, cities){
  return cities.filter(place => {
    // here we need to figure out if the city or state matches what was searched.
    
    const regex = new RegExp(wordToMatch, 'gi');
    return place.city.match(regex) || place.state.match(regex);
  })
}

이 함수는 입력받은, 즉 사용자가 검색창에 쓴 내용을 wordToMatch 변수에 저장하여 실행됩니다. cities 배열을 모두 돌면서 wordToMatch 와 일치하는 친구들을 죄다 찾을거에요.

해당 작업을 하는데 필요한 개념은 바로, RegExp 입니다.

정규표현식

깔끔하게 이해가도록 정의를 해드리면,

정규표현식(Regular Expression)은 문자열을 처리하는 방법 중의 하나로 특정한 조건의 문자를 검색하거나 치환하는 과정을 매우 간편하게 처리 할 수 있도록 하는 수단입니다.

즉, 문자열을 찾을때 쓰이는 친구다, 라고 생각하면 될 것 같습니다.

정규표현식을 정의할 땐 2가지 방법이 있습니다.

var pattern = /a/ 

var pattern = new RegExp('a');

두 구문 모두 a 를 찾을때 사용되는 것인데요, 아마 두번째 선언문이 훨씬 유용하게 쓰일 것 같네요 :)

더 자세한 예제 보여드릴게요.

// RegExp.exec() => 필요한 정보(문자)를 추출하는데 목적 

console.log(pattern.exec('abcdef')); // ["a"] 
// 실행결과는 문자열 a를 값으로 하는 배열을 리턴한다. 

console.log(pattern.exec('bcdefg')); // null 
// 인자 'bcdef'에는 a가 없기 때문에 null을 리턴한다. 

// RegExp.test() => 패턴이 있는지 없는지를 테스트하는데 목적 
// test는 인자 안에 패턴에 해당되는 문자열이 있으면 true, 없으면 false를 리턴한다. 
console.log(pattern.test('abcdef')); // true 
console.log(pattern.test('bcdefg')); // false

이렇게 정규 표현식에 대해서만 사용할 수 도 있지만, string 과 함께 쓰일 수 있답니다.

string.match()

console.log('abcdef'.match(pattern)); // ["a"] 
console.log('bcdefg'.match(pattern)); // null

pattern 이라는 문자열이 string에 있는지 확인이 가능합니다.

string.replace()

var pattern = /a/; 
var str = "abcdef"; 
console.log(str.replace(pattern, 'A')); // Abcdef

이렇게, string에 대해서 치환도 가능해요.

또한, 옵션도 추가적으로 부여할 수 있습니다.

var xi = /a/; 
console.log("Abcde".match(xi)); // null 
var oi = /a/i; 
console.log("Abcde".match(oi)); // ["A"];

var xg = /a/; 
console.log("abcdea".match(xg)); // ["a"] 0번째 index 'a'가 출력
var og = /a/g; 
console.log("abcdea".match(og)); // ["a", "a"] 모든 'a' 출력

i 옵션은 insensitive로, 대문자 소문자 관련없이 검색을 하도록 하고, gglobal 로, 하나를 찾고 끝내지말고 모든 부분에서 찾는 옵션입니다.


따라서, 여기서는 이 정규 표현식을 사용하여 검색을 시작했습니다.

const regex = new RegExp(wordToMatch, 'gi');

입력받은 this.valuewordToMatch에 넣어 정규표현식으로 global 하고 insensitive 하게 검색했어요.

    return place.city.match(regex) || place.state.match(regex);

해당 찾은 regexmatch 로 사용해 citystateregex 와 같은 객체들을 반환합니다.


그렇게 하면, 이제 findMatchescities 배열에서 사용자가 누른 입력값에 따라 상응하는 결과를 반환하게 됩니다!

다시! findMatches 를 호출한 displayMatches 함수를 봐볼게요.

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.city.replace(regEx, `<span class="hl">${this.value}</span>`);
    return `
      <li>
        <span class = "name">${cityName}, ${stateName}</span>
        <span class = "population">${numberWithCommas(place.population)}</span>
      </li>
    `;
  });

  suggestions.innerHTML = html.join("");
}

matchArray에는 검색 결과인 객체들인 담긴 배열이 담깁니다!

해당 배열을 가지고 이제 화면상에 하단에 목록으로 표출되도록 작업해줍니다. map 함수를 사용해 모든 원소에 대해 어떤 작업을 하여 새로운 배열을 반환하도록 하게 했어요.

const regEx = new RegExp(this.value, "gi");
const cityName = place.city.replace(regEx, `<span class="hl">${this.value}</span>`);
const stateName = place.city.replace(regEx, `<span class="hl">${this.value}</span>`);

위의 regEx 는, 다시 검색한 내용을 바탕으로 노란색으로 하이라이트를 주기 위해서 (hl class를 가지는 span으로 만들기) 따로 추출하여 replace 를 한 과정입니다.

이 과정에서 만든 cityNamestateName 은 반환시 쓰이게 돼요.

return `
      <li>
        <span class = "name">${cityName}, ${stateName}</span>
        <span class = "population">${numberWithCommas(place.population)}</span>
      </li>
    `;

저는 이렇게 map 함수를 사용할때 백틱을 사용하여 반환하게 하여, 그것을 그대로

suggestions.innerHTML = html.join("");

이렇게 innerHTML 로 끼워 맞추는 생각이 굉장히 신박한 것 같더라구요. 자주봐서
익숙해지면 좋을 것 같습니다. 😃

여기서 join 함수에 대해서 잠깐 정리하면,

join() 메서드는 배열의 모든 요소를 연결해 하나의 문자열로 만듭니다.

join 함수를 통해 html 에 있던

<li>
        <span class = "name">${cityName}, ${stateName}</span>
        <span class = "population">${numberWithCommas(place.population)}</span>
</li>

이 뭉텅이들을 주루룩 연결 시켜주었습니다!


또한 이 함수는 조금 유명한 함수더라구요!

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

x 에 숫자를 넣으면 3글자씩 잘라서 , 를 붙여주는 함수라고 합니다. 이 친구는 그냥 끌어와서 썼어요 😅




진짜.. 너무 길고도.. 어려운 내용이였습니다!

열심히 정리해서 올리려고 했는데, 틈틈히 보면서 수정할 부분이 있으면 해놓아야겠어요. 방대하다보니 놓친 부분이 있지 않을까, 걱정되네요 ;(

틀린내용이나 수정할 내용이 있다면 언제든지 피드백 부탁드립니다!
감사합니다!🤗

profile
Whereof one cannot speak, thereof one must be silent.

0개의 댓글