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