React

5. Netflix 앱 완성하기

기 도 2023. 5. 10. 13:15

 영화 나열을 위한 Row 컴포넌트 생성하기 

 

1. Row 컴포넌트 생성

2. 소스코드 작성

2.1 영화 정보 가져오기

  const[ movies, setMovies] = useState([]);

    useEffect(()=> {
    fetchMovieData();
  }, [fetchURL])
  
  const fetchMovieData = async() => {
    const request = await axios.get(fetchURL);
    setMovies(request.data.results);
  }

 

2.2 UI 생성하기

    return (
    <section className="row">
      <h2>{title}</h2>
      <div className="slider">
        <div className="slider__arrow-left">
            <span className="arrow"
                onClick={() => {
                document.getElementById(id).scrollLeft -= window.innerWidth - 80;
            }}
            >{"<"}</span>
        </div>
        <div id={id} className="row__posters">
            {movies.map(movie => (
                <img 
                    key={movie.id}
                    className={`row__poster ${isLargeRow && "row__posterLarge"}`}
                    src={`https://image.tmdb.org/t/p/original/${
                    isLargeRow ? movie.poster_path : movie.backdrop_path
                    } `}
                    alt={movie.name}
                />
            ))}
        </div>
        <div className="slider__arrow-right">
            <span className="arrow"
                onClick={() => {
                document.getElementById(id).scrollLeft += window.innerWidth - 80;
                }}
                >{">"}</span>
        </div>
      </div>
    </section>
  )
}

 

3. CSS 작성

 


 슬라이드 기능 추가하기 

: 화살표 방향 클릭 시 슬라이드 구현

 

1. 오른쪽 화살표

            <span className="arrow"
                onClick={() => {
                document.getElementById(id).scrollLeft += window.innerWidth - 80;
                }}
                >{">"}</span>

-> 영화 고유의 id 로 Element 를 찾는다.

-> scrollLeft : 요소의 콘텐츠가 왼쪽 가장자리에서 스크롤되는 픽셀 수

-> window.innerWidth : 안쪽의 가로폭

 

2. 왼쪽 화살표

            <span className="arrow"
                onClick={() => {
                document.getElementById(id).scrollLeft -= window.innerWidth - 80;
            }}
            >{"<"}</span>

=> 즉, onClick 이 발생했을 때 영화 이미지를 (window.innerWidth - 80) 만큼 왼쪽으로 옮긴다.

#오른쪽으로 옮길 경우에는 픽셀을 더해주면 된다.

 


 Styled Component를 이용해서 Footer 생성하기 

 

1.  UI 생성하기

export default function Footer() {
  return (
    <FooterContainer>
      <FooterContent>
        <FooterLinkContainer>
          <FooterLinkTitle>넷플릭스 대한민국</FooterLinkTitle>
          <FooterLinkContent>
            <FooterLink href="https://help.netflix.com/ko/node/412">
              넷플릭스 소개
            </FooterLink>
            <FooterLink href="https://help.netflix.com/ko">
              고객 센터
            </FooterLink>
            <FooterLink href="https://help.netflix.com/ko/">
              미디어 센터
            </FooterLink>
            <FooterLink href="https://help.netflix.com/ko/">
              이용 약관
            </FooterLink>
          </FooterLinkContent>
          <FooterDescContainer>
                <FooterDescRights>
                    Netflix Rights Reserved.
                </FooterDescRights>
          </FooterDescContainer>
        </FooterLinkContainer>
      </FooterContent>
    </FooterContainer>
  );
}

 

2. Styled componet 이용하기

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;
  }
`;

-> @media : 동적인 웹페이지를 만들 때 사용.

=> max-width 보다 작아졌을 때,  @media 안쪽 스타일링으로 바꾼다.

 

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;
  }
`;

-> FooterContent 는 스타일링이 없지만, 통일성을 위해 만들어준다.

 

const FooterLink = styled.a`
  color: gray;
  font-size: 14px;
  width: 110px;
  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;
`;

-> &:hover : 마우스를 올렸을 때

=> 마우스를 올렸을 때 hover 안쪽 스타일링으로 바꾼다.

 


 영화 자세히 보기 클릭 시 모달 생성하기 

1. 해당 영화 클릭 시 모달 OPEN

  const [ modalOpen, setModalOpen ] = useState(false);
  const [ movieSelected, setMovieSelected ] = useState({});
  const handleClick = (movie) => {
    setModalOpen(true);
    setMovieSelected(movie);
  }
          <img 
              key={movie.id}
              className={`row__poster ${isLargeRow && "row__posterLarge"}`}
              src={`https://image.tmdb.org/t/p/original/${
              isLargeRow ? movie.poster_path : movie.backdrop_path
              } `}
              alt={movie.name}
              onClick={()=> handleClick(movie)}
          />

 

2. 클릭 한 영화의 정보 가져오기

      {
        modalOpen && (
          <MovieModal {...setMovieSelected} setModalOpen={setModalOpen}/>
        )
      }

 

3. 모달 컴포넌트 생성

4. Props 가져오기

import React from 'react'
import "./MovieModal.css";

function MovieModal({
    backdrop_path,
    title,
    overview,
    name,
    release_date,
    first_air_date_,
    vote_average,
    setModalOpen
}) {
  return (
    <div>
      
    </div>
  )
}

export default MovieModal

 


 Movie 모달 UI 생성하기 

1. UI 생성하기

  return (
    <div className="presentation">
      <div className="wrapper-modal">
        <div className="modal">
          <span onClick={() => setModalOpen(false)} className="modal-close">
            X
          </span>

          <img
            className="modal__poster-img"
            src={`https://image.tmdb.org/t/p/original/${backdrop_path}`}
            alt="modal__poster-img"
          />

          <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>
  );
}
  .presentation {
    z-index: 1200;
    position: absolute;
  }

 

-> 다른 UI 보다 위로 올라오게끔 함

 

  .wrapper-modal {
    position: fixed;
    inset: 0px;
    background-color: rgb(0 0 0 / 71%);
    -webkit-tap-highlight-color: transparent;
    display: flex;
    justify-content: center;
  }

-> 화면 가운데에 위치하게끔 함

 

  .modal-close {
    position: absolute;
    right: 20px;
    top: 20px;
    cursor: pointer;
    z-index: 1000;
    color: white;
  }

-> X 버튼

-> 우측 상단에 위치하게끔 함

 


 React Router Dom 

1. React Router Dom 이란?

: React Router Dom 을 사용하면 웹 앱에서 동적 라우팅을 구현할 수 있습니다. 라우팅이 실행 중인 앱 외부의 구성에서 처리되는 기존 라우팅 아카텍처와 달리 React Router Dom 은 앱 및 플랫폼의 요구 사항에 따라 컴포넌트 기반 라우팅을 용이하게 함.

 

2. Single Page Application (SPA)

: 리액트는 SPA 기반이기 때문에 하나의 index.html 템플릿 파일을 가지고 있다. 이 하나의 템플릿에 자바스크립트를 이용해서 다른 컴포넌트를 이 index.html 템플릿에 넣으므로 페이지를 변경해주게 된다. 이때 이 React Router Dom 라이브러리가 새 컴포넌트로 라우팅/탐색을 하고 렌더링하는데 도움을 주게 된다.

 

3. React Router Dom 설치하기

-> npm install react-router-dom --save

 

4. React Router 설정하기

-> BrowserRouter 로 루트 컴포넌트 감싸주기

-> 여러 컴포넌트 생성 및 라우트 정의하기

-> <Link /> 를 이용해 경로를 이동하기


 React Router Dom APIs 

1. 중첩 라우팅 (Nested Routes)

: React Router 의 가장 강력한 기능 중 하나.

-> 레이아웃 코드를 어지럽힐 필요가 없다.

 

2. Outlet

: 자식 경로 요소를 렌더링하려면 부모 경로 요소에서 <Outlet> 을 사용해야 한다. 이렇게 하면 하위 경로가 렌더링될 때 중첩된 UI가 표시될 수 있다.

 

3. useNavigate

: 경로를 바꿔준다.

#navigate('/home') ==> localhost:3000/home 으로 간다.

 

4. useParams

: style 문법을 path 경로에 사용하였다면 useParams 를 사용할 수 있다.

 

5. useLocation

: 현재 위치 객체를 반환한다.

-> 현재 위치가 변경될 때마다 일부 side effect를 수행하려는 경우에 유용하다.

 

6.useRoutes

: <Routes>와 기능적으로 동일하나, <Route> 대신 JS 객체를 사용하여 경로를 정의한다.

-> 일반 <Route> 요소와 동일하나, JSX가 필요하지 않다.

#이번 앱 만들 때 사용X


 Netflix 앱에 React Router Dom 적용하기 

1. React Router Dom 이용해서 구현할 부분

-> 검색 페이지

: 영화에 관련된 단어를 타이핑하여 관련된 영화가 있으면 해당 영화의 포스터 표시

 

-> 디테일 페이지

: 검색 페이지에서 영화의 포스터를 클릭하면 해당 영화의 세부 정보를 보여주는 페이지

 

2. 페이지 생성을 위한 폴더 및 파일 추가

 

3. App.js 를 라우팅을 위한 파일로 변경

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

 

-> index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';


ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

reportWebVitals();

 

-> MainPage/index.js

import React from 'react'
import Banner from "../../components/Banner";
import Row from "../../components/Row";
import requests from '../../api/requests';

export default function MainPage() {
    return (
        <div>
            <Banner />
            <Row
            title="NETFLIX ORIGINALS"
            id="NO" 
            fetchUrl={requests.fetchNetflixOrignials}
            isLargeRow
            />

            <Row title="Trending Now" id="TN" fetchUrl={requests.fetchTrending}/>
            <Row title="Top Rated" id="TR" fetchUrl={requests.fetchTopRated}/>
            <Row title="Action Movies" id="AM" fetchUrl={requests.fetchActionMovies}/>
            <Row title="Comedy Movies" id="CM" fetchUrl={requests.fetchComedyMovies}/>

        </div>
    )
}

 useLocation을 이용한 검색 페이지 구현하기 

1. NavBar 에 검색 Input 생성

import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';
import "./Nav.css"
export default function Nav() {
    const [show, setShow] = useState(false);
    const [searchValue, setSearchValue] = useState(); //useState를 생성해준다.
    const navigate = useNavigate();

    useEffect(() => {
        window.addEventListener("scroll", () => {
            console.log('window.scrollY',window.scrollY);
            if(window.scrollY > 50) {
                setShow(true);
            } else {
                setShow(false);
            }
        })

        return () => {
            window.removeEventListener("scroll", () => {});
        };
    }, []);
    

    const handleChange = (e) => { //검섹창에 무언가를 칠 때마다 url주소창에 바로 반영이 된다.
        setSearchValue(e.target.value);
        navigate(`/search?q=${e.target.value}`)
    }

    return (
        <nav className={`nav ${show && "nav__black"}`}>
            <img
                alt='Netflix logo'
                src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Netflix_2015_logo.svg/400px-Netflix_2015_logo.svg.png"
                className='nav__logo'
                onClick={() => window.location.reload()}
            />

        <input 
        value={searchValue} 
        onChange={handleChange} 
        className="nav__input"
        type="text"
        placeholder='영화를 검색해주세요.'
        />

            <img
                alt="User logged"
                src="https://upload.wikimedia.org/wikipedia/commons/0/0b/Netflix-avatar.png?20201013161117"
                className='nav__avatar'
            />
        </nav>
    )
}

#CSS 코드는 복사해온다.

 

2. Search 페이지에서 SearchTerm 가져오기

-> SearchPage/index.js

import React from 'react'
import { useLocation } from 'react-router-dom'

export default function SearchPage() {
    console.log('useLocation()',useLocation());
    const useQuery =() => {
        return new URLSearchParams(useLocation().search)
    }

    let query = useQuery();
    const searchTerm = query.get("q")
    console.log('searchTerm',searchTerm);
    return (
        <div>SearchPage</div>
    )
}

 

3. SearchTerm 이 바뀔 때마다 새로 영화 데이터 가져오기

: 단어를 타이핑할 때마다 찾는 것이 달라지게끔 한다.

#Spider 에서 S / Sp / Spi 이런식으로 타이핑할 때마다 요청을 새로 보낸다.

 

-> SearchPage/index.js

import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'

export default 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);
        }
    }, [searchTerm]);

    const fetchSearchMovie = async (searchTerm) => { //try-catch문으로 요청을 보낼 때, error가 있으면 catch해준다.
        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)
        }

    }

    return (
        <div>SearchPage</div>
    )
}

 검색 페이지 UI 구현하기 

-> SearchPage/index.js

import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import "./SearchPage.css"

export default function SearchPage() {

    const [searchResults, setSearchResults] = useState([]); // searchResults 상태를 관리하는 state를 생성합니다.

const useQuery =() => { // URL의 query string을 가져오는 함수입니다.
    return new URLSearchParams(useLocation().search)
}

let query = useQuery(); // query 변수에 query string을 저장합니다.
const searchTerm = query.get("q") // searchTerm 변수에 검색어를 저장합니다.
console.log('searchTerm',searchTerm);

useEffect(() => { // 컴포넌트가 마운트되거나 searchTerm이 업데이트 될 때마다 실행됩니다.
    if(searchTerm) {
        fetchSearchMovie(searchTerm); // searchTerm이 있는 경우 fetchSearchMovie 함수를 호출합니다.
    }
}, [searchTerm]);

const fetchSearchMovie = async (searchTerm) => { // 검색어에 따른 영화 데이터를 가져오는 함수입니다.
    try {
        const request = await axios.get( // API 요청을 보냅니다.
            `/search/multi?include_adult=false&query=${searchTerm}`
        )
        console.log(request);
        setSearchResults(request.data.results); // 결과를 searchResults 상태에 저장합니다.
    } 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'>
                            <div
                            className="movie__column-poster"
                            >
                                <img
                                src={movieImageUrl} alt="movie"
                                className='movie__poster'
                                />
                            </div>
                        </div>
                    )
                }
            })}
        </section>
    ) : ( // 검색 결과가 없는 경우
        <section className='no-results'>
            <div className='no-results__text'>
                <p>찾고자 하는 검색어"{searchTerm}"에 맞는 영화가 없습니다.</p>
            </div>
        </section>
    )

}
return renderSearchResults(); // renderSearchResults 함수의 결과를 반환합니다.
}

#CSS 코드는 복사해온다.


 useDebounce Custom Hooks 만들기 

1. Debounce 란?

: 검색 입력에 입력할 때 입력 결과가 나타날 때까지 지연이 생기는데, 이 기능은 debounce 에 의해 제어된다. debounce 는 미리 결정된 시간 동안 사용자가 타이핑을 멈출 때까지 keyup 이벤트의 처리를 지연시킨다.
-> UI 코드가 모든 이벤트를 처리할 필요가 없어지고, 서버로 전송되는 API 호출 횟수도 크게 줄어든다. 입력된 모든 문자를 처리하면 성능이 저하되고 백엔드에 불필요한 로드가 추가될 수 있다.

#즉, Spider 를 검색할 때 Spi / der 이런식으로 타이핑을 중간에 멈춘다면, Spi / Spider 에 대한 요청만 보내는 것.

전에는 Spider 를 타이핑 하면 S / Sp / Spi / Spid ... 이런식으로 요청을 보냈다. 매우 비효율적.

 

2. hooks 폴더 및 파일 생성

-> useDebounce.js

import { useState, 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;
}

 

 

3. searchTerm => debouncedSearchTerm

-> SearchPage/index.js

import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useDebounce } from '../../hooks/useDebounce'; // useDebounce 커스텀 훅을 불러옵니다.
import "./SearchPage.css"

export default function SearchPage() {

    const [searchResults, setSearchResults] = useState([]);

const useQuery =() => {
    return new URLSearchParams(useLocation().search)
}

let query = useQuery();
const searchTerm = query.get("q")
const debouncedSearchTerm = useDebounce(searchTerm, 500); // 검색어를 500ms 간격으로 디바운스 합니다.

console.log('searchTerm',searchTerm);

useEffect(() => {
    if(debouncedSearchTerm) { // 디바운스된 검색어가 있을 경우
        fetchSearchMovie(debouncedSearchTerm); // 디바운스된 검색어를 이용해 영화를 검색합니다.
    }
}, [debouncedSearchTerm]);

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'
                                />
                            </div>
                        </div>
                    )
                }
            })}
        </section>
    ) : (
        <section className='no-results'>
            <div className='no-results__text'>
                <p>찾고자 하는 검색어"{debouncedSearchTerm}"에 맞는 영화가 없습니다.</p>
            </div>
        </section>
    )

}
return renderSearchResults();
}

 


 useParams를 이용한 영화 상세 페이지 구현하기 

1. 포스터 클릭 시 상세 페이지로

-> SearchPage/index.js

const navigate = useNavigate();
 <div className='movie' key = {movie.id}>
<div onClick={() => navigate(`/${movie.id}`)} className="movie__column-poster"> 

 

2. 상세 페이지에서 영화 상세 정보 가져오기

-> DetailPage/index.js

import React from 'react'
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from '../../api/axios';

export default function DetailPage() {
  const { movieId } = useParams();
  const[movie, setMovie] = useState({}); // 영화 정보 가져온 것을 state에 넣어줘야 한다.

  useEffect(() => {
    async function fetchData()  {
      const request = await axios.get (`/movie/${movieId}`);
      setMovie(request.data); // 정보들을 movie state에 넣어준다.
    }
    fetchData();
  }, [movieId]); // movieId가 바뀔 때마다 fetchData를 call해주기

  return (
    <div>DetailPage</div>
  )
}

 

3. UI 완성하기

  if(!movie) return <div>...loading</div>; // movie가 없을 때는 loading 텍스트 보여주기

  return <section>
  <img 
  className='modal__poster-img'
  src= {`https://image.tmdb.org/t/p/original/${movie.backdrop_path}`}
  alt="poster"
  />
  </section>
}

 모달 창 외부 클릭 시 모달 닫게 만드는 Custom Hooks 생성 

-> 어디를 클릭하는지 구분

-> react hooks 생성

-> 모달창 밖을 클릭하면 CallBack 함수를 호출하는 Event 를 등록

-> CallBack 함수 안에서 모달 닫아주기

=> useRef 를 이용해서 어디를 클릭하는지 구분할 수 있다.

 

1. useRef 란?

: 특정 Dom 을 선택할 때 사용하는 React Hooks

 

2. 특정 Dom 선택하기

 

3. Dom 을 직접 선택해야 하는 경우

-> 엘리먼트 크기를 가져와야 할 때

-> 스크롤바 위치를 가져와야 할 때

-> 엘리먼트에 포커스를 설정해줘야 할 때 등등

 

4. useRef 사용법

-> useRef() 를 통해 Ref 객체를 생성

-> 이 객체를 특정 Dom 에 ref 값으로 설정

-> Ref 객체의 .current 값이 특정 Dom 을 가르키게 된다.

 

5. React hooks 생성

 

6. 모달 창 바깥을 클릭하면 Callback 함수를 호출하는 Event 를 등록해주기

-> 클릭 시 모달 창 안이면 그냥 return

 

7. Callback 함수 안에서 모달 닫아주기

 

<코드>

-> useOnClickOutside.js

import React, { useEffect } from 'react'

const useOnClickOutside = (ref, handler) => {
    useEffect(() => {
        const listener = (event) =>{
            console.log('ref', ref.current) 
            if(!ref.current || ref.current.contains(event.target)) { 
                return; 
            }
            handler(); 
        }; 

        document.addEventListener("mousedown", listener) 
        document.addEventListener("touchstart", listener)
        return () => {
            document.addEventListener("mousedown", listener)
            document.addEventListener("touchstart", listener)
        };
    }, []);
};

export default useOnClickOutside;

 

-> MovieModal/index.js

import React, { useRef } from "react";
import useOnClickOutside from "../../hooks/useOnClickOutside";
import "./MovieModal.css";

function MovieModal({
  backdrop_path,
  title,
  overview,
  name,
  release_date,
  first_air_date,
  vote_average,
  setModalOpen,
}) 
{
  const ref = useRef()
  useOnClickOutside(ref, () => {setModalOpen(false)}); 

  return (
    <div className='presentation'>
      <div className="wrapper-modal">
        <div className="modal"ref={ref}> 
          <span onClick={()=> setModalOpen(false)} className='modal-close'>
            X
          </span>

          <img
            className='modal__poster-img'
            src={`https://image.tmdb.org/t/p/original/${backdrop_path}`}
            alt='modal__poster-img'
          />

          <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"></p>
          </div>
        </div>
      </div>
    </div>
    )
}

export default MovieModal;

 

 

 swiper 모듈을 이용한 터치 슬라이드 구현하기 

-> https://swiperjs.com/react#usage 

 

Swiper React Components

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

 npm install swiper --save

 

-> Row.js

import React, { useEffect, useState } from 'react'
import axios from '../api/axios';
import MovieModal from "./MovieModal";
import "./Row.css";

import { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';

import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';



export default function 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>
            <Swiper
                modules={[Navigation, Pagination, Scrollbar, A11y]}
                navigation // 화살표 버튼 사용 유무
                pagination={{ clickable: true }}
                loop={true} //loop 기능을 사용할 것인지, true로 하면 영화 목록의 마지막에 도달했을 때, 화살표를 누르면 처음으로 돌아간다.
                breakpoints={{
                    1378: {
                        slidesPerView: 6,
                        slidesPerGroup: 6,
                    },
                    998: {
                        slidesPerView: 5,
                        slidesPerGroup: 5,
                    },
                    625: {
                        slidesPerView: 4,
                        slidesPerGroup: 4,
                    },
                    0: {
                        slidesPerView: 3,
                        slidesPerGroup: 3,
                    },
                }}
            >
            

            <div id={id} className="row__posters">
                {movies.map(movie => (
                    <SwiperSlide>
                        <img
                            key={movie.id}
                            className={`row__poster ${isLargeRow && "new__posterLarge"}`}
                            src={`https://image.tmdb.org/t/p/original/${
                                isLargeRow ? movie.poster_path : movie.backdrop_path
                            } `}
                            alt={movie.name}
                            onClick={() => handleClick(movie)} 
                        />
                    </SwiperSlide>
                ))}
            </div>
        </Swiper>

            {
                modalOpen && (
                    <MovieModal {...movieSelected} setModalOpen={setModalOpen}/> 
                )
            }

        </section>
        
    )
}

#CSS도 수정해주어야 한다.

 


 github를 이용해서 배포하기 

1. 깃허브 저장소 생성

 

2. API_KEY 환경변수로 숨기기

-> .env 는 깃허브에 올라가지 않는다. 즉, .env 파일을 생성하여 이 안에서 환경변수를 만든다.

이후 axios 에서 API_KEY 부분을 수정해주면 된다.

 

-> axios.js

import axios from "axios";

const instance = axios.create({
    baseURL: "https://api.themoviedb.org/3",
    params: {
        api_key : process.env.REACT_APP_MOVIE_DB_API_KEY,
        language: "ko-KR",
    },
});

export default instance;

 

3. 로컬 앱과 저장소 연결

 

4. gh-pages 모듈 설치

npm install gh-pages --save-dev

 

5. 홈페이지 url 작성

-> package.json

homepage : "https://깃허브 유저 이름.github.io/저장소 이름/"

 

6. 배포를 위한 script 추가

-> package.json

 "predeploy": "npm run build", 
 "deploy": "gh-pages -d build",

 

7. react router dom 의 기본 경로 변경

-> index.js

ReactDOM.render(
  <BrowserRouter basename='저장소의 이름'>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

 

8. deploy 시작

npm run deploy