프론트엔드

[React] 영화 목록 웹사이트 만들기 + 검색 기능 구현

밍들레밍 2025. 4. 27. 21:16

검색 기능을 구현한 화면

 

https://www.youtube.com/watch?v=qN6Svts61qs&t=13s 

유튜브의

React 기본개념(Component, JSX, Props) 활용 예제 - 영화 앱 만들기

를 통해 클론코딩을 해보았다

TMDB API 

  • TMDB (The Movie Database) 에서 제공하는 "현재 한국에서 상영 중인 영화 목록" 을 가져오는 API를 활용했다.
  • API에서 가져온 실제 Response 데이터를 dummy.js 파일로 만들어서 저장했다.
// movieDummy.js

export const dummy = {
  dates: { maximum: "2022-06-29", minimum: "2022-05-12" },
  page: 1,
  results: [
    {
      id: 507086,
      title: "쥬라기 월드: 도미니언",
      poster_path: "/odxdUZWZ7fBfy3ZRj063wuJnZvo.jpg",
      vote_average: 6.7,
      overview: "...",
      release_date: "2022-06-01",
      ...
    },
    {
      id: 361743,
      title: "탑 건: 매버릭",
      poster_path: "/jMLiTgCo0vXJuwMzZGoNOUPfuj7.jpg",
      vote_average: 8.3,
      overview: "...",
      release_date: "2022-06-22",
      ...
    },
    ...
  ],
  total_pages: 3,
  total_results: 44,
};

여기서 핵심 데이터는 dummy.results 배열인데 영화 고유 ID와 영화 제목 등을 나타낸다.

import { dummy } from "./movieDummy";
import Movie from "./components/Movie";

function App() {
  return (
    <>
      <div className="app-container">
        {dummy.results.map((item) => (
          <Movie
            key={item.id}
            title={item.title}
            poster_path={item.poster_path}
            vote_average={item.vote_average}
          />
        ))}
      </div>
    </>
  );
}

export default App;

App.js는 dummy 데이터에서 가져온 전체 영화 목록(dummy.results)을 map()으로 반환하여 제목과 별점을 보여준다.

여기서 클론코딩은 끝이었지만 별다른 기능이 없는 것이 아쉬워 검색 기능을 구현해보기로 했다.

 

 

구현해볼 것

  • useState를 사용해 검색어와 영화 리스트를 상태로 관리
  • 검색어를 포함하는 영화만 필터링
  • 엔터키로도 검색 가능
  • 뒤로가기 버튼 클릭 시 원래 화면으로 돌아오기 

 

 

 

1. useState를 사용해 검색어(search)와 영화 리스트(filteredMovies)를 상태로 관리하기

const [search, setSearch] = useState("");
const [filteredMovies, setFilteredMovies] = useState(dummy.results);

filteredMovies를 통해 첫 화면에서는 전체 영화 목록을 보여주도록 했다.

 

2. handleSearch()로 검색어를 포함하는 영화만 필터링

const handleSearch = () => {
  const newMovies = dummy.results.filter((item) =>
    item.title.toLowerCase().includes(search.toLowerCase())
  );
  setFilteredMovies(newMovies);
};
  • dummy.results 안에서, 영화 제목이 검색어를 포함하는지 검사한다 
  • 대소문자 구분 없이 검색하기 위해 검색어를 .toLowerCase()로 소문자로 변환
  • 검색어가 포함된 영화들만 새 배열(newMovies)에 저장하고 setFilteredMovies(newMovies)로 화면에 새롭게 영화 목록을 보여준다

 

3. handleKeyDown()을 이용하여 엔터키로도 검색

const handleKeyDown = (e) => {
  if (e.key === "Enter") {
    handleSearch();
  }
};

 

사용자가 누른 키가 Enter라면 handleSearch()를 실행한다. 

그리고 위의 투 버튼에 이벤트 달아주기!

 <button className="search-button"onClick={handleSearch}>검색</button>
  <button className="reset-button" onClick={handleReset}>뒤로가기</button>

 

 

 

4. handleReset()으로 뒤로가기 버튼 클릭 시 전체 목록으로 돌아오기

const handleReset = () => {
  setSearch(""); 
  setFilteredMovies(dummy.results); 
};

handleReset() 발생하면 setFilteredMovies에 다시 dummy.results가 반영되도록 한다.

 

검색 후 엔터 or 버튼 눌렀을 때의 화면

최종코드

App.js

import { dummy } from "./movieDummy";
import Movie from "./components/Movie";
import React, { useState } from "react";

function App() {
  const [search, setSearch]=useState("");
  const [filteredMovies, setFilteredMovies]=useState(dummy.results);

  const handleSearch=()=>{
    const newMovies=dummy.results.filter((item)=>
      item.title.toLowerCase().includes(search.toLowerCase())
    );
    setFilteredMovies(newMovies);
  }
  const handleKeyDown=(e)=>{
    if(e.key==="Enter"){
      handleSearch();
    }
  }
  const handleReset=()=>{
    setSearch("");
    setFilteredMovies(dummy.results);
  }

  return (
    <>
    <div className="search-container">
      <input
         type="text"
         placeholder="영화 제목 검색"
         value={search}
        onChange={(e) => setSearch(e.target.value)}
        onKeyDown={handleKeyDown}
        />
      <button className="search-button"onClick={handleSearch}>검색</button>
      <button className="reset-button" onClick={handleReset}>뒤로가기</button>
      </div>
      
      <div className="app-container">
        {
          filteredMovies.map((item)=>(
            <Movie
              key={item.id}
              title={item.title}
              poster_path={item.poster_path}
              vote_average={item.vote_average}
            />
          ))
        }
      </div>
      </>
  );
}

export default App;

index.css

body{
  background-color: #22254b;
}

.app-container{
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.movie-container{
  width: 250px;
  margin: 16px;
  background-color: #373b69;
  color: white;
  border-radius: 5px;
  box-shadow: 3px 3px 5px rgba(0, 0, 0, 1);

}

.movie-container img{
  max-width: 100%;
}

.movie-info{
  display: flex;
  padding: 20px;
  justify-content: space-between;
  align-items: center;
}
.movie-info h4{
  margin: 0;
}

.movie-info span{
  margin-left: 3px;
}
.search-container{
  display: flex;
  justify-content: center;
  padding: 10px;
  width: 100%;
  margin-top: 20px;
  border-radius: 5px;
  border: none;
  gap: 10px;
}
input {
  width: 250px;
  font-size: 20px;
  height: 50px;
  cursor: pointer;
}
button{
  border-radius: 5px;
  background-color: darkgray;
  cursor: pointer;
  font-size: 18px;
}
button:hover {
  background-color: #4b50a3;
}
.search-button{
  width: 60px;
}
.reset-button{
  width: 100px;
}

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Movie.js

import React from "react";
const IMG_BASE_URL = "https://image.tmdb.org/t/p/w1280/";

export default function Movie({title, poster_path, vote_average}){
  return(
    <div className="movie-container">
      <img src={IMG_BASE_URL + poster_path} alt="영화포스터"/>
      <div className="movie-info">
          <h4>{title}</h4>
          <span>{vote_average}</span>
      </div>
    </div>
  )
}