따라하며 배우는 리액트 A-Z
[프론트엔드, 웹 개발] 강의입니다.
이 강의를 통해 리액트 기초부터 중급까지 배우게 됩니다. 하나의 강의로 개념도 익히고 실습도 하며, 리액트를 위해 필요한 대부분의 지식을 한번에 습득할 수 있도록 만들었습니다.
✍️
이런 걸
배워요!
리액트
NextJS
타입스크립트
정적 사이트 자동 배포
도커
강의: https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B8#
강의 자료 : https://github.com/braverokmc79/DiagramPDF
소스: https://github.dev/braverokmc79/react-netflix-clone
[4]. Netflix 앱 완성하기
44.영화 나열을 위한 Row 컴포넌트 생성하기
강의:
src/components/Row.js
import React, { useEffect, useState } from 'react'; import axios from '../api/axios'; import "./Row.css"; const Row = ({ isLargeRow, title, id, fetchUrl }) => { const [movies, setMovies] = useState([]); useEffect(() => { fetchMovieData(); }, []); const fetchMovieData = async () => { const request = await axios.get(fetchUrl); setMovies(request.data.results); } console.log("title : ", movies); return ( <section className='row'> <h2>{title}</h2> <div className='slider'> <div className='slider_arrow-left'> <span className='arrow'>{"<"}</span> </div> <div id={id} className="row_posters"> { movies.map((movie) => { return ( <div className='poster' key={movie.id}> <img className={`row_poster ${isLargeRow && "row_posterLarge"}`} src={`https://image.tmdb.org/t/p/original/${isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name} /> <span className='movie_name'>{movie.name || movie.title}</span> </div> ) }) } </div> <div className='slider_arrow-right'> <span className='arrow'>{">"}</span> </div> </div> </section> ); }; export default Row;
src/components/Row.css
.row { margin-left: 20px; color: white; margin-bottom: 70px; } h2 { padding-left: 20px; } .slider { position: relative; } .slider_arrow-left { background-clip: content-box; padding: 20px 0; box-sizing: border-box; transition: 400ms all ease-in-out; cursor: pointer; width: 80px; z-index: 1000; position: absolute; left: 0px; top: 0; height: 100%; display: flex; align-items: center; justify-content: center; visibility: hidden; } .slider_arrow-right { padding: 20px 0; background-clip: content-box; box-sizing: border-box; transition: 400ms all ease-in-out; cursor: pointer; width: 80px; z-index: 1000; position: absolute; right: 0px; top: 0; height: 100%; display: flex; align-items: center; justify-content: center; visibility: hidden; } .arrow { transition: 400ms all ease-in-out; } .arrow:hover { transition: 400ms all ease-in-out; transform: scale(1.5); } .slider:hover .slider_arrow-left { transition: 400ms all ease-in-out; visibility: visible; } .slider:hover .slider_arrow-right { transition: 400ms all ease-in-out; visibility: visible; } .slider_arrow-left:hover { background: rgba(20, 20, 20, 0.5); transition: 400ms all ease-in-out; } .slider_arrow-right:hover { background: rgba(20, 20, 20, 0.5); transition: 400ms all ease-in-out; } .row_posters { display: flex; overflow-y: hidden; overflow-x: scroll; padding: 20px 0 20px 20px; scroll-behavior: smooth; } .row_posters::-webkit-scrollbar { display: none; } .poster{ height: 100%; } .row_poster { object-fit: contain; width: 100%; max-height: 144px; margin-right: 10px; transition: transform 450ms; border-radius: 4px; width: auto; } .row_poster:hover { transform: scale(1.08); } .row_posterLarge { max-height: 320px; } .row_posterLarge:hover { transform: scale(1.1); opacity: 1; } .row_arrow-left { position: absolute; top: 0; left: 20px; height: 100%; width: 32px; background: rgba(0, 0, 0, 0.2); display: flex; align-items: center; } .row_arrow-right { position: absolute; top: 0; right: 0px; height: 100%; width: 32px; background: rgba(0, 0, 0, 0.2); display: flex; align-items: center; } .movie_name{ bottom: 0px; } @media screen and (min-width: 1200px) { .row_poster { max-height: 160px; } .row_posterLarge { max-height: 360px; } } @media screen and (max-width: 768px) { .row_poster { max-height: 100px; } .row_posterLarge { max-height: 280px; } } .swiper-pagination { text-align: right !important; } .swiper-pagination-bullet { background: gray !important; opacity: 1 !important; } .swiper-pagination-bullet-active { background: white !important; } .swiper-button-prev { color: white !important; } .swiper-button-next { color: white !important; } .swiper-button-next:after, .swiper-button-prev:after{ font-size: 1.3rem !important; font-weight: 600 !important; }
45.슬라이드 기능 추가하기
강의:
<span className='arrow' onClick={() => { document.getElementById(id).scrollLeft -= window.innerWidth - 80; }}
46.Styled Component를 이용해서 Footer 생성하기
강의 :
src/components/Footer.js
import React from "react"; import styled from "styled-components"; export default function Footer() { return ( <FooterContainer> <FooterContent> <FooterLinkContainer> <FooterLinkTitle>Now Movie 대한민국</FooterLinkTitle> <FooterLinkContent> {/* <FooterLink href="https://help.netflix.com/ko/node/412"> Now Movie 소개 </FooterLink> */} <FooterLink href="https://help.netflix.com/ko"> Now Movies 소개 </FooterLink> <FooterLink href="https://help.netflix.com/ko/"> 미디어 센터 </FooterLink> <FooterLink href="https://help.netflix.com/ko/"> 이용 약관 </FooterLink> </FooterLinkContent> <FooterDescContainer> <FooterDescRights> Now Movie Rights Reserved. </FooterDescRights> </FooterDescContainer> </FooterLinkContainer> </FooterContent> </FooterContainer> ); } const FooterContainer = styled.div` display: flex; justify-content: center; align-items: center; padding: 40px 0; border-top: 1px solid rgb(25, 25, 25); width: 100%; position: relative; z-index: 100; @media (max-width: 769px) { padding: 20px 20px; padding-bottom: 30px; } `; const FooterContent = styled.div``; const FooterLinkContainer = styled.div` width: 500px; @media (max-width: 768px) { width: 100%; } `; const FooterLinkTitle = styled.h1` color: gray; font-size: 17px; `; const FooterLinkContent = styled.div` display: flex; justify-content: space-bewteen; flex-wrap: wrap; margin-top: 35px; @media (max-width: 768px) { margin-top: 26px; } `; const FooterLink = styled.a` color: gray; font-size: 14px; width: 160px; margin-bottom: 21px; text-decoration: none; &:hover { text-decoration: underline; } @media (max-width: 768px) { margin-bottom: 16px; } `; const FooterDescContainer = styled.div` margin-top: 30px; @media (max-width: 768px) { margin-top: 20px; } `; const FooterDescRights = styled.h2` color: white; font-size: 14px; text-align: center; `;
47.영화 자세히 보기 클릭 시 모달 생성하기
강의 :
src/components/Row.js
import React, { useEffect, useState } from 'react'; import axios from '../api/axios'; import "./Row.css"; import MovieModal from './MovieModal/index'; const Row = ({ isLargeRow, title, id, fetchUrl }) => { const [movies, setMovies] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [movieSelected, setMovieSelected] = useState({}); useEffect(() => { fetchMovieData(); }, []); const fetchMovieData = async () => { const request = await axios.get(fetchUrl); setMovies(request.data.results); } const handleClick = (movie) => { setModalOpen(true); setMovieSelected(movie); } return ( <section className='row'> <h2>{title}</h2> <div className='slider'> <div className='slider_arrow-left' onClick={() => { document.getElementById(id).scrollLeft -= window.innerWidth - 80; }}> <span className='arrow' >{"<"}</span> </div> <div id={id} className="row_posters"> { movies.map((movie) => { return ( <div className='poster' key={movie.id} onClick={() => handleClick(movie)}> <img className={`row_poster ${isLargeRow && "row_posterLarge"}`} src={`https://image.tmdb.org/t/p/original/${isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name} /> <span className='movie_name'>{movie.name || movie.title}</span> </div> ) }) } </div> <div className='slider_arrow-right' onClick={() => { document.getElementById(id).scrollLeft += window.innerWidth - 80; }}> <span className='arrow' >{">"}</span> </div> </div> { modalOpen && <MovieModal {...setMovieSelected} setModalOpen={setModalOpen} /> } </section > ); }; export default Row;
src/components/MovieModal/index.js
import React from 'react'; import './MovieModal.css'; const MovieModal = ({ backdrop_path, title, overview, name, release_date, first_air_date, vote_average, setModalOpen }) => { return ( <div> </div> ); }; export default MovieModal;
48.Movie 모달 UI 생성하기
강의 :
src/components/MovieModal/index.js
import React, { useEffect, useState } from 'react'; import axios from '../../api/axios'; import axiosEn from '../../api/axiosEn'; import styled from 'styled-components'; import './MovieModal.css'; const HomeContainer = styled.div` width: 100%; height: auto; `; const Iframe = styled.iframe` width: 100%; height: 500px; z-index: -1; opacity: 0.65; border: none; &::after{ content:"" ; position: absolute; top: 0; left: 0; width: 100%; height: 200; } `; const MovieModal = ({ isLargeRow, id, backdrop_path, title, overview, name, release_date, first_air_date, vote_average, setModalOpen }) => { const [movieKey, setMovieKey] = useState(""); useEffect(() => { if (!isLargeRow) { movieDetail(); } }, []); const movieDetail = async () => { //특정 영화의 더 상세한 정보를 가져오기 (비디오 정보도 포함) const movie = await axios.get(`movie/${id}`, { params: { append_to_response: "videos" } }) //비디오가 없다면 다음을 실행 if (movie.videos === undefined) { const { data: movieDetailEn } = await axiosEn.get(`movie/${id}`, { params: { append_to_response: "videos" }, }); setMovieKey(movieDetailEn.videos.results[0].key); } else { setMovieKey(movie.videos.results[0].key) } } console.log("영화 movie : ", movieKey); return ( <div className='presentation'> <div className='wrapper-modal' onClick={() => setModalOpen(false)} > <div className='modal'> <span onClick={() => setModalOpen(false)} className="modal-close"> x </span> {movieKey && <HomeContainer> <Iframe src={`https://www.youtube.com/embed/${movieKey}?controls=1&autoplay=1&loop=1&mute=0&playlist=${movieKey}`} title="YouTube video player" frameborder="0" allow="autoplay; fullscreen" allowfullscreen ></Iframe> </HomeContainer> } <img className='modal_poster-img' src={`https://image.tmdb.org/t/p/original${backdrop_path}`} alt={name} /> <div className='modal_content'> <p className='modal_details'> <span className='modal_user_perc'> 100% for you </span> {release_date ? release_date : first_air_date} </p> <h2 className='modal_title'>{title ? title : name}</h2> <p className='modal_overview'>평점 : {vote_average}</p> <p className='modal_overview'>{overview}</p> </div> </div> </div> </div> ); }; export default MovieModal;
src/components/MovieModal/MovieModal.css
import React, { useEffect, useState } from 'react'; import axios from '../../api/axios'; import axiosEn from '../../api/axiosEn'; import styled from 'styled-components'; import './MovieModal.css'; const HomeContainer = styled.div` width: 100%; height: auto; `; const Iframe = styled.iframe` width: 100%; height: 500px; z-index: -1; opacity: 0.65; border: none; &::after{ content:"" ; position: absolute; top: 0; left: 0; width: 100%; height: 200; } `; const MovieModal = ({ isLargeRow, id, backdrop_path, title, overview, name, release_date, first_air_date, vote_average, setModalOpen }) => { const [movieKey, setMovieKey] = useState(""); useEffect(() => { if (!isLargeRow) { movieDetail(); } }, []); const movieDetail = async () => { //특정 영화의 더 상세한 정보를 가져오기 (비디오 정보도 포함) const movie = await axios.get(`movie/${id}`, { params: { append_to_response: "videos" } }) //비디오가 없다면 다음을 실행 if (movie.videos === undefined) { const { data: movieDetailEn } = await axiosEn.get(`movie/${id}`, { params: { append_to_response: "videos" }, }); setMovieKey(movieDetailEn.videos.results[0].key); } else { setMovieKey(movie.videos.results[0].key) } } console.log("영화 movie : ", movieKey); return ( <div className='presentation'> <div className='wrapper-modal' onClick={() => setModalOpen(false)} > <div className='modal'> <span onClick={() => setModalOpen(false)} className="modal-close"> x </span> {movieKey && <HomeContainer> <Iframe src={`https://www.youtube.com/embed/${movieKey}?controls=1&autoplay=1&loop=1&mute=0&playlist=${movieKey}`} title="YouTube video player" frameborder="0" allow="autoplay; fullscreen" allowfullscreen ></Iframe> </HomeContainer> } <img className='modal_poster-img' src={`https://image.tmdb.org/t/p/original${backdrop_path}`} alt={name} /> <div className='modal_content'> <p className='modal_details'> <span className='modal_user_perc'> 100% for you </span> {release_date ? release_date : first_air_date} </p> <h2 className='modal_title'>{title ? title : name}</h2> <p className='modal_overview'>평점 : {vote_average}</p> <p className='modal_overview'>{overview}</p> </div> </div> </div> </div> ); }; export default MovieModal;
49.React Router Dom
강의:
npm install react-router-dom yarn add react-router-dom
50.React Router Dom
강의:
51.Netflix 앱에 React Router Dom 적용하기
index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { BrowserRouter } from 'react-router-dom'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <App /> </BrowserRouter> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
App.js
import './App.css'; import Nav from './components/Nav'; import Footer from './components/Footer'; import { Outlet, Routes ,Route } from 'react-router-dom'; import MainPage from './pages/MainPage'; import DetailPage from './pages/DetailPage'; import SearchPage from './pages/SearchPage'; const Layout =()=>{ return( <div> <Nav /> <Outlet /> <Footer /> </div> ) } function App() { return ( <div className="App"> <Routes> <Route path='/' element={<Layout />}> <Route index element={<MainPage />} /> <Route path=":movieId" element={<DetailPage />} /> <Route path="search" element={<SearchPage />} /> </Route> </Routes> </div> ); } export default App;
src/pages/MainPage/index.js
import React from 'react' import requests from '../../api/requests' import Banner from '../../components/Banner' import Row from '../../components/Row' function MainPage() { return ( <div> <Banner /> <Row title="넷플릭스 오리지널" id="NO" fetchUrl={requests.fetchNetflixOriginals} isLargeRow /> <Row title="Trending Now" id="TN" fetchUrl={requests.fetchTrending} /> <Row title="Top Rated" id="TR" fetchUrl={requests.fetchTopRated} /> <Row title="액션 영화" id="AM" fetchUrl={requests.fetchActionMovies} /> <Row title="코미디 영화" id="CM" fetchUrl={requests.fetchComedyMovies} /> <Row title="공포 영화" id="HM" fetchUrl={requests.fetchHorrorMovies} /> <Row title="로맨스 영화" id="RM" fetchUrl={requests.fetchRomanceMovies} /> <Row title="다큐멘터리" id="DM" fetchUrl={requests.fetchDocumentaries} /> </div> ) } export default MainPage
댓글 ( 4)
댓글 남기기