따라하며 배우는 리액트 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)
댓글 남기기