본문 바로가기
내일배움캠프/Today I Learned

[TIL 2023.05.30] 자바스크립트 검색기능 (GOE’s CINEMA)

by 괴코딩 2023. 5. 31.

💻오늘 배운 내용


insertAdjacentHTML()

element.insertAdjacentHTML(position, text);

요소(element)의 내용을 변경하는 대신 HTML을 문서(document)에 삽입

요소 변경이 아닌 요소 삽입만을 해야 하는 상황

position의 값은 beforebegin, afterbegin, beforeend, afterend만 사용할 수 있다.

 

trim()

문자열 좌우에서 공백을 제거하는 함수

 

var a = " 가 나 다 라 마 "

a.trim()
//"가 나 다 라 마"

 

replace()

문자열에서 변경하려는 문자열이 여러 번 반복될 경우, 첫 번째로 발견한 문자열만 치환

apple, banana, banana' 이렇게 'banana'가 여러 번 반복될 경우

replace('banana', 'tomato')를 실행하면 첫번째로 발견된 'banana'만 치환되어

'apple, tomato, banana'의 결과를 얻게 됨

모든 문자열을 치환하려면

 

let str = 'apple, banana, orange, banana';
let replaced_str = str.replace(/banana/g, 'tomato');

document.write('변경 전 : ', str, '<br/>');
document.write('변경 후 : ', replaced_str, '<br/>');

 

파라미터로 들어가는 값이 정규식 임을 알려준다.

그리고, '/' 뒤에는 'g'라는 modifier를 붙였다. (g는 'global match'라는 의미로 사용)

 

❓발생한 이슈/고민


검색기능을 만드는데 하루종일 걸렸다

  1. 먼저 페이지 내 요소에대해 검색 기능을 만들고
  2. 자동완성 기능을 추가
  3. 띄어쓰기에 상관없이 검색가능하도록 구현 해보았다

 

💡해결과정


 

가장 처음 구성해본 검색기능

 

for (let i = 0; i < movies.length; i++) {
  document.querySelectorAll('#movies')[i].innerText = movies[i].title;
}

function searchFilter(data, search) {
  // data 값을 하나하나 꺼내와서
  return data.map((d) => {
    // 만약 해당 데이터가 search 값을 가지고 있다면 리턴한다.
    if (d.includes(search)) {
      return d;
    }
  });
}

// search 버튼 클릭 시 호출되는 함수
function search() {
  // 폼에 입력된 값

  let text = document.getElementById('input').value;

  // res [undefined, {id:, name: favorites:}, undefined] 이런식으로 리턴
  // 따라서 undefined 값을 제거해줘야하기 때문에 filter 메소드 적용
  let res = searchFilter(movies, text).filter((d) => d !== undefined);

  // 결과 값 화면 출력
  document.getElementById('movies').innerText = res.join(', ');
}

// 클릭 시 search 함수 호출
document.getElementById('btn').addEventListener('click', search);

 

for문과 map 메소드를 이용하여 야심차게 구성했지만

검색이 동작하지 않는 문제발생검색이 동작하지 않는 문제발생

 

for문에서 innerText = movies[i].title이 아니라 result의 배열을 사용하도록 수정해야할 것 같다.

또한 생성될 영화 카드는 템플릿 리터럴을 이용하여 한눈에 보이도록 가독성을 높임

 

for (let i = 0; i < response.results.length; i++) {
  document.querySelectorAll('.movieName')[i].innerText =
    response.results[i].title;
}

function searchFilter(data, search) {
  return data.filter((d) => d.title.includes(search));
}

function search() {
  let text = document.getElementById('input').value;
  let res = searchFilter(response.results, text);

  document.getElementById('movies').innerHTML = '';

  res.forEach((movie) => {
    let template = `<div class="movie">
                      <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                      <h2 class="movieName">${movie.title}</h2>
                      <p class="movieSum">${movie.overview}</p>
                      <p class="movieRate">평점 ${movie.vote_average}</p>
                      
                    </div>`;

    document.querySelector('#movies').insertAdjacentHTML('beforeend', template);
  });
}

document.getElementById('btn').addEventListener('click', search);

 

기본적인 검색은 가능했지만 버튼이 아니라 엔터를 쳤을때 값이 검색이 실행되도록 하고 싶었다.

 

3번째 수정

 

// 초기화 및 이벤트 리스너 설정
function initialize() {
  fetchMovies();
  document
    .getElementById('input')
    .addEventListener('keydown', function (event) {
      if (event.key === 'Enter') {
        search();
      }
    });
}

initialize();

 

성공

 

검색기능 4차 수정 실시간반영 시도

 

// 검색어 입력 이벤트를 감지하여 실시간으로 검색 실행
function handleSearchInput() {
  const searchInput = document.getElementById('input');
  searchInput.addEventListener('input', function () {
    search();
  });
}

 

검색어 실시간 반영함수 추가 인풋창에 어떤 값이 입력되는것을 감지하면 바로바로 search 함수를 호출하게 했다.

반영은 되지만 기존카드 + 검색된 카드 중복생성되는 문제가 발생

 

검색 5차 수정

 

// 검색 버튼 클릭 시 호출되는 함수
function search() {
  const searchInput = document.getElementById('input');
  const searchText = searchInput.value.trim().toLowerCase();
  const moviesContainer = document.getElementById('movies');

  // 기존 영화 카드 삭제
  moviesContainer.innerHTML = '';

  if (searchText === '') {
    // 검색어가 없는 경우, 모든 영화를 다시 표시
    movies.forEach((movie) => {
      const template = `<div class="movie">
                          <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                          <h2 class="movieName">${movie.title}</h2>
                          <p class="movieSum">${movie.overview}</p>
                          <p class="movieRate">평점 ${movie.vote_average}</p>
                        </div>`;

      moviesContainer.insertAdjacentHTML('beforeend', template);
    });
    return;
  }

  const filteredMovies = searchFilter(movies, searchText);

  filteredMovies
    .forEach((movie) => {
      const template = `<div class="movie">
                          <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                          <h2 class="movieName">${movie.title}</h2>
                          <p class="movieSum">${movie.overview}</p>
                          <p class="movieRate">평점 ${movie.vote_average}</p>
                        </div>`;

      moviesContainer.insertAdjacentHTML('beforeend', template);
    })
    .catch((error) => console.error(error));
}

 

성공!

 

  1. 검색 결과를 표시하는 부분에서, 기존에는 fetchMovies() 함수를 호출하여 모든 영화를 표시한 다음, 검색 결과에 맞는 영화 카드만 따로 표시했었는데,
  2. 이를 수정하여 검색 결과에 따라 필터링된 영화 카드만 표시하도록 변경.
  3. 이를 위해 searchFilter() 함수를 사용하여 검색어에 맞는 영화를 필터링하고, 필터링된 영화 카드를 반복적으로 생성하여 표시하도록 수정.

 

이렇게 변경된 코드는 검색어 입력 시 실시간으로 결과를 반영하고, 중복된 카드가 생성되지 않도록 한다.

추가로 띄어쓰기에 상관없게!

 

function searchFilter(data, search) {
  const searchKeywords = search.toLowerCase().split(' ');

  return data.filter((movie) => {
    const movieTitle = movie.title.toLowerCase().replace(/\\s/g, '');

    return searchKeywords.every((keyword) => movieTitle.includes(keyword));
  });
}

 

먼저, searchFilter 함수 내부에서는 검색어를 소문자로 변환하고,

split 메서드를 사용하여 띄어쓰기를 기준으로 검색어를 여러 개의 키워드로 분리한다.

이렇게 분리된 키워드들은 searchKeywords 배열에 저장.

그 후, filter 메서드를 사용하여 데이터 배열을 필터링하는데,

각 영화의 제목을 소문자로 변환하고, replace 메서드를 이용하여 제목 내의 모든 공백을 제거.

이렇게 만들어진 movieTitle은 영화 제목을 공백 없이 이어붙인 문자열.

마지막으로, searchKeywords 배열의 각 키워드를 순회하면서,

every 메서드를 사용하여 모든 키워드가 movieTitle에 포함되는지 확인한다.

모든 키워드가 포함되는 경우에만 true를 반환하고, 그 외의 경우는 false를 반환하여 해당 영화를 필터링.

이렇게 하면 검색어의 키워드들이 영화 제목에 띄어쓰기에 상관없이

키워드에 포함되어 있는 경우에는 해당하는 영화들을 찾을 수 있게 된다.

 

그렇게 어찌저찌 완성한 코드가

 

let movies = []; // 영화 데이터를 저장할 배열

function fetchMovies() {
  fetch(
    'https://api.themoviedb.org/3/movie/upcoming?language=ko&page=1',
    options
  )
    .then((response) => response.json())
    .then((response) => {
      movies = response.results; // 영화 데이터를 배열에 저장

      movies.forEach((movie) => {
        let template = `<div class="movie">
                          <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                          <h2 class="movieName">${movie.title}</h2>
                          <p class="movieSum">${movie.overview}</p>
                          <p class="movieRate">평점 ${movie.vote_average}</p>
                        </div>`;

        document
          .querySelector('#movies')
          .insertAdjacentHTML('beforeend', template);
      });
    })
    .catch((err) => console.error(err));
}

// 검색어를 사용하여 영화를 필터링하는 함수
function searchFilter(data, search) {
  const searchKeywords = search.toLowerCase().split(' ');

  return data.filter((movie) => {
    const movieTitle = movie.title.toLowerCase().replace(/\s/g, '');

    return searchKeywords.every((keyword) => movieTitle.includes(keyword));
  });
}

// 검색 버튼 클릭 시 호출되는 함수
function search() {
  const searchInput = document.getElementById('input');
  const searchText = searchInput.value.trim().toLowerCase();
  const moviesContainer = document.getElementById('movies');

  // 기존 영화 카드 삭제
  moviesContainer.innerHTML = '';

  if (searchText === '') {
    // 검색어가 없는 경우, 모든 영화를 다시 표시
    movies.forEach((movie) => {
      const template = `<div class="movie">
                          <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                          <h2 class="movieName">${movie.title}</h2>
                          <p class="movieSum">${movie.overview}</p>
                          <p class="movieRate">평점 ${movie.vote_average}</p>
                        </div>`;

      moviesContainer.insertAdjacentHTML('beforeend', template);
    });
    return;
  }

  const filteredMovies = searchFilter(movies, searchText);

  filteredMovies
    .forEach((movie) => {
      const template = `<div class="movie">
                          <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                          <h2 class="movieName">${movie.title}</h2>
                          <p class="movieSum">${movie.overview}</p>
                          <p class="movieRate">평점 ${movie.vote_average}</p>
                        </div>`;

      moviesContainer.insertAdjacentHTML('beforeend', template);
    })
    .catch((error) => console.error(error));
}

// 검색어 입력 이벤트를 감지하여 실시간으로 검색 실행
function handleSearchInput() {
  const searchInput = document.getElementById('input');
  searchInput.addEventListener('input', function () {
    search();
  });
}

// 초기화 및 이벤트 리스너 설정
function initialize() {
  fetchMovies();
  handleSearchInput();
  document
    .getElementById('input')
    .addEventListener('keydown', function (event) {
      if (event.key === 'Enter') {
        event.preventDefault(); // 기본 동작 방지
        search();
      }
    });
}

initialize();

 

 

…이것인데…이게 전체 코든데.. let template만 3번이나 반복되는 것이 상당히 비효율적이라고 느낌

 

수정한 코드

 

function fetchMovies() {
  fetch(
    'https://api.themoviedb.org/3/movie/upcoming?language=ko&page=1',
    options
  )
    .then((response) => response.json())
    .then((response) => {
      movies = response.results; // 영화 데이터를 배열에 저장
      displayMovies(movies); // 영화 카드 표시 함수 호출
    })
    .catch((err) => console.error(err));
}

// 영화 카드를 표시하는 함수
function displayMovies(movies) {
  const moviesContainer = document.getElementById('movies');

  movies.forEach((movie) => {
    const template = `<div class="movie">
                        <img src="https://image.tmdb.org/t/p/w500${movie.poster_path}" alt="" />
                        <h2 class="movieName">${movie.title}</h2>
                        <p class="movieSum">${movie.overview}</p>
                        <p class="movieRate">평점 ${movie.vote_average}</p>
                      </div>`;

    moviesContainer.insertAdjacentHTML('beforeend', template);
  });
}

// 검색어를 사용하여 영화를 필터링하는 함수
function searchFilter(data, search) {
  const searchKeywords = search.toLowerCase().split(' ');

  return data.filter((movie) => {
    const movieTitle = movie.title.toLowerCase().replace(/\s/g, '');

    return searchKeywords.every((keyword) => movieTitle.includes(keyword));
  });
}

// 검색 버튼 클릭 시 호출되는 함수
function search() {
  const searchInput = document.getElementById('input');
  const searchText = searchInput.value.trim().toLowerCase();
  const moviesContainer = document.getElementById('movies');

  // 기존 영화 카드 삭제
  moviesContainer.innerHTML = '';

  if (searchText === '') {
    // 검색어가 없는 경우, 모든 영화를 다시 표시
    displayMovies(movies);
    return;
  }

  const filteredMovies = searchFilter(movies, searchText);
  displayMovies(filteredMovies);
}

 

수정된 코드에서는 displayMovies 함수를 도입하여 영화 카드를 표시하는 부분을 별도의 함수로 분리했다.

그리고 검색 관련 로직도 변경되었으며, displayMovies 함수를 호출하여 영화 카드를 표시한다.

 

📋레퍼런스


https://yeomss.tistory.com/65 검색기능

반응형