따라하며 배우는 리액트 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 앱 완성하기
52.useLocation을 이용한 검색 페이지 구현하기
강의:

src/pages/SearchPage/index.js
import axios from '../../api/axios';
import axiosEn from '../../api/axiosEn';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
/** useLocation 값들
hash: ""
key: "8eqba7lc"
pathname:"/search"
search: "?q=d"
state:null
**/
function SearchPage() {
const [searchResults, setSearchResults] =useState([]);
const useQuery =()=>{
return new URLSearchParams(useLocation().search);
}
let query =useQuery();
const searchTerm =query.get("q");
console.log('searchTerm', searchTerm);
useEffect(()=>{
if(searchTerm){
fetchSearchMovie(searchTerm);
}
}, []);
const fetchSearchMovie= async (searchTerm)=>{
try{
const request = await axios.get(`/search/multi?include_adult=false&query=${searchTerm}`);
console.log(request);
}catch(error){
console.log("error", error);
}
}
return (
<div>index</div>
)
}
export default SearchPage
53.검색 페이지 UI 구현하기
강의:

src/pages/SearchPage/index.js
import axios from '../../api/axios';
import axiosEn from '../../api/axiosEn';
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import './SearchPage.css';
/** useLocation 값들
hash: ""
key: "8eqba7lc"
pathname:"/search"
search: "?q=d"
state:null
**/
function SearchPage() {
const [searchResults, setSearchResults] =useState([]);
const navigate =useNavigate();
const useQuery =()=>{
return new URLSearchParams(useLocation().search);
}
let query =useQuery();
const searchTerm =query.get("q");
useEffect(()=>{
if(searchTerm){
fetchSearchMovie(searchTerm);
}
}, [searchTerm]);
const fetchSearchMovie= async (searchTerm)=>{
try{
const request = await axios.get(`/search/multi?include_adult=false&query=${searchTerm}`);
console.log(request);
setSearchResults(request.data.results);
}catch(error){
console.log("error", error);
}
}
const renderSearchResults=()=>{
return searchResults.length >0 ? (
<section className='search-container'>
{searchResults.map((movie)=>{
if(movie.backdrop_path !==null && movie.media_type !== "person"){
const movieImageUrl = "https://image.tmdb.org/t/p/w500"+movie.backdrop_path ;
return (
<div className='movie' key={movie.id}>
<div className='movie_column-poster'>
<img src={movieImageUrl} alt="movie" className='movie_poster' />
<span className='movie_name'>{movie.name || movie.title} (평점 : {movie.vote_average})</span>
</div>
</div>
);
}
})}
</section> ) :
(<section className='no-results'>
<div className='no-results_text'>
<p>
찾고자하는 검색어 "{searchTerm}"에 맞는 영화가 없습니다.
</p>
</div>
</section>)
}
return renderSearchResults();
}
export default SearchPage
src/pages/SearchPage/SearchPage.css
.searchContent {
height: 100vh;
background-color: black;
}
.search-container {
background-color: black;
width: 100%;
text-align: center;
padding: 5rem 0;
}
.no-results {
display: flex;
justify-content: center;
align-content: center;
color: #c5c5c5;
height: 100%;
padding: 8rem;
}
.movie {
flex: 1 1 auto;
display: inline-block;
padding-right: 0.5rem;
padding-bottom: 7rem;
}
.movie_column-poster {
cursor: pointer;
transition: transform 0.3s;
-webkit-transition: transform 0.3s;
position: relative;
}
.movie_column-poster :hover {
transform: scale(1.25);
}
.movie_poster {
width: 90%;
border-radius: 5px;
}
.movie_name{
color: #fff;
position: absolute;
bottom: -10%;
left: 5%;
}
54.useDebounce Custom Hooks 만들기
검색:

src/hooks/useDebounce.js
import React, { useEffect } from 'react'
export const useDebounce = (value, delay)=>{
const [debounceValue, setDebounceValue] =useState(value);
useEffect(()=>{
const handler =setTimeout(()=>{
setDebounceValue(value);
}, delay);
return ()=>{
clearTimeout(handler);
}
}, [value, delay]);
return debounceValue;
}
src/pages/SearchPage/index.js
import { useDebounce } from '../../hooks/useDebounce';
~
const searchTerm =query.get("q");
const debounceTerm =useDebounce(searchTerm, 500);
~
55.useParams를 이용한 영화 상세 페이지 구현하기
강의:

src/coponents/pages/DetailPage/index.js
import React, { useEffect, useState } from 'react';
import axios from '../../api/axios';
import axiosEn from '../../api/axiosEn';
import styled from 'styled-components';
import './DetailPage.css';
import { useNavigate, useParams } from 'react-router-dom';
import GoMove from "../../components/GoMove";
const HomeContainer = styled.div`
width: 100%;
height: auto;
`;
const Iframe = styled.iframe`
width: 100%;
height: 800px;
z-index: -1;
opacity: 0.65;
border: none;
&::after{
content:"" ;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200;
}
`;
const DetailPage = ({setModalOpen}) => {
let {movieId} =useParams();
const navigate=useNavigate();
const [movie, setMovie] = useState("");
const [movieKey, setMovieKey] = useState("");
const [requestError, setRequestError]=useState(false);
useEffect(() => {
if (movieId) {
movieDetail();
}
}, []);
const movieDetail = async () => {
//특정 영화의 더 상세한 정보를 가져오기 (비디오 정보도 포함)
let request="";
try{
request= await axios.get(`movie/${movieId}`, {
params: { append_to_response: "videos" }
}) ;
}catch(error){
console.log("데이터 없음");
setRequestError(true);
return;
}
setMovie(request.data);
//비디오가 없다면 다음을 실행해서 영어 데이터 가져오기
if (request.videos === undefined) {
const { data: movieDetailEn } = await axiosEn.get(`movie/${movieId}`, {
params: { append_to_response: "videos" },
});
if(movieDetailEn.videos.results.length>0){
setMovieKey(movieDetailEn.videos.results[0].key);
}
} else {
if(request.videos.results.length>0){
setMovieKey(request.videos.results[0].key);
}
}
}
if(requestError){
return (
<section className='section-detail'>
<h1 className='section-detail-title'>상세 내용이 없습니다.</h1>
</section>
)
}
if (!movieId || !movie){
return (
<section className='section-detail'>
<h1 className='section-detail-title'>...loading</h1>
</section>
)
}
return (
<section className='section-detail'>
<img
alt='User logged'
src={`${process.env.PUBLIC_URL}/img/back.png`}
className='nav_avatar_back'
onClick={() => navigate(-1)}
/>
{movieKey && <HomeContainer>
<Iframe
src={`https://www.youtube.com/embed/${movieKey}?controls=1&autoplay=1&loop=1&mute=0&playlist=${movieKey}&volume=5`}
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/${movie.backdrop_path}`}
alt={movie.name}
/>
<div className='modal_content'>
<p className='modal_details'>
<span className='modal_user_perc'>
100% for you
</span>
<span className='modal_user_release_date'>
개봉일1: {movie.release_date ? movie.release_date : movie.first_air_date}
</span>
justify-content: flex-end
</p>
<h2 className='modal_title'>{movie.title ? movie.title : movie.name}</h2>
<div className='go-moive'>
<GoMove title={movie.title} name={movie.name} domain={"peekle"} webSiteName={"피클"} />
<GoMove title={movie.title} name={movie.name} domain={"qooqootv"} webSiteName={"쿠쿠티비"} />
<GoMove title={movie.title} name={movie.name} domain={"youtube"} webSiteName={"유튜브"} />
<GoMove title={movie.title} name={movie.name} domain={"kugabox"} webSiteName={"쿠가박스"} />
<GoMove title={movie.title} name={movie.name} domain={"koreanz"} webSiteName={"코리안즈"} />
<GoMove title={movie.title} name={movie.name} domain={"sonagitv"} webSiteName={"소나기"} />
<GoMove title={movie.title} name={movie.name} domain={"justlink"} webSiteName={"저스트링크"} />
</div>
<p className='modal_overview'>평점 : {movie.vote_average}</p>
<p className='modal_overview'>{movie.overview}</p>
</div>
</section>
);
};
export default DetailPage;
src/coponents/pages/DetailPage/DetailPage.css
.section-detail{
width: 90%;
margin: 0 auto;
position: relative;
top:100px;
}
.modal_user_release_date{
display: flex;
justify-content: flex-end;
}
.section-detail .section-detail-title{
color:#fff;
text-align: center;
height: 200px;
}
.nav_avatar_back{
width: 64px;
height: 64px;
position: fixed;
right:40px;
object-fit: contain;
cursor: pointer;
z-index: 10;
}
56.모달 창 외부 클릭 시 모달 닫게 만드는 Custom Hooks 생성
강의:

src/components/MovieModal/index.js
~
//모달창 외부 클릭시 모달 닫게
const ref =useRef();
useOnClickOutside(ref, ()=>{setModalOpen(false)});
~
src/hooks/useOnClickOutside.js
import React, { useEffect } from 'react'
export default function useOnClickOutside(ref, handler) {
useEffect(()=>{
const listener=(event)=>{
console.log(" ref " , ref.current);
console.log(" event.target " , event.target);
if(!ref.current || ref.current.contains(event.target)){
return;
}
handler();
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart",listener)
return ()=>{
//unmount 될때
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
}
}, [ref, handler]);
}
57.swiper 모듈을 이용한 터치 슬라이드 구현하기
강의 :

https://swiperjs.com/get-started
src/components/Row.js
import React, { useEffect, useState } from 'react';
import axios from '../api/axios';
import "./Row.css";
import MovieModal from './MovieModal/index';
// import Swiper core and required modules
import { Navigation, Pagination, Scrollbar, A11y } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
// Import Swiper styles
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/scrollbar";
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> */}
<Swiper
// install Swiper modules
modules={[Navigation, Pagination, Scrollbar, A11y]}
loop={true} // loop 기능을 사용할 것인지
breakpoints={{
1378: {
slidesPerView: 6, // 한번에 보이는 슬라이드 개수
slidesPerGroup: 6, // 몇개씩 슬라이드 할지
},
998: {
slidesPerView: 5,
slidesPerGroup: 5,
},
625: {
slidesPerView: 4,
slidesPerGroup: 4,
},
0: {
slidesPerView: 3,
slidesPerGroup: 3,
},
}}
navigation // arrow 버튼 사용 유무
pagination={{ clickable: true }} // 페이지 버튼 보이게 할지
>
<div id={id} className="row_posters">
{
movies.map((movie) => {
if((isLargeRow ? movie.poster_path : movie.backdrop_path)!==null){
return (
<SwiperSlide key={movie.id}>
<div className={`poster ${isLargeRow !==undefined? "posterLarge" : "general" }`} onClick={() => handleClick(movie)}>
<img
className={`row_poster ${isLargeRow !==undefined? "row_posterLarge" : "row_general"}`}
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} (평점 : {movie.vote_average})</span>
</div>
</SwiperSlide>
)
}
})
}
</div>
{/* <div className='slider_arrow-right' onClick={() => {
document.getElementById(id).scrollLeft += window.innerWidth - 80;
}}>
<span className='arrow' >{">"}</span>
</div> */}
</Swiper>
{
modalOpen &&
<MovieModal {...movieSelected} isLargeRow={isLargeRow} setModalOpen={setModalOpen} />
}
</section >
);
};
export default Row;
src/components/Row.css
.swiper{
padding-bottom: 30px !important;
}
.swiper-slide{
padding:0px 40px;
}
.swiper-pagination {
text-align: center !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;
}
.swiper-slide, swiper-slide{
padding: 0px 70px !important;
}
58.github를 이용해서 배포하기
강의:

깃허브 배포 라이브러리 설치
npm install gh-pages --save-dev
package.json
"homepage": "https://braverokmc79.github.io/netflix",
"scripts": {
"dev" :"cross-env REACT_APP_API_URL=localhost react-scripts start",
"start": "react-scripts start",
"build": "react-scripts build --mode production",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
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 basename={`${process.env.REACT_APP_API_URL=== "localhost" ? "" : "netflix"}`} >`
<App />
</BrowserRouter>
);














댓글 ( 4)
댓글 남기기