React

 

따라하며 배우는 리액트 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을 이용한 검색 페이지 구현하기

 

강의:

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/unit/119894?tab=curriculum

 

 

 

 

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 구현하기

 

강의:

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/unit/119895?tab=curriculum

 

 

 

 

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 만들기

 

검색:

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/unit/119896?tab=curriculum

 

 

 

 

 

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를 이용한 영화 상세 페이지 구현하기

 

강의:

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/unit/119897?tab=curriculum

 

 

 

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 &nbsp;
                           </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 생성
 

강의:

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/unit/119898?tab=curriculum

 

 

 

 

 

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://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/unit/119899?tab=curriculum

 

 

 

 

 

 

 

 

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를 이용해서 배포하기

강의:

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/unit/119900?tab=curriculum

 

 

 

 

 

 

깃허브 배포 라이브러리 설치

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>
);

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

감기는 치료하면 7일 가지만, 만일 아무 것도 하지 않는다면 일주일 간다. -레이몽 두모스

댓글 ( 4)

댓글 남기기

작성