[8-1] ReactJS로 Movie app 만들기
영화 목록을 불러와서 화면에 표시하고, 각 영화를 클릭하면 모달 창에서 상세 정보를 확인할 수 있도록 구현할 것이다.
필요한 패키지를 import하고 useState, useEffect를 사용하여 상태와 데이터를 관리한다. 그리고 CSS 스타일링을 위해 styled-components(@emotion/styled
)를 사용한다.
// 필요한 패키지 import import { useState, useEffect } from "react"; import styled from "@emotion/styled"; import { Modal } from "antd";
// 상태와 API 키 설정 const [movies, setMovies] = useState([]); const [modalVisible, setModalVisible] = useState(false); // 모달의 가시성 상태 const [selectedMovie, setSelectedMovie] = useState(null); // 선택한 영화 const API_KEY = "15f8fbf5168d6da001f1e3c2c4b76277"; // 영화 데이터 가져오기 useEffect(() => { fetchMoviesData(); }, []); const fetchMoviesData = async () => { try { const response = await fetch( `https://yts.mx/api/v2/list_movies.json?minimum_rating=8.8&sort_by=year&api_key=${API_KEY}` ); const data = await response.json(); setMovies(data.data.movies); } catch (error) { console.log(error); } };
Main
, Item
, Img
, Tie
, TextTie
등의 스타일을 정의하고, fetchMoviesData에서 가져온 영화 데이터를 map 함수를 사용하여 그리드 형태로 화면에 표시한다. 클릭한 영화에 대한 상세 정보는 Modal 컴포넌트로 모달 창에 표시된다.// UI 스타일과 컴포넌트 정의 const Main = styled.div` display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; width: 80%; margin: 0 auto; padding: 40px 0px; `; const Item = styled.div` width: calc(25% - 20px); text-align: center; `; const Img = styled.img` cursor: pointer; background-color: gray; object-fit: cover; transition: border-color 0.3s, transform 0.3s; border: 2px solid transparent; :hover { border: 5px solid gold; transform: translateY(-10px); } `; const Tie = styled.div` display: flex; `; const TextTie = styled.div` display: flex; flex-direction: column; margin-left: 10px; `; // 영화 목록 UI 구성 return ( <Main className="grid-container"> {movies.map((movie) => ( <Item key={movie.id} className="grid-item"> <Img src={movie.medium_cover_image} alt={movie.title} onError={(event) => { event.target.style.display = "none"; }} onClick={() => handleMovieClick(movie)} /> <p>제목: {movie.title}</p> <p>연도: {movie.year}</p> </Item> ))} {selectedMovie && ( <Modal visible={modalVisible} onCancel={handleModalClose} footer={null} destroyOnClose <Tie> <img src={selectedMovie.medium_cover_image} alt={selectedMovie.title} /> <TextTie> <p>제목: {selectedMovie.title}</p> <p>연도: {selectedMovie.year}</p> <p>내용: {truncateSummary(selectedMovie.summary, 200)}</p> <Button onClick={handleMoreButtonClick}>더보기</Button> </TextTie> </Tie> </Modal> )} </Main> );
// 영화 요약 내용 요약 함수 const truncateSummary = (summary, maxLength) => { if (summary.length > maxLength) { return summary.substring(0, maxLength) + "..."; } return summary; }; // 더보기 버튼 클릭 핸들러 const handleMoreButtonClick = () => { window.open(selectedMovie.url, "_blank"); };
import { useState, useEffect } from "react"; import styled from "@emotion/styled"; import { Modal } from "antd"; export default function Weather() { const [movies, setMovies] = useState([]); const [modalVisible, setModalVisible] = useState(false); // 모달의 가시성 상태 const [selectedMovie, setSelectedMovie] = useState(null); // 선택한 영화 const API_KEY = "15f8fbf5168d6da001f1e3c2c4b76277"; useEffect(() => { fetchMoviesData(); }, []); const Main = styled.div` display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; width: 80%; margin: 0 auto; padding: 40px 0px; `; const Item = styled.div` width: calc(25% - 20px); text-align: center; `; const Img = styled.img` cursor: pointer; background-color: white; object-fit: cover; transition: border-color 0.3s, transform 0.3s; /* 테두리 색상 및 변화에 애니메이션 효과 추가 */ border: 2px solid transparent; /* 초기 테두리 설정 */ margin-top: 20px; :hover { transform: translateY(-10px); /* 위로 10px 이동하는 애니메이션 효과 */ } `; const Tie = styled.div` display: flex; `; const TextTie = styled.div` display: flex; flex-direction: column; margin-left: 10px; `; const P1 = styled.p` margin-top: 10px; `; const Button = styled.button` margin-top: 10px; cursor: pointer; background-color: #1890ff; color: white; border: none; padding: 5px 10px; border-radius: 4px; transition: background-color 0.3s; margin-top: 30px; :hover { background-color: #40a9ff; } `; const fetchMoviesData = async () => { try { const response = await fetch( `https://yts.mx/api/v2/list_movies.json?minimum_rating=8.8&sort_by=year&api_key=${API_KEY}` ); const data = await response.json(); console.log(data.data.page_number); setMovies(data.data.movies); } catch (error) { console.log(error); } }; const handleMovieClick = (movie) => { setSelectedMovie(movie); setModalVisible(true); }; const handleModalClose = () => { setModalVisible(false); }; const truncateSummary = (summary, maxLength) => { if (summary.length > maxLength) { return summary.substring(0, maxLength) + "..."; } return summary; }; const handleMoreButtonClick = () => { if (selectedMovie && selectedMovie.url) { window.open(selectedMovie.url, "_blank"); } }; return ( <Main className="grid-container"> {movies.map((movie) => ( <Item key={movie.id} className="grid-item"> <Img src={movie.medium_cover_image} alt={movie.title} onError={(event) => { event.target.style.display = "none"; }} onClick={() => handleMovieClick(movie)} /> </Item> ))} {selectedMovie && ( <Modal visible={modalVisible} onCancel={handleModalClose} footer={null} destroyOnClose <Tie> <img src={selectedMovie.medium_cover_image} alt={selectedMovie.title} /> <TextTie> <P1>제목: {selectedMovie.title}</P1> <P1>연도: {selectedMovie.year}</P1> <P1>내용: {truncateSummary(selectedMovie.summary, 200)}</P1> <Button onClick={handleMoreButtonClick}>더보기</Button> </TextTie> </Tie> </Modal> )} </Main>);}