개인공부/FE

[React] 넷플릭스 메인 화면 클론 코딩

파뱁 2022. 3. 11. 22:42
728x90

넷플릭스 메인화면을 React로 클론 코딩해보려 한다.

넷플릭스 안 본지 진짜 오래되었는데... 컨텐츠 말고 맨날 메인 화면만 보고 있었던 탓인지 메인화면은 대충 떠오르는 것 같다. (솔직히 뭐 보지 고민하느라 메인화면 보는 시간이 실제 컨텐츠 보는 시간 보다 길었던거.. 나만 그런거 아니지)

 

기본적으로 아래의 영상을 기반으로 한 클론 코딩이다.

https://www.youtube.com/watch?v=XtMThy8QKqU 

 


TMDB의 영화 정보 API를 이용해서 메인 화면에 영화 정보를 넷플릭스의 메인화면 처럼 보여주는 코드를 구성했다.

React 공부에 치중을 둔 프로젝트인 만큼 실제 서비스의 동작은 하지 않는다.

 

우선 기본 index 페이지 코드는 다음과 같다.

 

</public/index.html>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>Netfleact</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

이 코드에서는 후에 우리가 사용할 루트 id를 가진 div를 선언해주었다.

 

이후 화면을 구성하는 것은 상단 네비게이션(Nav), 랜덤한 영화를 크게 띄워주는 배너(Banner), 영화를 카테고리에 따라 나열한 Row이다.

 

이들 각각은 함수형 컴포넌트로 만들어서 최상단인 App.js에서 불러와 화면에 띄울 것이다.

 

최상단인 App.js는 다음과 같다.

 

</src/App.js>

import './App.css';
import Banner from './components/Banner';
import Nav from './components/Nav';
import Row from './components/Row';
import requests from './requests';

function App() {
  return (
    <div className="App">
      {/* Nav */}
      <Nav/>
      <Banner/>
      {/* 별도의 할당을 props에 하지 않으면 true */}
      <Row title="Netflix Originals" fetchUrl={requests.fetchNetflixOriginals} isLargeRow/>
      <Row title="Trending Now" fetchUrl={requests.fetchTrending} />
      <Row title="Top Rated" fetchUrl={requests.fetchTopRated}/>
      <Row title="Action Movies" fetchUrl={requests.fetchActionMovies}/>
      <Row title="Comedy Movies" fetchUrl={requests.fetchComedyMovies}/>
      <Row title="Horror Movies" fetchUrl={requests.fetchHorrorMovies}/>
      <Row title="Romance Movies" fetchUrl={requests.fetchRomanceMovies}/>
      <Row title="Documentaries" fetchUrl={requests.fetchDocumentaries}/>
    </div>
  );
}

export default App;

여기서 Row는 각 카테고리별로 title값을 부여해서 해당하는 영화를 불러올 예정이다.

 

다음 3개의 코드는 순서대로 Nav, Banner, Row 컴포넌트에 해당하는 코드들이다.

 

</src/components/Nav.jsx>

import React, { useState, useEffect } from 'react';
import './Nav.css';


function Nav() {
    const [show, handleShow] = useState(false);

    useEffect(() => {
        window.addEventListener('scroll', () => {
            if(window.scrollY > 100) {
                handleShow(true);
            } else handleShow(false);
        });
        return () => {
            window.removeEventListener('scroll');
        };
    }, []);

    return (
        <div className={`nav ${show && "nav__black"}`}>
            <img 
            className="nav__logo"
            src="/images/netflix-logo.png" alt="Netflix Logo" />

            <img 
            className="nav__avatar"
            src="/images/netflix-profile.jpg" alt="Netflix profile" />
        </div>
    );
};

export default Nav;

 

</src/components/Banner.jsx>

import React, { useState, useEffect } from 'react'

// axios.js 모듈을 import하는 코드
import axios from '../axios';
import requests from '../requests';
import './Banner.css';


const Banner = () => {
    const [movie, setMovie] = useState([]); // 초기값 : 빈 배열

    // useEffect() hook
    useEffect(() => {
        // 화면이 초기에 렌더링된 직후 한 번 호출.

        // API 서버에 데이터 요청하는 부분.
        async function fetchData() {// async, await

            // 비동기 요청으로 받아온 응답 데이터 
            const request = await axios.get(requests.fetchNetflixOriginals);

            const randomMovie = request.data.results[
                Math.floor(Math.random() * request.data.results.length - 1)
              ];
            setMovie(randomMovie);
        }

        fetchData();
    }, []);

    function truncate(str, n) {
        return str?.length > n ? str.substr(0, n - 1) + "..." : str;
    }

  return (
    <header className='banner' style={{
        backgroundSize: "cover",
        backgroundImage: `url(https://image.tmdb.org/t/p/original/${movie?.backdrop_path})`,
        backgroundPosition: "center center"
    }}>
        {/* Background image */}
        <div className="banner__contents">
            {/* 옵셔널 체이닝? = "?" */}
            <h1 className="banner__title">{movie?.title || movie?.name || movie?.original_name}</h1>

            {/* div.banner__buttons > div.banner__button*2 */}
            <div className="banner__buttons">
                <button className="banner__button">Play</button>
                <button className="banner__button">My List</button>
            </div>

            {/* description */}
            <h1 className="banner__description">
                {truncate(movie?.overview, 150)}
            </h1>
        </div>

        <div className="banner--fadeBottom"></div>
    </header>
  )
}

export default Banner;

 

<src/components/Row.jsx>

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

const baseUrl = 'https://image.tmdb.org/t/p/original/';

const Row = (props) => {

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

    useEffect(() => {

        // fetchData()가 비동기적으로 동작하도록 명시
        async function fetchData(){
            const request = await axios.get(props.fetchUrl);

            setMovies(request.data.results);
            return request;
        }
        fetchData();
    }, []);

  return (
    <div className='row'>
        <h2>{props.title}</h2>

        <div className="row__posters">
            {/* 여러장의 포스터 */}
            {movies.map((movie) => (
                <img key={movie.id} className={`row__poster ${props.isLargeRow && "row__posterLarge"}`}
                    src={`${baseUrl}${props.isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name} />
            ))}

        </div>
    </div>
  )
}

export default Row

 

여기서 이제 외부 API를 가져오는 작업인 Axios를 활용해서 코드를 구성하려면 아래의 코드가 추가로 필요하다.

 

</src/axios.js>

import axios from 'axios'; // 실제로 설치한 axios 라이브러리를 임포트 하는 코드

const instance = axios.create({
    baseURL: 'https://api.themoviedb.org/3',
});

export default instance;

// 만약에 instance.get('/foo-bar')
// https://api.themoviedb.org/3/foo-bar 이렇게 요청하게 됨

</src/requests.js>

const API_KEY = 'TMDB에서 부여받은 api 키';

const requests = {
    fetchTrending: `/trending/all/week?api_key=${API_KEY}&language=ko`,
    fetchNetflixOriginals: `/discover/tv?api_key=${API_KEY}&with_networks=213`,
    fetchTopRated: `/movie/top_rated?api_key=${API_KEY}&language=ko`,
    fetchActionMovies: `/discover/movie?api_key=${API_KEY}&with_genres=28`,
    fetchComedyMovies: `/discover/movie?api_key=${API_KEY}&with_genres=35`,
    fetchHorrorMovies: `/discover/movie?api_key=${API_KEY}&with_genres=27`,
    fetchRomanceMovies: `/discover/movie?api_key=${API_KEY}&with_genres=10749`,
    fetchDocumentaries: `/discover/movie?api_key=${API_KEY}&with_genres=99`,
}

export default requests;

위 코드에서 API_KEY에는 TMDB에서 개인적으로 부여받은 api 키를 문자열로 넣어주면 된다.

 

이렇게 완성한 페이지는 최종적으로 아래와 같이 보여진다.

제법 넷플릭스 같은 걸?!

 

이 프로젝트에서 사용한 CSS는 아래 나열했다.

 

</src/App.css>

* {
  margin: 0;
}

.App {
  background-color: #111;
}

 

</src/components/Nav.css>

.nav {
    position: fixed;
    top: 0;
    width: 100%;
    padding: 20px;
    height: 30px;
    z-index: 1;

    display: flex;
    justify-content: space-between;

    /* Animations */
    transition-timing-function: ease-in;
    transition: all 0.5s;
}

.nav__black {
    background-color: #111;
}

.nav__logo {
    position: fixed;
    left: 20px;
    width: 80px;
    object-fit: contain;
}

.nav__avatar {
    position: fixed;
    right: 20px;
    width: 30px;
    object-fit: contain;
}

 

<src/components/Banner.css>

.banner {
    color: white;
    object-fit: contain;
    height: 448px;
   }
   
   .banner__contents {
       margin-left: 30px;
       padding-top: 140px;
       height: 190px;
   }
   
   .banner__title {
       font-size: 3rem;
       font-weight: 800;
       padding-bottom: 0.3rem;
   }
   
   .banner__description {
       width: 45rem;
       line-height: 1.3;
       padding-top: 1rem;
       font-size: 0.8rem;
       max-width: 360px;
       height: 80px;
   }
   
   .banner__button {
       cursor: pointer;
       color: #fff;
       outline: none; /* ? */
       border: none;
       font-weight: 700;
       border-radius: 0.2vw;
       padding-left: 2rem;
       padding-right: 2rem;
       margin-right: 1rem;
       padding-top: 0.5rem;
       background-color: rgba(51, 51, 51, 0.5);
       padding-bottom: 0.5rem;
   }
   
   .banner__button:hover {
       color: #000;
       background-color: #e6e6e6;
       transition: all 0.2s;
   }
   
   .banner--fadeBottom {
       height: 7.4rem;
       background-image: linear-gradient(
           180deg,
           transparent,
           rgba(37, 37, 37, 0.61),
           #111
       );
   }

 

</src/components/Row.css>

.row {
    margin-left: 20px;
    color: white;
}

.row__posters {
    display: flex;
    overflow-y: hidden;
    overflow-x: scroll;
    padding: 20px;
}

.row__posters::-webkit-scrollbar {/* 모든 브라우저 설정 */
    display: none;
}

.row__poster {
    width: 100%; /* 이미지 width 100% */
    max-height: 100px; /* 이미지 최대 높이 */
    object-fit: contain; /* 이미지 비율 유지 */
    margin-right: 10px;
    transition: transform 450ms;
}

.row__poster:hover {
    transform: scale(1.08);
}

.row__posterLarge {
    max-height: 250px;
}

.row__posterLarge:hover {
    transform: scale(1.09);
    opacity: 1;
}

 

사용한 이미지는 다음 2개이다.

로고 같은 경우 투명 배경을 구하지 못해 그냥 검은 배경인 로고를 그대로 사용했다.

 

</public/images/netflix-logo.png>

로고 이미지

</public/images/netflix-profile.jpg>

(입 돌아간) 사용자 이미지&nbsp;

 


함수형 컴포넌트를 복습한 프로젝트 였다.

axios도 써보고,, 좋은 경험이었다. 이게.. 되나..? 했는데 다 쓰고 보니 제법 그럴 듯해서 뿌듯했다.

이제 리액트로 미니 프로젝트 하나 더 하면 백엔드로 넘어간다.

짧은 시간이었지만.. 프론트 기술 많이 배운 것같다.

자바 스크립트 알러지도.. 어느 정도 극복한 것 같아 다행이다.

 

뿌엥

 

728x90
반응형