TIL #12 개인 프로젝트 피드백 수정

DO YEON KIM·2024년 4월 30일
0

부트캠프

목록 보기
12/72

하루 하나씩 작성하는 TIL #12


이번 TIL에선 튜터님의 피드백을 수용하여 코드를 수정해보았다.


github에도 업로드 해놓긴 했지만 보기 수월하게 전체 코드를 작성해두겠다.

수정 전 코드이며 수정본은 js만 수정하였기 때문에 마지막에 전체 코드를 업로드 하였다.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>A03조 김도연 영화 검색 사이트</title>
  <link rel="stylesheet" href="styles.css">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/typeit/dist/typeit.min.js"></script>

</head>

<body>

  <header>
    <p id="multipleStrings"></p>
  </header>

  <div class="search-container">
    <input type="text" id="searchInput" placeholder="영화 제목을 입력해주세요" autofocus>
    <button id="searchButton">Search</button>
  </div>
  <div id="movieContainer" class="movie-container"></div>
  <footer>
    <div class="footer-content">
      <a href="https://github.com/eldoradodo">
        <img class="footer-img" src="img\git.png" alt="GitHub">
      </a>
      <a href="https://velog.io/@eldoradodo/posts">
        <img class="footer-img" src="img\velog.jpg" alt="Velog">
      </a>
    </div>
  </footer>

  <script src="movie.js"></script>
</body>

</html>

styles.css

header {
  text-align: center;
  background-color: #9a95ff;
  padding: 20px 0;
  /*위아래 여백*/
}

/*헤더 글꼴*/
header p#multipleStrings {
  font-family: "Black Han Sans", sans-serif;
  font-weight: 300;
  font-size: 35px;
  color: white;
  font-style: normal;
}


/* Flex 사용 */
.search-container {
  display: flex;
  justify-content: center;
  /*수평 가운데 정렬*/
  margin-bottom: 20px;
}

/* Grid 사용 */
.movie-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  /*그리드 컨테이너의 열 크기 지정. 그리드 셀의 수 자동으로 조정, (셀의 최소크기, 최대 크기(가능한 늘리기))*/
  gap: 20px;
  /*그리드 셀 사이 간격*/
}

/* 카드 스타일 */
.movie-card {
  border: 1px solid #7169ff;
  border-radius: 8px;
  border-width: 5px;
  padding: 10px;
  background-color: #9a95ff;
}

.movie-card img {
  width: 100%;
  border-radius: 8px;
}

.movie-card h3 {
  margin: 10px 0;
}

/* 알림 스타일 */
.alert {
  background-color: #9a95ff;
  color: #333;
  padding: 15px;
  border-radius: 8px;
  margin: 20px auto;
  width: fit-content;
  /*알림의 크기 너비에 맞게 자동 조절*/
}

.search-container {
  margin-top: 30px;
  margin-bottom: 30px;
}

.movie-container {
  margin-bottom: 30px;
  /* 카드들 아래에 간격 추가 */
}

/* 서치 버튼 스타일 */
#searchButton {
  background-color: #9a95ff;
  color: white;
  border-color: black;
  padding: 10px 20px;
  /* 내부 여백 */
  text-align: center;
  text-decoration: none;
  font-size: 16px;
  border-radius: 20px;
  margin-left: 10px;
  /* 텍스트 박스와의 간격 */
}

/*텍스트박스 스타일*/
#searchInput {
  width: 300px;
  /* 텍스트 박스 너비 조절 */
  border-radius: 10px;
}

/* 서치 컨테이너 스타일 */
.search-container {
  display: flex;
  justify-content: center;
  margin-bottom: 30px;
  /* 텍스트 박스와 서치 버튼 사이의 간격 */
}


footer {
  background-color: #9a95ff;
  /* 연노랑색 배경 */
  padding: 30px;
  text-align: right;
}


.footer-content {
  display: flex;
  justify-content: flex-end;
}

.footer-content a {
  margin-left: 10px;
}

.footer-img {
  width: 40px;
  border-radius: 50%;
  /* 모서리를 둥글게 함 */
}

/*선택 요구 사항*/
/*flex, grid 사용 O*/
/*순수 css 사용 O*/

movie.js(수정 전 ver)

document.addEventListener('DOMContentLoaded', function () {
  const options = {
    method: 'GET',
    headers: {
      accept: 'application/json',
      Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A'
    }
  };

  // 영화 정보 불러오기
  fetch('https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1', options)
    .then(response => response.json())
    .then(data => displayMovies(data.results))
    .catch(err => console.error(err));
  // 14번줄까지 tdmb 세팅 코드. 


  //html 요소 가져오기
  const searchInput = document.getElementById('searchInput');
  const searchButton = document.getElementById('searchButton');
  const movieContainer = document.getElementById('movieContainer');

  // 검색 입력란에 포커스
  searchInput.focus();

  // 검색 버튼 눌렀을 때 이벤트 처리
  searchButton.addEventListener('click', () => {
    const query = searchInput.value.trim().toLowerCase();
    if (query !== '') {
      searchMovies(query);
    }
  });

  // 검색 함수
  function searchMovies(query) {
    const apiKey = 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A';
    const apiUrl = `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&query=${query}&page=1&include_adult=false`;

    fetch(apiUrl, options)
      .then(response => response.json())
      .then(data => {
        displayMovies(data.results);
      })
      .catch(error => console.log('Error:', error));
  }

  // 영화 정보를 화면에 표시하는 함수
  function displayMovies(movies) {
    movieContainer.innerHTML = '';

    movies.forEach(movie => { //요구사항 - forEach 사용
      const {
        id,
        title,
        overview,
        poster_path,
        vote_average
      } = movie;
      const imageUrl = poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : 'https://via.placeholder.com/150';

      const movieCard = document.createElement('div');
      movieCard.classList.add('movie-card');
      movieCard.innerHTML = `
        <img src="${imageUrl}" alt="${title}">
        <h3>${title}</h3>
        <p>${overview}</p>
        <p>평점: ${vote_average}</p>
      `;
      movieCard.addEventListener('click', () => {
        movies.map(movie => { //요구사항 - map 사용
          if (movie.id === id) {
            alert(`선택한 영화 ID: ${movie.id}`);
          }
        });
      });
      movieContainer.appendChild(movieCard);
    });
  }

  // Enter 키로 검색 실행
  searchInput.addEventListener('keypress', function (e) {
    if (e.key === 'Enter') {
      searchButton.click();
    }
  });

  // TypeIt 효과 적용
  new TypeIt("#multipleStrings", {
    strings: ["내일 배움 캠프", "영화 검색 사이트"],
    speed: 50,
    waitUntilVisible: true,
  }).go();
});


// let, const 만을 사용하여 변수 선언 O
// 화살표 함수 사용 O
// 배열 메소드 예시 중 2개 이상 사용 O (forEach, map)
// DOM 제어 - 예시 목록 중 2개 이상 사용  O (document.addEventListener, document.createElement)
/*대소문자 관계 없이 검색 가능하도록 하기 O*/
/*키보드 엔텈키를 입력해도 검색버튼 클릭한 것과 동일하게 검색 실행 O*/

피드백의 내용

  • API key, URL 정보 등을 별도의 파일로 분리하여 관리하면 프로젝트가 커졌을 때 유지보수가 수월하실 것 같습니다.

  • 70번줄은 map없이 movie.id를 바로 사용해주셔도 될 것 같습니다.(배열 메소드 2개 이상 사용으로 인해 map을 사용한것으로 보이네요~)

  • 커밋 내역은 아쉽습니다. 다른 사람에게 작업 내용을 알려준다고 생각해주시면 좋을 것 같습니다.

  • 48번째 줄 displayMovies 함수에서 movies 배열을 순회하면서 movieContainer 요소에 자식 요소를 하나씩 붙입니다. DOM은 기본적으로 변경이 생기면 reflow 비용이 발생합니다. 이를 방지하기 위해 movieCard를 전부 만드신 후에 movieContainer 붙이시면 성능 향상에 도움이 됩니다.

  • keyword: reflow, createDocumentFragment


키워드 내용을 메인으로 수정하고 commit 내역은 ,,, 너무 대충썼었다. 수정 후 push할 때 구체적으로 작성해보고자한다.

reflow

웹 브라우저가 렌더링 엔진을 통해 DOM 요소의 위치, 크기, 스타일 등의 변경 사항을 반영하여 화면 에 다시 그리는 과정을 말한다.

이 과정에서 브라우저는 요소의 크기와 위치를 다시 계산하고, 이를 화면에 반영한다.

비용이 많이 드는 작업 중 하나로, 성능에 영향을 미친다.

Document Fragment

실제 DOM 트리에 추가되지 않으면서 여러 개의 노드를 포함할 수 있는 가상의 노드 컨테이너이다.

여러 노드를 처리할 때 일반적으로 사용되며, 한 번에 실제 DOM에 추가할 때 Reflow를 최소화하여 성능을 향상시킨다.

위 코드를 수정함으로서 기존에는 각 moviecard를 생성할 때마다 실제 dom이 추가되었지만 이제는 documentFragement에 추가하여 한 번에 dom에 추가함으로서 reflow 비용을 최소화 하였다.


문제의 48번째 코드 수정 전

  // 영화 정보를 화면에 표시하는 함수
  function displayMovies(movies) {
    movieContainer.innerHTML = '';

    movies.forEach(movie => { //요구사항 - forEach 사용
      const {
        id,
        title,
        overview,
        poster_path,
        vote_average
      } = movie;
      const imageUrl = poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : 'https://via.placeholder.com/150';

      const movieCard = document.createElement('div');
      movieCard.classList.add('movie-card');
      movieCard.innerHTML = `
        <img src="${imageUrl}" alt="${title}">
        <h3>${title}</h3>
        <p>${overview}</p>
        <p>평점: ${vote_average}</p>
      `;
      movieCard.addEventListener('click', () => {
        movies.map(movie => { //요구사항 - map 사용
          if (movie.id === id) {
            alert(`선택한 영화 ID: ${movie.id}`);
          }
        });
      });
      movieContainer.appendChild(movieCard);
    });
  }

수정 후

  // 영화 정보를 화면에 표시하는 함수
  function displayMovies(movies) {
    // documentFragment를 생성.

    movieContainer.innerHTML = '';
    const fragment = document.createDocumentFragment();

    movies.forEach(movie => {
      const {
        id,
        title,
        overview,
        poster_path,
        vote_average
      } = movie;
      const imageUrl = poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : 'https://via.placeholder.com/150';

      const movieCard = document.createElement('div');
      movieCard.classList.add('movie-card');
      movieCard.innerHTML = `
        <img src="${imageUrl}" alt="${title}">
        <h3>${title}</h3>
        <p>${overview}</p>
        <p>평점: ${vote_average}</p>
      `;
      movieCard.addEventListener('click', () => {
        movies.map(movie => {
          if (movie.id === id) {
            alert(`선택한 영화 ID: ${movie.id}`);
          }
        });
      });

    // movieContainer에 한 번에 fragment를 추가.
    movieContainer.appendChild(fragment);
  };
                   
                     // fragment에 movieCard를 추가.
      fragment.appendChild(movieCard);                    //fragment 사용 (피드백 반영 수정 부분)
    });

    // movieContainer에 한 번에 fragment를 추가.
    movieContainer.appendChild(fragment);
  };

수정 내용

movieContainer.innerHTML = '';

vs

const fragment = document.createDocumentFragment();

이전 코드에서는 movieContainer의 내용을 지우고(innerHTML = ''), 각각의 영화 정보를 새로운 요소로 대체했다.

수정된 코드에서는 document.createDocumentFragment()를 사용하여 빈 fragment를 생성 후 각각의 영화 정보를 fragment에 추가한 후, 한 번에 movieContainer에 추가한다.


movieContainer.appendChild(movieCard);

vs

fragment.appendChild(movieCard);

이전 코드에서는 각각의 영화 정보를 생성할 때마다 movieContainer에 직접 추가했지만, 수정된 코드에서는 각각의 영화 정보를 fragment에 추가한 후, 한 번에 movieContainer에 추가한다.

이렇게 하면 Reflow 비용을 최소화할 수 있다.


  • 수정 후 검색 버튼을 눌러도 엔터를 눌러도 입력 값에 대한 영화가 뜨지 않았다. input과 button은 수정 조차 하지 않았는데도 이런 에러가 생겼었는데, 알고보니
movieContainer.innerHTML = '';

초기화를 해주지않아 초기 화면 밑으로 검색 결과가 출력되고 있었다.


피드백 수정 끝!

  • 최종 코드
document.addEventListener('DOMContentLoaded', function () {
  const options = {
    method: 'GET',
    headers: {
      accept: 'application/json',
      Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A'
    }
  };

  // 영화 정보 불러오기
  fetch('https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1', options)
    .then(response => response.json())
    .then(data => displayMovies(data.results))
    .catch(err => console.error(err));
  // 14번줄까지 tdmb 세팅 코드. 


  //html 요소 가져오기
  const searchInput = document.getElementById('searchInput');
  const searchButton = document.getElementById('searchButton');
  const movieContainer = document.getElementById('movieContainer');

  // 검색 입력란에 포커스
  searchInput.focus();

  // 검색 버튼 눌렀을 때 이벤트 처리
  searchButton.addEventListener('click', () => {
    const query = searchInput.value.trim().toLowerCase();
    if (query !== '') {
      searchMovies(query);
    }
  });

  // 검색 함수
  function searchMovies(query) {
    const apiKey = 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A';
    const apiUrl = `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&query=${query}&page=1&include_adult=false`;

    fetch(apiUrl, options)
      .then(response => response.json())
      .then(data => {
        displayMovies(data.results);
      })
      .catch(error => console.log('Error:', error));
  }

  // 영화 정보를 화면에 표시하는 함수
  function displayMovies(movies) {
    // documentFragment를 생성.

    movieContainer.innerHTML = '';
    const fragment = document.createDocumentFragment();

    movies.forEach(movie => {
      const {
        id,
        title,
        overview,
        poster_path,
        vote_average
      } = movie;
      const imageUrl = poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : 'https://via.placeholder.com/150';

      const movieCard = document.createElement('div');
      movieCard.classList.add('movie-card');
      movieCard.innerHTML = `
        <img src="${imageUrl}" alt="${title}">
        <h3>${title}</h3>
        <p>${overview}</p>
        <p>평점: ${vote_average}</p>
      `;
      movieCard.addEventListener('click', () => {
        movies.map(movie => {
          if (movie.id === id) {
            alert(`선택한 영화 ID: ${movie.id}`);
          }
        });
      });

      // fragment에 movieCard를 추가.
      fragment.appendChild(movieCard);                    //fragment 사용 (피드백 반영 수정 부분)
    });

    // movieContainer에 한 번에 fragment를 추가.
    movieContainer.appendChild(fragment);
  };

  // Enter 키로 검색 실행
  searchInput.addEventListener('keypress', function (e) {
    if (e.key === 'Enter') {
      searchButton.click();
    }
  });

  // TypeIt 효과 적용
  new TypeIt("#multipleStrings", {
    strings: ["내일 배움 캠프", "영화 검색 사이트"],
    speed: 50,
    waitUntilVisible: true,
  }).go();
});


// let, const 만을 사용하여 변수 선언 O
// 화살표 함수 사용 O
// 배열 메소드 예시 중 2개 이상 사용 O (forEach, map)
// DOM 제어 - 예시 목록 중 2개 이상 사용  O (document.addEventListener, document.createElement)
/*대소문자 관계 없이 검색 가능하도록 하기 O*/
/*키보드 엔터키를 입력해도 검색버튼 클릭한 것과 동일하게 검색 실행 O*/
profile
프론트엔드 개발자를 향해서

0개의 댓글