JavaScriptJavaScript 기초강의 10개 중 10번째난이도 중급

[JavaScript 완성 프로젝트] 영화 검색 앱 만들기

발행 읽는 시간 약 2 분

  • JavaScript
  • 프로젝트
  • 실습
  • 완성
  • 영화앱

강의 소개

JavaScript 시리즈 1~9편의 모든 내용을 활용해 검색, 필터, localStorage, 즐겨찾기 기능을 갖춘 영화 검색 앱을 완성합니다.

JavaScript의 모든 것을 담은 마지막 프로젝트#

HTML 시리즈, CSS 시리즈를 거쳐 드디어 JavaScript의 마지막 프로젝트입니다.

이번에는 실제로 쓸 수 있는 영화 검색 앱을 만듭니다.

구현할 기능#

기능사용 기술
영화 목록 표시배열, DOM 렌더링
제목으로 검색이벤트, 디바운스, filter
장르 필터select 이벤트, filter
별점순/제목순 정렬sort
즐겨찾기 추가/제거localStorage, classList
모달로 상세 정보모달 패턴
반응형 카드 그리드CSS Flexbox

파일 구조#

Text
movie-app/
├── index.html
├── style.css
└── script.js

index.html#

HTML
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>영화 검색 앱</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>

  <header>
    <h1>🎬 영화 검색</h1>
  </header>

  <div class="controls">
    <input type="text" id="search" placeholder="영화 제목 검색..." />
    <select id="genre-filter">
      <option value="">전체 장르</option>
      <option value="SF">SF</option>
      <option value="드라마">드라마</option>
      <option value="액션">액션</option>
      <option value="뮤지컬">뮤지컬</option>
      <option value="스릴러">스릴러</option>
    </select>
    <select id="sort">
      <option value="rating">별점 높은 순</option>
      <option value="title">제목 순</option>
      <option value="year">최신 순</option>
    </select>
    <button id="fav-toggle">★ 즐겨찾기만 보기</button>
  </div>

  <p id="result-count"></p>
  <div id="movie-grid" class="movie-grid"></div>

  <!-- 모달 -->
  <div id="modal" class="modal-overlay" hidden>
    <div class="modal-box" id="modal-content"></div>
  </div>

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

style.css#

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: -apple-system, "Noto Sans KR", sans-serif;
  background: #f5f5f5;
  color: #222;
}

header {
  background: #1a1a2e;
  color: white;
  padding: 20px 24px;
  font-size: 1.5rem;
}

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  padding: 20px 24px;
  background: white;
  border-bottom: 1px solid #e0e0e0;
}

.controls input,
.controls select {
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
}

.controls input { flex: 1; min-width: 160px; }

#fav-toggle {
  padding: 8px 16px;
  border: 1px solid #ddd;
  border-radius: 6px;
  cursor: pointer;
  background: white;
  transition: all 0.2s;
}

#fav-toggle.active {
  background: #f5a623;
  color: white;
  border-color: #f5a623;
}

#result-count {
  padding: 12px 24px;
  color: #666;
  font-size: 14px;
}

.movie-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  padding: 0 24px 40px;
}

.movie-card {
  flex: 1 1 220px;
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  position: relative;
}

.movie-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}

.movie-title { font-weight: 700; font-size: 16px; margin-bottom: 8px; }
.movie-meta { color: #666; font-size: 13px; margin-bottom: 8px; }
.movie-rating { color: #f5a623; font-weight: 700; }
.movie-genre {
  display: inline-block;
  background: #eef2ff;
  color: #4f46e5;
  padding: 2px 10px;
  border-radius: 20px;
  font-size: 12px;
}

.fav-btn {
  position: absolute;
  top: 12px;
  right: 12px;
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #ccc;
  transition: color 0.2s;
}

.fav-btn.active { color: #f5a623; }

/* 모달 */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
}

.modal-box {
  background: white;
  padding: 32px;
  border-radius: 16px;
  max-width: 480px;
  width: 90%;
}

.modal-box h2 { margin-bottom: 12px; }
.modal-box p { line-height: 1.7; color: #444; }
.modal-close {
  margin-top: 20px;
  padding: 10px 24px;
  background: #1a1a2e;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

script.js#

JavaScript
// ===== 데이터 =====
const MOVIES = [
  { id: 1, title: "인터스텔라",    year: 2014, rating: 8.6, genre: "SF",    synopsis: "우주를 탐험하는 아버지의 이야기. 시간과 중력을 넘는 SF 걸작." },
  { id: 2, title: "기생충",        year: 2019, rating: 8.5, genre: "드라마", synopsis: "두 가족의 극명한 대비를 통해 사회 계층을 날카롭게 풍자한 작품." },
  { id: 3, title: "어벤져스",      year: 2012, rating: 8.0, genre: "액션",   synopsis: "마블 히어로들이 한자리에 모여 세계를 구하는 블록버스터." },
  { id: 4, title: "라라랜드",      year: 2016, rating: 8.0, genre: "뮤지컬", synopsis: "꿈을 쫓는 두 남녀의 사랑과 이별을 그린 아름다운 뮤지컬." },
  { id: 5, title: "조커",          year: 2019, rating: 8.4, genre: "드라마", synopsis: "평범한 광대가 악당이 되기까지의 비극적 이야기." },
  { id: 6, title: "매트릭스",      year: 1999, rating: 8.7, genre: "SF",    synopsis: "가상 현실과 진짜 현실 사이에서 진실을 찾는 철학적 액션." },
  { id: 7, title: "올드보이",      year: 2003, rating: 8.4, genre: "스릴러", synopsis: "15년간 이유 없이 감금됐다 풀려난 남자의 복수극." },
  { id: 8, title: "다크나이트",    year: 2008, rating: 9.0, genre: "액션",   synopsis: "배트맨과 조커의 철학적 대결. 슈퍼히어로 영화의 교과서." },
];

// ===== 상태 =====
let favorites = JSON.parse(localStorage.getItem("favorites")) || [];
let showFavOnly = false;
let currentMovies = [...MOVIES];

// ===== 유틸 =====
function isFav(id) {
  return favorites.includes(id);
}

function toggleFav(id) {
  if (isFav(id)) {
    favorites = favorites.filter(f => f !== id);
  } else {
    favorites.push(id);
  }
  localStorage.setItem("favorites", JSON.stringify(favorites));
  render();
}

// ===== 렌더링 =====
function render() {
  const query = document.querySelector("#search").value.toLowerCase();
  const genre = document.querySelector("#genre-filter").value;
  const sortBy = document.querySelector("#sort").value;

  let filtered = MOVIES
    .filter(m => m.title.toLowerCase().includes(query))
    .filter(m => genre === "" || m.genre === genre)
    .filter(m => showFavOnly ? isFav(m.id) : true);

  if (sortBy === "rating") filtered.sort((a, b) => b.rating - a.rating);
  if (sortBy === "title")  filtered.sort((a, b) => a.title.localeCompare(b.title));
  if (sortBy === "year")   filtered.sort((a, b) => b.year - a.year);

  currentMovies = filtered;

  document.querySelector("#result-count").textContent = `총 ${filtered.length}편`;

  document.querySelector("#movie-grid").innerHTML = filtered.length === 0
    ? "<p>검색 결과가 없습니다.</p>"
    : filtered.map(m => `
        <div class="movie-card" onclick="openModal(${m.id})">
          <button class="fav-btn ${isFav(m.id) ? "active" : ""}"
            onclick="event.stopPropagation(); toggleFav(${m.id})">★</button>
          <div class="movie-title">${m.title}</div>
          <div class="movie-meta">${m.year}년 · <span class="movie-genre">${m.genre}</span></div>
          <div class="movie-rating">★ ${m.rating}</div>
        </div>
      `).join("");
}

// ===== 모달 =====
function openModal(id) {
  const movie = MOVIES.find(m => m.id === id);
  const modal = document.querySelector("#modal");
  document.querySelector("#modal-content").innerHTML = `
    <h2>${movie.title}</h2>
    <p>${movie.year}년 · ${movie.genre} · ★ ${movie.rating}</p>
    <p style="margin-top:12px">${movie.synopsis}</p>
    <button class="modal-close" onclick="closeModal()">닫기</button>
  `;
  modal.hidden = false;
}

function closeModal() {
  document.querySelector("#modal").hidden = true;
}

// ===== 이벤트 =====
const debounce = (fn, ms) => {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
};

document.querySelector("#search").addEventListener("input", debounce(render, 250));
document.querySelector("#genre-filter").addEventListener("change", render);
document.querySelector("#sort").addEventListener("change", render);

document.querySelector("#fav-toggle").addEventListener("click", (e) => {
  showFavOnly = !showFavOnly;
  e.target.classList.toggle("active", showFavOnly);
  render();
});

document.querySelector("#modal").addEventListener("click", (e) => {
  if (e.target === e.currentTarget) closeModal();
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") closeModal();
});

// ===== 초기 실행 =====
render();

완성 후 체크리스트#

  • 영화 목록이 카드 형태로 표시됨
  • 검색 시 실시간으로 필터링됨 (디바운스 적용)
  • 장르 필터가 작동함
  • 정렬이 작동함
  • 즐겨찾기 추가/제거 후 새로고침해도 유지됨
  • 카드 클릭 시 모달로 상세 정보 표시
  • 모달 닫기: 닫기 버튼, 오버레이 클릭, ESC 키

다음 단계#

HTML + CSS + JavaScript 시리즈 완료를 축하드립니다! 🎉

여기서 더 나아가고 싶다면:

  1. React — 컴포넌트 기반으로 더 큰 앱 만들기
  2. TypeScript — 타입 안전성 추가
  3. Node.js — 서버 사이드 JavaScript
  4. 실제 API 연동 — TMDB, YouTube API 등

지금 만든 영화 앱을 실제 API(TMDB)와 연동하는 것이 좋은 다음 단계입니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.