Skip to content

Conversation

@ruddnjs3769
Copy link

@ruddnjs3769 ruddnjs3769 commented May 10, 2023

Vanilla JS로 OMDB API 영화검색 사이트 만들기

💬[데모] : (https://frolicking-trifle-07d151.netlify.app/)

사이트 설명

HOME

  • 영화를 검색할 수 있습니다.
  • 출시년도와 영화 타입을 선택하여 검색할 수 있습니다.
  • 검색 후 enter 입력 혹은 search 버튼 클릭 시 영화 포스터가 출력됩니다.
  • 포스터에 hover 시에 제목과 출시년도가 나타납니다.
  • 포스터 클릭 시 상세 영화정보 페이지로 이동합니다.

INFO

  • 상세 영화정보를 보여줍니다.
  • 잘못된 주소를 입력받으면 찾는 영화정보가 없다는 이미지가 출력됩니다.
  • 기본 값은 존윅4입니다.

ABOUT

  • 허전해서 넣었습니다.
  • 사이트 로고와 작성자 이름, github 주소를 보여줍니다.

개요:

❗ 필수

  • 영화 제목으로 검색이 가능해야 합니다!
  • 검색된 결과의 영화 목록이 출력돼야 합니다!
  • 단일 영화의 상세정보(제목, 개봉연도, 평점, 장르, 감독, 배우, 줄거리, 포스터 등)를 볼 수 있어야 합니다!
  • 실제 서비스로 배포하고 접근 가능한 링크를 추가해야 합니다.

❔ 선택

  • 한 번의 검색으로 영화 목록이 20개 이상 검색되도록 만들어보세요.
  • 영화 개봉연도로 검색할 수 있도록 만들어보세요.
  • 영화 목록을 검색하는 동안 로딩 애니메이션이 보이도록 만들어보세요.
  • 무한 스크롤 기능을 추가해서 추가 영화 목록을 볼 수 있도록 만들어보세요.
  • 영화 포스터가 없을 경우 대체 이미지를 출력하도록 만들어보세요.
  • 영화 상세정보가 출력되기 전에 로딩 애니메이션이 보이도록 만들어보세요.
  • 영화 상세정보 포스터를 고해상도로 출력해보세요. (실시간 이미지 리사이징)
  • 차별화가 가능하도록 프로젝트를 최대한 예쁘게 만들어보세요.
  • 영화와 관련된 기타 기능도 고려해보세요.

파일 구조 및 기능

C:.
│  index.html
│  main.js :: 각 페이지가 #app 요소 안에 렌더링 되도록 DOMContentLoaded 이벤트를 추가합니다.
│  package-lock.json
│  package.json
│  README.md
│  style.scss
│
└─src
    │  app.js :: header 요소에 클릭 이벤트를 추가하고, 최초 실행 시 랜딩페이지를 불러옵니다.
    │  router.js :: route주소를 받아와 SPA 기능을 제어합니다.
    │
    ├─constants
    │      routeInfo.js :: route주소 정보와 경로를 담고 있습니다.
    │
    ├─core
    │      getDetail.js :: 영화의 세부정보를 fetch합니다.
    │      movieFetch.js :: 검색어에 맞는 영화 정보를 fetch합니다.
    │
    ├─pages :: 이 폴더 안의 js파일은 각 주소에 렌더링 될 요소를 결정합니다.
    │  │  about.js
    │  │  main.js :: createMovieList를 호출합니다.
    │  │  movieinfo.js
    │  │  notfound.js :: URL주소가 잘못됐을 때 렌더링됩니다.
    │  │
    │  └─styles
    │          about.scss
    │          main.scss
    │          movieinfo.scss
    │
    ├─scripts
    │  ├─infoPage
    │  │      createEl.js :: detailRes.js에서 받아온 데이터를 화면에 출력합니다.
    │  │      detailRes.js :: getDetail.js를 통해 받아온 데이터에서, 필요한 데이터만 꺼내어 저장합니다.
    │  │      infoRoute.js :: main화면에서 검색 결과 요소(영화목록 중 하나)를 선택 시, 해당 주소로 URL 전환을 담당합니다.
    │  │
    │  └─mainPage
    │          createMovieList.js :: 검색 시 이벤트와, 스크롤 시 추가 데이터 fetch 이벤트를 제어합니다.
    │          movieResult.js     :: API에서 받아온 영화 목록 데이터를 화면에 출력합니다.
    │
    └─utils
            navigate.js      :: URL주소가 바뀔 때 이벤트를 발송합니다.
            querySelector.js :: querySelector 메서드를 `$`로 단축합니다. 찾을 수 없을 시 오류처리도 포함합니다.

## 사용된 모듈
package :
    "eslint": "^8.38.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-prettier": "^4.2.1",
    "prettier": "^2.8.7",
    "sass": "^1.62.0",
    "vite": "^4.3.0"

🧨문제점 :

  • info페이지에서 뒤로가기 시, 메인 페이지가 초기화 됩니다. 검색 내역과 데이터가 그대로 남아있어야 사용자도 편하고, 데이터 처리량도 줄어들 것 같습니다. route이벤트 관련된 문제인 것 같은데, 해당 문제점 관련해 서칭중입니다.

  • 무한 스크롤 시 영화 정보가 없음에도, 스크롤을 올렸다가 내리면 이벤트 리스너가 계속 생성되는 현상이 있습니다. 최초 1회 영화정보 없음 표시가 나타나면, 이벤트리스너가 다시 생성되지 않는 로직이 추가되지 않았습니다.

🧠아쉬운 점 :

  • router 기능에 대해 100% 이해하지 못해, 기능을 따라할 수는 있어도, 수정하기는 힘들었습니다.
  • JS 기초 지식이 부족해 많은 기능을 구현하지 못했고, 작업하는 중간중간 생각나는 원하는 기능들을 포기하기도 했습니다.

🎈얻은 것들 :

  • JS에 대한 이해가 늘었습니다. 함수의 호이스팅, 객체와 배열, json, API 통신과 비동기방식 등 다양한 문법들을 실사용하면서 원리와 사용법을 숙지하게 됐습니다.

Copy link

@iamidlek iamidlek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다.
전체적으로 잘 구현해 주셨습니다.
README도 잘 작성해 주셨습니다.
라우터 등 여러모로 고민하신 부분이 있어 보여 좋은 것 같습니다.
commit도 나누려고 노력하신 모습이 보입니다.

commit 의 경우 메세지를 조금 더 신경써서 어떤 작업인지 알 수 있으면 좋을 것 같습니다.
src 밖에 scss나 main.js가 있는 부분은 일반적이진 않은 것 같습니다. src 안에서 관리해 주세요.
함수형으로 구현하셨는데 거의 class로 구현하신 것 같은 느낌이 들었습니다.
store와 같이 값을 가지고 있는 객체가 있어도 좋을 것 같습니다.

@@ -0,0 +1,6 @@
export const $ = (selector) => {
const result = document.querySelector(selector);
if (!(result instanceof HTMLElement)) return null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

querySelector 자체가 Element, 실패 시 null을 반환하기 때문에
결과를 바로 리턴을 주어도 될 것 같습니다.

main.js Outdated
import "./style.scss";
import { $ } from "~/utils/querySelector";

window.addEventListener("DOMContentLoaded", (e) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event 객체를 사용하지 않고 있기 때문에
e 를 작성하지 않으셔도 될 것 같습니다.

src/app.js Outdated
import Router from "~/router";

export default function App($container) {
this.$container = $container;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

할당할 필요가 없을 것 같습니다.

src/app.js Outdated
Comment on lines 11 to 16
const target = e.target.closest("a");
if (!(target instanceof HTMLAnchorElement)) return;

e.preventDefault();
const targetURL = e.target.href.replace(BASE_URL, "");
navigate(targetURL);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 변수 혹은 변수로 만들고 사용하지 않는 부분이 있는데
정확한 동작과 값을 파악하면서 구성하면 좋을 것 같습니다.

Suggested change
const target = e.target.closest("a");
if (!(target instanceof HTMLAnchorElement)) return;
e.preventDefault();
const targetURL = e.target.href.replace(BASE_URL, "");
navigate(targetURL);
e.preventDefault();
const target = e.target.closest("a");
if (!(target instanceof HTMLAnchorElement)) return;
navigate(target.href);

Comment on lines 7 to 12
const target = e.target.closest("a");
if(!(target instanceof HTMLAnchorElement)) return null;

e.preventDefault();
const targetURL = target.href.replace(BASE_URL,"");
navigate(targetURL);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동일하게 불필요한 변수 혹은
변수로 잡은 내용을 활용하고 있지 않은 것 같습니다.

Suggested change
const target = e.target.closest("a");
if(!(target instanceof HTMLAnchorElement)) return null;
e.preventDefault();
const targetURL = target.href.replace(BASE_URL,"");
navigate(targetURL);
e.preventDefault();
const target = e.target.closest("a");
if (!(target instanceof HTMLAnchorElement)) return;
navigate(target.href);

Comment on lines 42 to 47
const currentYear = new Date().getFullYear();
for(let i = currentYear; i >= 1985; i--) {
const option = document.createElement('option');
option.value = i;
option.innerText = i;
selectYear.appendChild(option);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for문 안에서 반복적으로 append 를 하는 것은 좋지 않을 것 같습니다.
주입해야 할 html을 string으로 만들고 한번 주입하는 것이 좋을 것 같습니다.

로 all year를 제거하고
한번만 주입하면 좋을 것 같습니다.

Suggested change
const currentYear = new Date().getFullYear();
for(let i = currentYear; i >= 1985; i--) {
const option = document.createElement('option');
option.value = i;
option.innerText = i;
selectYear.appendChild(option);
const selectYear = $('.select:last-child')
const currentYear = new Date().getFullYear()
const endYear = 1985
selectYear.insertAdjacentHTML(
'afterbegin',
Array.from({ length: currentYear - endYear + 1 }, (_, i) => {
if (i) {
return /*HTML*/ `<option>${currentYear - i}</option>`
} else {
return /*HTML*/ `<option>All Year</option><option>${
currentYear - i
}</option>`
}
}).join('')
)

import { $ } from "~/utils/querySelector.js";
import detailRes from "./detailRes"

export default async function createEl () {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

innerHTML을 사용하여 MovieInfo 에서 만드는 만큼
거기서 필요한 정보를 넣으면 좋을 것 같습니다.

틀을 만드는 부분 따로 만들고
다시 찾아서 거기에 필요한 정보를 넣는 방법은 힘든 방법인 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 함수를 movieinfo.js에서 작성하다가, 코드가 너무 길어져 따로 빼내는 과정을 거쳤습니다.
처음에는 무작정 함수로 구현하고 movieinfo에서 호출하는 식으로 작성했었는데, 리뷰를 보니 맞는 말씀이라 어떻게 수정할지 고민했습니다.
innerHTML 안에서 문자열 보간을 사용해서 수정해 봤습니다. 이렇게 하면 createEl.js가 필요 없어지고, 코드는 더 깔끔해진 것 같은데, 익숙치 않아서 시간이 좀 걸렸네요. 이렇게 작성하는 게 더 효율적인 방법일까요?

  this.render = async () => {
    const details = await detailRes()
    this.$container.innerHTML = `
      <main class="movieInfo">
        <div class="movieInfo__detail">
          <div class="detail__container">
            <div class="detail__poster">
              <img src="${
                details.Poster === 'N/A' || details.Response === 'False'
                  ? '/xboxdog.jpg'
                  : details.Poster.replace('SX300', 'SX700')
              }" alt="포스터를 못찾았어요.">
            </div>
            <div class="container__infos">
              <div class="detail__title">
                <h2>${details.Title}</h2>
              </div>
              <div class="detail__rating">
                <ul class="rating__container">
                  ${details.Ratings.map(
                    rating => `
                    <li class="rating__${rating.Source.replace(
                      /\s/g,
                      ''
                    ).toLowerCase()}">
                      ${
                        rating.Source === 'Internet Movie Database'
                          ? '<img src="/imdb.png" class="rating__imdb">'
                          : ''
                      }
                      ${
                        rating.Source === 'Rotten Tomatoes'
                          ? '<img src="/rotten.png" class="rating__rt">'
                          : ''
                      }
                      ${
                        rating.Source === 'Metacritic'
                          ? '<img src="/metacritic.png" class="rating__mt">'
                          : ''
                      }
                      ${rating.Value}
                    </li>
                  `
                  ).join('')}
                </ul>
              </div>
              <ul class="detail__info">
                <li class="info__plot"><h2>PLOT</h2><p>${details.Plot}</p></li>
                <li class="info__genre"><h2>GENRE</h2><p>${
                  details.Genre
                }</p></li>
                <li class="info__year"><h2>YEAR</h2><p>${details.Year}</p></li>
                <li class="info__director"><h2>DIRECTOR</h2><p>${
                  details.Director
                }</p></li>
                <li class="info__actors"><h2>ACTORS</h2><p>${
                  details.Actors
                }</p></li>
                <li class="info__runtime"><h2>RUNTIME</h2><p>${
                  details.Runtime
                }</p></li>
                <li class="info__released"><h2>RELEASED</h2><p>${
                  details.Released
                }</p></li>
                <li class="info__country"><h2>COUNTRY</h2><p>${
                  details.Country
                }</p></li>
                <li class="info__language"><h2>LANGUAGE</h2><p>${
                  details.Language
                }</p></li>
              </ul>
            </div>
          </div>
        </div>
      </main>
    `
  }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 함수를 movieinfo.js에서 작성하다가, 코드가 너무 길어져 따로 빼내는 과정을 거쳤습니다. 처음에는 무작정 함수로 구현하고 movieinfo에서 호출하는 식으로 작성했었는데, 리뷰를 보니 맞는 말씀이라 어떻게 수정할지 고민했습니다. innerHTML 안에서 문자열 보간을 사용해서 수정해 봤습니다. 이렇게 하면 createEl.js가 필요 없어지고, 코드는 더 깔끔해진 것 같은데, 익숙치 않아서 시간이 좀 걸렸네요. 이렇게 작성하는 게 더 효율적인 방법일까요?

  this.render = async () => {
    const details = await detailRes()
    this.$container.innerHTML = `
      <main class="movieInfo">
        <div class="movieInfo__detail">
          <div class="detail__container">
            <div class="detail__poster">
              <img src="${
                details.Poster === 'N/A' || details.Response === 'False'
                  ? '/xboxdog.jpg'
                  : details.Poster.replace('SX300', 'SX700')
              }" alt="포스터를 못찾았어요.">
            </div>
            <div class="container__infos">
              <div class="detail__title">
                <h2>${details.Title}</h2>
              </div>
              <div class="detail__rating">
                <ul class="rating__container">
                  ${details.Ratings.map(
                    rating => `
                    <li class="rating__${rating.Source.replace(
                      /\s/g,
                      ''
                    ).toLowerCase()}">
                      ${
                        rating.Source === 'Internet Movie Database'
                          ? '<img src="/imdb.png" class="rating__imdb">'
                          : ''
                      }
                      ${
                        rating.Source === 'Rotten Tomatoes'
                          ? '<img src="/rotten.png" class="rating__rt">'
                          : ''
                      }
                      ${
                        rating.Source === 'Metacritic'
                          ? '<img src="/metacritic.png" class="rating__mt">'
                          : ''
                      }
                      ${rating.Value}
                    </li>
                  `
                  ).join('')}
                </ul>
              </div>
              <ul class="detail__info">
                <li class="info__plot"><h2>PLOT</h2><p>${details.Plot}</p></li>
                <li class="info__genre"><h2>GENRE</h2><p>${
                  details.Genre
                }</p></li>
                <li class="info__year"><h2>YEAR</h2><p>${details.Year}</p></li>
                <li class="info__director"><h2>DIRECTOR</h2><p>${
                  details.Director
                }</p></li>
                <li class="info__actors"><h2>ACTORS</h2><p>${
                  details.Actors
                }</p></li>
                <li class="info__runtime"><h2>RUNTIME</h2><p>${
                  details.Runtime
                }</p></li>
                <li class="info__released"><h2>RELEASED</h2><p>${
                  details.Released
                }</p></li>
                <li class="info__country"><h2>COUNTRY</h2><p>${
                  details.Country
                }</p></li>
                <li class="info__language"><h2>LANGUAGE</h2><p>${
                  details.Language
                }</p></li>
              </ul>
            </div>
          </div>
        </div>
      </main>
    `
  }

관리적인 측면이나 효율면에서도 좋을 것 같아요
기존 방식은 innerHTML 변경 후 다른 파일에서
해당 엘리먼트를 찾고 거기에 필요한 것들을 추가하는 방식이라
작성도 힘들고 새로운 엘리먼트가 추가 될 때마다 쿼리셀렉트로 찾아주어야 하여
좋지 않았던 것 같습니다.

resultBox.addEventListener('scroll', () => debouncedHandleScroll(resultBox));
if(resultBox.Response === "False") {
resultBox.removeEventListener('scroll', handleScroll(resultBox));
}s

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s 는 오타인 듯 합니다

Comment on lines 35 to 36
const t = $('.select:first-child').value;
const y = $('.select:nth-child(2)').value;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수가 무슨 값을 가지게 되는지 의미적으로 알 수 있으면 좋을 것 같습니다.

resultList.appendChild(loading);
searchPage++
const nextPageRes = await fetchMovies(inputText, y, t, searchPage);
loading.remove()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try, catch, final의 final에 로딩 제거를 하는 것이 좋지 않을까 생각이 듭니다.

createEl.js 를 moveinfo.js로 병합.
main.js, style.scss to src
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants