React

 

 

 

 

John Ahn

 

인프런   ==>     https://www.inflearn.com/course/따라하며-배우는-노드-리액트-영화사이트-만들기#reviews

 

 

유튜브 강의 목록 :   https://www.youtube.com/playlist?list=PL9a7QRYt5fqkowXUgTj_tbkFClsPhO5XV

 

강의 파일 : https://braverokmc79.github.io/react_movie_clone/movie_clone.html

 

Boiler Plate  소스 

https://github.com/jaewonhimnae/boilerplate-mern-stack

https://github.com/braverokmc79/react_boiler_plate

 

완성본 소스 (John Ahn)  :  https://github.com/jaewonhimnae/react-movie-app-ko

 

소스 :  https://github.com/braverokmc79/react_movie_clone

 

강의 목록

1. 소개 영상
2. Boiler-Plate & MongoDB 연결 
3. The MovieDB API 설명 
4. Landing Page 만들기 (1) 
5. Grid Card Component 
6. Load More Button 만들기 
7. Movie Detail 페이지 만들기 
8. 영화 출연진들 가져오기 
9. Favorite 버튼 만들기 (1) 
10. Favorite 버튼 만들기 (2) 
11. Favorite Button 만들기 (3) 
12. Favorite list에 추가 삭제 
13. Favorite 페이지 만들기 (1) 
14. Favorite 페이지 (2) 
15. 강의 마무리 

 

vscode  확장 패키지 추가

1. - Auto Import - ES6, TS, JSX, TSX

2. - Reactjs code snippets

3. - ESLint

4. - Prettier - Code formatter

Visual Studio Code 폴더/파일 아이콘 변경하기

 

리액트  프로젝트 생성

 npx create-react-app  경로

예) npx create-react-app E:\react-app2

 

 

넥스트 프로젝트 생성

$ npx create-next-app nextjs-tutorial

 

 

 

1. 소개 영상

 

 

 

 

 

 

 

 

 

2. Boiler-Plate & MongoDB 연결 

 

 

React 유튜브 클론 시리즈 ( John Ahn) ) 1 - Boiler Plate 설정

https://macaronics.net/m04/react/view/1911

 

 

 

 

 

Boiler Plate  소스 

1) https://github.com/jaewonhimnae/boilerplate-mern-stack

2) https://github.com/braverokmc79/react_boiler_plate

 

1 또는 2 번 소스 다운로드 받은 후 진행

 

 

2번 소스인 경우

# 실행 방법

1. server 에 디렉토리에서 yarn install

2. client 에 디렉토리에서 yarn install

3. server 에 디렉토리에 npm run dev

 

 

config 디렉토리에서 dev.js 파일 생성

몽고 DB 

module.exports = {
    mongoURI: "mongodb+srv://macaronics:<password>@mongo-macaronics.y37mjuf.mongodb.net/movie_app"
}

 

 

 

 

 

 

 

3. The MovieDB API 설명 

 

 

 

 

themoviedb  사이트 등록 하기

=> https://www.themoviedb.org/

 

 

리액트 :  client/src/components/Config.js  

export const USER_SERVER = '/api/users';

export const API_URL = "https://api.themoviedb.org/3/";

export const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/";

 

 

 

문서

https://www.themoviedb.org/documentation/api/discover

 

 

 

 

 

 

4. Landing Page 만들기 (1) 

 

 

 

 

 

 

 

Config.js

export const USER_SERVER = '/api/users';

export const API_URL = "https://api.themoviedb.org/3/";

export const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/";

 

MovieApiKey.js

// API 키(v3 auth)
export const MOVIE_API_KEY = "키값";
// API 요청 예
// https://api.themoviedb.org/3/movie/550?api_key=08d90cc4e7968b1f8e51588a0d42cf06

// API 읽기 액세스 토큰(v4 auth)
export const MOVIE_API_TOKEN = "토큰키";

 

 

LandingPage.js

import React, { useEffect } from 'react'
import { API_URL, IMAGE_BASE_URL } from '../../Config';
import { MOVIE_API_KEY } from '../../MovieApiKey';
import { useState } from 'react';
import MainImage from './Sections/MainImage';

function LandingPage() {

    const [Movies, setMovies] = useState([]);
    const [MainMovieImage, setMainMovieImage] = useState(null);

    useEffect(() => {

        const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`;
        fetch(endPoint)
            .then(res => res.json())
            .then(data => {
                setMovies(data.results);
                setMainMovieImage(data.results[0]);

            })
            .catch(err => {
                console.log("에러 :", err);
            });


    }, []);


    return (
        <div style={{ width: '100%', margin: '0' }}>

            {/* Main Image */}

            {MainMovieImage &&
                <MainImage
                    title={MainMovieImage.title}
                    text={MainMovieImage.overview}
                    image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} />
            }

            <div style={{ width: '85%', margin: '1rem auth', position: "relative", left: "7.5%" }}>
                <h2>Movies by latest</h2>
                <hr />
                {
                    Movies.map((movie, index) => (
                        <div key={index}>
                            <img src={`${IMAGE_BASE_URL}w500${movie.backdrop_path}`} />
                        </div>
                    ))

                }

                {/* Movie Grid Cards */}
            </div>



            <div style={{ display: 'flex', justifyContent: 'center' }}>
                <button>Load More</button>
            </div>

        </div>
    )
}

export default LandingPage

 

 

MainImage.js

import React from 'react'

function MainImage(props) {


    return (
        <div style={{
            background:
                `linear-gradient(to bottom, rgba(0,0,0,0) 39%, rgba(0,0,0,0) 41%, rgba(0,0,0,0.65) 100%), url('${props.image}') , #1c1c1c`,
            backgroundSize: "100% 100%",
            height: "500px", backgroundPosition: 'center, center',
            width: '100%', position: 'relative'
        }}>

            <div style={{ position: "absolute", maxWidth: '500px', bottom: '2rem', marginLeft: '2rem' }}>
                <h2 style={{ color: 'white' }}>{props.title}</h2>
                <p style={{ color: 'white', fontSize: '1rem' }}>{props.text}</p>
            </div>
        </div>
    )
}


export default MainImage

 

 

 

 

 

 

5. Grid Card Component  

 

 

 

 

 

 

LandingPage.js

import React, { useEffect } from 'react'
import { API_URL, IMAGE_BASE_URL } from '../../Config';
import { MOVIE_API_KEY } from '../../MovieApiKey';
import { useState } from 'react';
import MainImage from './Sections/MainImage';
import GridCards from './../commons/GridCards';
import { Row } from 'antd';

function LandingPage() {

    const [Movies, setMovies] = useState([]);
    const [MainMovieImage, setMainMovieImage] = useState(null);

    useEffect(() => {

        const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`;
        fetch(endPoint)
            .then(res => res.json())
            .then(data => {
                setMovies(data.results);
                setMainMovieImage(data.results[0]);
                console.log(data.results);

            })
            .catch(err => {
                console.log("에러 :", err);
            });


    }, []);


    return (
        <div style={{ width: '100%', margin: '0' }}>

            {/* Main Image */}

            {MainMovieImage &&
                <MainImage
                    title={MainMovieImage.title}
                    text={MainMovieImage.overview}
                    image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} />
            }

            <div style={{ width: '85%', margin: '1rem auth', position: "relative", left: "7.5%" }}>
                <h2>Movies by latest</h2>
                <hr />

                {/* Movie Grid Cards */}

                <Row gutter={[16, 48]} >
                    {Movies &&
                        Movies.map((movie, index) => (
                            <React.Fragment key={index}>
                                <GridCards
                                    image={movie.poster_path ?
                                        `${IMAGE_BASE_URL}w500${movie.poster_path}` : null}
                                    movieId={movie.id}
                                    movieName={movie.original_title}
                                />

                            </React.Fragment>
                        ))

                    }

                </Row>
            </div>



            <div style={{ display: 'flex', justifyContent: 'center' }}>
                <button>Load More</button>
            </div>

        </div>
    )
}

export default LandingPage

 

MainImage.js

import React from 'react'

function MainImage(props) {

    return (
        <div style={{
            background:
                `linear-gradient(to bottom, rgba(0,0,0,0) 39%, rgba(0,0,0,0) 41%, rgba(0,0,0,0.65) 100%), url('${props.image}') , #1c1c1c`,
            backgroundSize: "100% 100%",
            height: "500px", backgroundPosition: 'center, center',
            width: '100%', position: 'relative'
        }}>

            <div style={{ position: "absolute", maxWidth: '500px', bottom: '2rem', marginLeft: '2rem' }}>
                <h2 style={{ color: 'white' }}>{props.title}</h2>
                <p style={{ color: 'white', fontSize: '1rem' }}>{props.text}</p>
            </div>
        </div>
    )
}


export default MainImage

 

 

GridCards.js

import React from 'react';
import { Col } from 'antd';
import { Link } from 'react-router-dom';
import style from './GridCards.css';

function GridCards(props) {
    return (
        <Col lg={6} md={8} xs={24}>
            <div style={{ position: 'relative' }} >
                <Link to={`/movie/${props.movieId}`}>
                    <img src={props.image} alt={props.movieName} className="img"
                        style={{
                            width: "80%",
                            height: "auto"
                        }}
                    />
                </Link>
            </div>
        </Col>
    )
}

export default GridCards

 

GridCards.css

.img{
    box-shadow: 5px 5px 5px rgb(0 0 0 / 20%);
}

.img:hover{
    box-shadow: 5px 5px 5px rgb(0 0 0 / 80%);
}

 

 

 

 

 

 

6. Load More Button 만들기 

 

LandingPage.js

import React, { useEffect } from 'react'
import { API_URL, IMAGE_BASE_URL } from '../../Config';
import { MOVIE_API_KEY } from '../../MovieApiKey';
import { useState } from 'react';
import MainImage from './Sections/MainImage';
import GridCards from './../commons/GridCards';
import { Row, Button } from 'antd';

function LandingPage() {

    const [Movies, setMovies] = useState([]);
    const [MainMovieImage, setMainMovieImage] = useState(null);
    const [CurrentPage, setCurrentPage] = useState(0);

    useEffect(() => {

        const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`;
        fetchMovies(endPoint);

    }, []);


    const fetchMovies = (endPoint) => {

        fetch(endPoint)
            .then(res => res.json())
            .then(data => {
                setMovies([...Movies, ...data.results]);
                setMainMovieImage(data.results[0]);
                setCurrentPage(data.page);

            })
            .catch(err => {
                console.log("에러 :", err);
            });

    }


    const loadMoreItems = () => {
        const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=${CurrentPage + 1}`;
        fetchMovies(endPoint);
    }


    return (
        <>
            <div style={{ width: '100%', margin: '0' }}>
                {MainMovieImage &&
                    <MainImage
                        title={MainMovieImage.title}
                        text={MainMovieImage.overview}
                        image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} />
                }
            </div>

            <div style={{ width: '100%', margin: '0', padding: '10px 5%' }}>


                <div style={{ width: '100%', margin: '1rem auth', }}>
                    <h2>Movies by latest</h2>
                    <hr />

                    {/* Movie Grid Cards */}

                    <Row gutter={[16, 48]} >
                        {Movies &&
                            Movies.map((movie, index) => (
                                <React.Fragment key={index}>
                                    {movie.poster_path &&
                                        <GridCards path={movie.poster_path}
                                            image={movie.poster_path ?
                                                `${IMAGE_BASE_URL}w500${movie.poster_path}` : null}
                                            movieId={movie.id}
                                            movieName={movie.original_title}
                                        />
                                    }
                                </React.Fragment>
                            ))

                        }

                    </Row>


                    <div style={{ textAlign: 'center', margin: "100px 0" }}>
                        <Button onClick={loadMoreItems}>더보기</Button>
                    </div>

                </div>



            </div>
        </>
    )
}

export default LandingPage

 

 

 

 

 

 

 

7. Movie Detail 페이지 만들기 

 

 


 

 

   

      https://api.themoviedb.org/3/      

      ${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}

  1. adult: false
  2. backdrop_path: "/wDyM1lIIgK4RIDAgr8iuZe9N1cf.jpg"
  3. belongs_to_collection: null
  4. budget: 0
  5. genres: (4) [{…}, {…}, {…}, {…}]
  6. homepage: "https://www.netflix.com/title/81186049"
  7. id: 755566
  8. imdb_id: "tt13314558"
  9. original_language: "en"
  10. original_title: "Day Shift"
  11. overview: "An LA vampire hunter has a week to come up with the cash to pay for his kid's tuition and braces. Trying to make a living these days just might kill him."
  12. popularity: 1320.051
  13. poster_path: "/bI7lGR5HuYlENlp11brKUAaPHuO.jpg"
  14. production_companies: (2) [{…}, {…}]
  15. production_countries: [{…}]
  16. release_date: "2022-08-10"
  17. revenue: 0
  18. runtime: 113
  19. spoken_languages: [{…}]
  20. status: "Released"
  21. tagline: "Some jobs really go for the throat."
  22. title: "Day Shift"
  23. video: false
  24. vote_average: 6.91
  25. vote_count: 912

 

MovieInfo.js

import React from 'react'
import { Descriptions, Badge } from 'antd';

function MovieInfo(props) {
    let { movie } = props;
    return (
        <Descriptions title="Movie Info" bordered>
            <Descriptions.Item label="Title">{movie.original_title}</Descriptions.Item>
            <Descriptions.Item label="release_date">{movie.release_date}</Descriptions.Item>
            <Descriptions.Item label="revenue">{movie.revenue}</Descriptions.Item>
            <Descriptions.Item label="runtime">{movie.runtime}</Descriptions.Item>
            <Descriptions.Item label="vote_average" span={2}>
                {movie.vote_average}
            </Descriptions.Item>
            <Descriptions.Item label="vote_count">{movie.vote_count}</Descriptions.Item>
            <Descriptions.Item label="status">{movie.status}</Descriptions.Item>
            <Descriptions.Item label="popularity">{movie.popularity}</Descriptions.Item>
        </Descriptions>
    )
}

export default MovieInfo

 

 

MovieDetail.js

import { Button } from 'antd';
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { API_URL, IMAGE_BASE_URL } from '../../Config';
import { MOVIE_API_KEY } from '../../MovieApiKey';
import MainImage from '../LandingPage/Sections/MainImage';
import MovieInfo from './Sections/MovieInfo';

function MovieDetail(props) {

    const { movieId } = useParams();
    const [Movie, setMovie] = useState([])
    useEffect(() => {

        let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${MOVIE_API_KEY}`;

        let endpointInfo = `${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}`;

        fetch(endpointInfo)
            .then(res => res.json())
            .then(data => {
                console.log("data: ", data);
                setMovie(data);

            }).catch(error => {
                console.error("에러 : ", error);
            })


    }, [])


    return (
        <div>

            {/* Header */}



            <div style={{ width: '100%', margin: '0' }}>
                {Movie &&
                    <MainImage
                        title={Movie.title}
                        text={Movie.overview}
                        image={`${IMAGE_BASE_URL}original${Movie.backdrop_path}`} />
                }
            </div>



            {/* Body */}
            <div style={{ width: '85%', margin: '1rem auto' }}>

                {/* Movie Info */}
                <MovieInfo movie={Movie} />



                <br />
                {/* Actors Grid */}

                <div style={{ display: 'flex', justifyContent: 'center', margin: '2rem' }}>
                    <Button>Toggle Actor View </Button>
                </div>

            </div>





        </div>
    )
}

export default MovieDetail

 

 

 

 

 

 

8. 영화 출연진들 가져오기 

 

 

 

 

src\components\views\LandingPage\LandingPage.js

  landingPage={true} 추가


~~


                 <GridCards path={movie.poster_path}
                                            landingPage={true}
                                            image={movie.poster_path ?
                                                `${IMAGE_BASE_URL}w500${movie.poster_path}` : null}
                                            movieId={movie.id}
                                            movieName={movie.original_title}
                                        />


~~

 

 

 

 

src\components\views\MovieDetail\MovieDetail.js

import { Button, Row } from 'antd';
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { API_URL, IMAGE_BASE_URL } from '../../Config';
import { MOVIE_API_KEY } from '../../MovieApiKey';
import GridCards from '../commons/GridCards';
import MainImage from '../LandingPage/Sections/MainImage';
import MovieInfo from './Sections/MovieInfo';


function MovieDetail(props) {

    const { movieId } = useParams();
    const [Movie, setMovie] = useState([]);
    const [Casts, setCasts] = useState([]);
    const [ActorToggle, setActorToggle] = useState(false);

    useEffect(() => {

        let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${MOVIE_API_KEY}`;

        let endpointInfo = `${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}`;

        fetch(endpointInfo)
            .then(res => res.json())
            .then(data => {
                console.log("endpointInfo: ", data);
                setMovie(data);
            }).catch(error => {
                console.error("에러 : ", error);
            })


        fetch(endpointCrew)
            .then(res => res.json())
            .then(data => {
                console.log("endpointCrew  - cast: ", data.cast);
                setCasts(data.cast);
            }).catch(error => {
                console.error("에러 : ", error);
            })

    }, [])


    return (
        <div>

            {/* Header */}



            <div style={{ width: '100%', margin: '0' }}>
                {Movie && Movie.backdrop_path !== undefined &&
                    <MainImage
                        title={Movie.title}
                        text={Movie.overview}
                        image={`${IMAGE_BASE_URL}original/${Movie.backdrop_path}`} />
                }
            </div>



            {/* Body */}
            <div style={{ width: '85%', margin: '1rem auto' }}>

                {/* Movie Info */}
                <MovieInfo movie={Movie} />



                <br />
                {/* Actors Grid */}

                <div style={{ display: 'flex', justifyContent: 'center', margin: '2rem' }}>
                    <Button onClick={() => (setActorToggle(!ActorToggle))}>
                        Toggle Actor View
                    </Button>
                </div>

                {/* 
                console.log("endpointCrew  - cast: ", data.cast);
                adult: false
                cast_id: 1
                character: ""
                credit_id: "5f8f1cec383df20034aab300"
                gender: 2
                id: 134
                known_for_department: "Acting"
                name: "Jamie Foxx"
                order: 0
                original_name: "Jamie Foxx"
                popularity: 33.058
                profile_path: "/hPwCMEq6jLAidsXAX5BfoYgIfg2.jpg" */}

                {ActorToggle &&
                    <Row gutter={[16, 48]} >
                        {Casts &&
                            Casts.map((cast, index) => (
                                <React.Fragment key={index}>
                                    {cast.profile_path &&
                                        <GridCards path={cast.profile_path}
                                            image={cast.profile_path ?
                                                `${IMAGE_BASE_URL}w500${cast.profile_path}` : null}
                                            characterName={cast.name}
                                        />
                                    }
                                </React.Fragment>
                            ))

                        }
                    </Row>
                }

            </div>


        </div>
    )
}

export default MovieDetail

 

 

 

 

 

src\components\views\commons\GridCards.js

import React from 'react';
import { Col } from 'antd';
import { Link } from 'react-router-dom';
import style from './GridCards.css';

function GridCards(props) {

    if (props.landingPage) {
        return (
            <Col lg={6} md={8} xs={24}>
                <div style={{ position: 'relative', textAlign: 'center' }} >
                    <Link to={`/movie/${props.movieId}`}>
                        <img src={props.image} alt={props.movieName} className="img"
                            style={{ width: "80%", height: "auto", maxHeight: '400px' }} />
                    </Link>
                </div>
            </Col>
        )


    } else {
        return (
            <Col lg={6} md={8} xs={24}>
                <div style={{ position: 'relative', textAlign: 'center' }} >
                    <img src={props.image} alt={props.characterName} className="img"
                        style={{ width: "80%", height: "auto", maxHeight: '400px' }} />
                </div>
            </Col>
        )
    }

}

export default GridCards

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

대부분의 사람들로서는 거의 환상적이거나 상식 밖의 일로 여겨지는 것이 오히려 가장 깊은 현실의 본질을 나타내는 것일 수가 있다. 일상적인 사건을 그저 피상적으로 관찰하는 것이 사실주의일 수 없을 뿐 아니라, 오히려 전혀 그 반대인 것이다. -도스토예프스키

댓글 ( 4)

댓글 남기기

작성