ABOUT ME

Today
Yesterday
Total
  • 5. Netflix 앱 완성하기
    React 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
Designed by Tistory.