
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가 반영되도록 한다.

최종코드
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>
)
}
'프론트엔드' 카테고리의 다른 글
| [React] 영화 대여 온라인몰 확장 구현하기 (0) | 2025.05.04 |
|---|---|
| [React] 린캔버스 프로젝트 (0) | 2025.04.06 |
| [React]React 완벽 마스터: 기초 개념부터 린캔버스 프로젝트까지 ② (0) | 2025.03.31 |
| [React]React 완벽 마스터: 기초 개념부터 린캔버스 프로젝트까지 ① (0) | 2025.03.22 |
| [HTML, CSS] 네이버 클론코딩 ② (0) | 2025.02.18 |