개발/React&Redux

[React] 영화 추천 사이트 제작하기 (2)

xuwon 2024. 11. 8. 01:00

2번째 미션도 무사히 마쳤습니다!
(비록 현재 시간은 23시 46분이지만...)

 

2단계 미션은 바로...

TMdb API를 이용한 영화 웹 애플리케이션 확장

입니다!!

 

이제 더미데이터가 아닌, 실제 API를 활용해서 데이터를 받아올 거예요.

오늘 구현한 기능은~

1. TMdb API Key를 발급받고 .env에 저장하여 환경변수로 이용
2. API 호출해서 더미데이터를 응답 데이터로 변경
3. 클릭한 MovieCard에 맞는 MovieDetail로 이동하도록 라우팅
4. NavBar 만들기
5. 회원가입, 로그 페이지 레이아웃 만들기
6. 반응형 Navbar 구현하기 (모바일 모드에서 햄버거 노출시키기)
7. 더보기 버튼으로 데이터 더 받아오기

입니다!! 하나씩 해봅시다.

 


그 전에, TMDB API가 뭔지 알아볼게요.

 TMDB API(The Movie Database API)는 영화, TV 프로그램, 배우 등의 정보를 제공하는 API

라고 합니다!

- TMDB는 방대한 영화 데이터베이스를 제공하며, 영화 웹사이트나 앱을 개발할 때 유용하게 쓰인다.
- 영화 정보 검색, 인기 영화 목록, 장르별 영화 검색, 영화 세부 정보 등 다양한 엔드포인트를 제공한다.

- 주요 엔드포인트

더보기

// 영화
/movie/popular, /movie/top_rated, /movie/now_playing, /movie/upcoming, /movie/{movie_id}

// TV 프로그램
/tv/popular, /tv/top_rated, /tv/{tv_id}

// 검색
/search/movie, /search/tv, /search/person

// 장르 정보
/genre/movie/list, /genre/tv/list

 

 

1. TMdb API Key를 발급받고 .env에 저장하여 환경변수로 이용

 

 

The Movie Database (TMDB)

환영합니다 수백만 개의 영화, TV 프로그램 및 인물을 발견하세요. 지금 살펴보세요.

www.themoviedb.org

API 키는 여기서 발급받을 수 있답니다.

저는 .env 파일을 생성해서
VITE_TMDB_API_KEY에 API 키를 할당했습니다.
(.env 파일은 .gitignore에도 추가해 줍니다.)

 

 

// main.jsx

const apiKey = import.meta.env.VITE_TMDB_API_KEY;
const [movieListDatas, setMovieListDatas] = useState([]);
const [page, setPage] = useState(1); // 페이지 수

useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(`https://api.themoviedb.org/3/movie/popular?api_key=${apiKey}&page=${page}&language=ko-KR`)
                const data = await response.json()

                if (page === 1) {
                    setMovieListDatas(data.results)
                } else {
                    setMovieListDatas((prev) => [...prev, ...data.results])
                }
            } catch (error) {
                console.error('Error fetching data:', error);
            }
        }

        fetchData();
}, [page]) // page 수가 바뀔 때마다 데이터 더 불러오기

json 파일에서 import 해오던 코드는 지우고,
fetch를 통해 진짜 데이터를 받아옵니다!
/movie/popular 엔드포인트로 인기 순으로 나열된 영화 데이터들을 받아왔어요.

page는 데이터의 특정 페이지를 나타내는 매개변수입니다.
더보기 버튼을 구현할 때 사용했어요.

더보기 버튼을 누르면 page가 1씩 증가되고, 그에 따라 useEffect가 실행되어 새로운 데이터를 받아 movieListDatas에 업데이트 합니다.

  setMovieListDatas((prev) => [...prev, ...data.results])  
이렇게 이전 데이터에 새로운 데이터를 더해서 새로 업데이트 해줬습니닷.

page가 1일 때 조건문을 건 이유는, 자꾸 데이터가 중복으로 들어와서... 이유는 모르겠는데 ㅠㅠ
그래서 걸어줬습니다 하하

const [movieListDatas, setMovieListDatas] = useState([]);
  const apiKey = import.meta.env.VITE_TMDB_API_KEY;

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(`https://api.themoviedb.org/3/movie/top_rated?api_key=${apiKey}&language=ko-KR`)
        const data = await response.json()

        setMovieListDatas(data.results)
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    }

    fetchData();
}, [])

MovieSlider 컴포넌트도 실제 데이터로 갈아끼웁니다.
슬라이더에는 /movie/top_rated 엔드포인트를 통해 평점 높은 순으로 데이터를 20개만 받아왔습니다.


이렇게 데이터를 갈아끼우면, 같은 이름으로 받아왔기 때문에 더미 데이터 활용할 때 썼던 변수 그대로 사용이 가능합니다.
스타일도 쫌쫌따리 고쳐주면... 끝!

 

3. 클릭한 MovieCard에 맞는 MovieDetail로 이동하도록 라우팅

각 영화에 맞는 상세 설명 데이터를 불러와서 Detail 페이지에 적용시키면 됩니다.

function App() {
  const [isStyle, setIsStyle] = useState(false);

  return (
    <div>
        <Header isStyle={isStyle} />
        <Routes>
            <Route path="/" element={<Main />} /> {/* 홈 페이지 */}
            <Route path="/details/:id" element={<MovieDetail />} /> {/* 상세 페이지 - id를 받아오기 */}
            <Route path="/signup" element={<Signup setIsStyle={setIsStyle}/>} />
            <Route path="/login" element={<Login setIsStyle={setIsStyle}/>}/>
        </Routes>
    </div>
  );
}

라우팅 설정을 먼저 해줍니다!
/details 경로에 :id 파라미터를 추가하여 특정 영화의 세부 정보 페이지로 접근할 수 있도록 설정합니다.

회원가입이랑 로그인 컴포넌트도 라우팅 설정 끝!

 

// MovieDetail.jsx

const [movieDetailDatas, setMovieDetailDatas] = useState({});
    const apiKey = import.meta.env.VITE_TMDB_API_KEY;
    const { id } = useParams(); // id값에 접근

    useEffect(() => {
        fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=ko-KR`)
          .then(response => response.json())
          .then(data => setMovieDetailDatas(data))
}, [id]) // id가 바뀔 때마다 새로 데이터 받아오기

그리고 MovieDetail 컴포넌트에서 iduseParams를 통해 받아옵니다.
/movie/{movie_id} 엔드포인트에 갖고 온 id를 전달해주면 각 id에 맞는 영화의 상세 설명 데이터가 받아집니다!

요기도 역시 같은 변수명을 사용하였기에 바로 잘 적용이 됩니다.

 


데이터만 바꿔주면 돼서 조금은 수월했던 것 같아요 ㅎㅎ;;

 

4. NavBar 만들기

요거는 이미 어제 만들어서 패스할게용

 

5. 회원가입, 로그인 페이지 레이아웃 만들기

// Login.jsx, Signup.jsx

import { useEffect, useState } from "react"
import '../App.scss';
import '../index.css';

export default function Login ({ setIsStyle }) {
    const [email, setEmail] = useState('')
    const [password, setPassword] = useState('')
    
    useEffect(() => {
        setIsStyle(true); // 컴포넌트가 렌더링되면 스타일 적용
    
        return () => setIsStyle(false); // 컴포넌트가 사라질 때 스타일 제거
    }, [setIsStyle]);

    return (
        <div className="w-1000px h-[calc(100vh-200px)] flex flex-col items-center justify-center gap-[50px] login">
            <p className="text-[35px]">Log In</p>
            <div>
                <p>Email</p>
                <input 
                    type="email" 
                    value={email} 
                    onChange={(e) => setEmail(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <div>
                <p>Password</p>
                <input 
                    type="password" 
                    value={password} 
                    onChange={(e) => setPassword(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <button className="w-[400px] h-[40px] border border-black">Log In</button>
        </div>
    )
}

import { useEffect, useState } from "react"
import '../App.scss';
import '../index.css';

export default function Signup ({ setIsStyle }) {
    useEffect(() => {
        setIsStyle(true); // ComponentA가 렌더링되면 스타일 적용
    
        return () => setIsStyle(false); // 컴포넌트가 사라질 때 스타일 제거
    }, [setIsStyle]);


    const [name, setName] = useState('')
    const [email, setEmail] = useState('')
    const [password, setPassword] = useState('')
    const [confirmPassword, setConfirmPassword] = useState('')


    return (
        <div className="w-full h-[calc(100vh-200px)] flex flex-col items-center justify-center gap-[50px] signup">
            <p className="text-[35px]">Sign Up</p>
            <div>
                <p>Name</p>
                <input 
                    type="text" 
                    value={name} 
                    onChange={(e) => setName(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <div>
                <p>Email</p>
                <input 
                    type="email" 
                    value={email} 
                    onChange={(e) => setEmail(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <div>
                <p>Password</p>
                <input 
                    type="password" 
                    value={password} 
                    onChange={(e) => setPassword(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <div>
                <p>Confirm Password</p>
                <input 
                    type="password" 
                    value={confirmPassword} 
                    onChange={(e) => setConfirmPassword(e.target.value)} 
                    className="w-[400px] h-[30px] border border-black"
                />
            </div>
            <button className="w-[400px] h-[40px] border border-black">Sign Up</button>
        </div>
    )
}

그냥 대충 모양만 만들고, 값들은 모두 상태로 관리해줬습니다.
setIsStyle 역시 스타일 지정을 위한 거라...

그리고 로그인 버튼과 회원가입 버튼에 컴포넌트들을 연결하여 페이지 이동이 가능하도록 만들었습니당.

 


분명히 시간은 오래 걸린 거 같은데...
솔직히 무한 스크롤 구현하려다 시간 다 썼습니다 ㅠ_ㅠ

6. 반응형 Navbar 구현하기 (모바일 모드에서 햄버거 노출시키기)

이렇게 모바일 모드로 보면 햄버거가 생깁니다!
그리고 이 햄버거를 누르면 메뉴판이 나오는데, 어떤 의도인지는 모르겠어요...

// Hamburger.jsx

import { useState } from "react"
import '../App.scss'

export default function Hamburger() {
    const [onMenu, setOnMenu] = useState(false);

    return (
        <>
            <p 
                className="text-[25px]"
                onClick={() => {
                    setOnMenu(!onMenu);
                }}
            >🍔</p>
            {onMenu ? <HamburgerMenu /> : null}
        </>
    )
}

function HamburgerMenu () {
    return (
        <div className="
            w-[200px] h-[200px] bg-[white] rounded-2xl
            absolute top-[80px] right-[100px] z-10 hamburger
            flex flex-col items-center justify-center gap-2
        ">
            <p className="text-[blue] text-[20px] font-bold">🍔 메뉴 🍔</p>
            <p className="text-black">🍖 불고기 버거 🍖</p>
            <p className="text-black">🧀 모짜렐라 인더버거 🧀</p>
            <p className="text-black">🦐 통새우 와퍼 🦐</p>
            <p className="text-black">🔥 상하이 스파이시 버거 🔥</p>
        </div>
    )
}

우선 귀염뽀짝한 Hamburger 컴포넌트를 만들어줬습니다

햄버거를 누르면 메뉴판이 토글되어야 해서
onMenu를 상태로 관리해줍니당.
(false일 땐 메뉴판 off, true일 땐 메뉴판 on)

return (
        <div 
            className="w-[100vw] h-[100px] xs:h-[200px] border bg-black flex items-center text-white justify-between px-9"
            style={isStyle ? {marginRight: '5px'} : {}}
        >
            <p className="text-[35px] ozMovieText" 
                onClick={() => {
                    navigate('/')
                }}
            >XU Movie</p>
            <div className="flex items-center gap-5">
                {windowWidth < 500 ? <Hamburger /> : null}
                <div className="flex flex-row gap-4 headerBtns">
                    <button className="bg-[#ff3dff] w-[80px] h-[35px] rounded-md"
                        onClick={() => {
                            navigate('/login')
                        }}
                    >로그인</button>
                    <button className="bg-[#ff3dff] w-[80px] h-[35px] rounded-md"
                        onClick={() => {
                            navigate('/signup')
                        }}
                    >회원가입</button>
                </div>
            </div>
        </div>
)

이 햄버거 컴포넌트는 Header 컴포넌트에서 호출합니다!
모바일 모드에서만 지원이 되니까 windowWidth가 500보다 작을 때에만 호출되도록 했어요.

그럼 windowWidth는 실시간으로 변하는 값이어야 하니, 상태로 관리해 주어야겠죠.

const [windowWidth, setWindowWidth] = useState(window.innerWidth);
    // 처음 렌더링 될 때 자꾸 햄버거 생김

useEffect(() => { // windowWidth 업데이트
        const handleSize = () => {
            setWindowWidth(window.innerWidth)
        }

        window.addEventListener('resize', handleSize)

        return () => window.removeEventListener('resize', handleSize); // cleanup
}, [windowWidth])

초기값으로 처음엔 0을 줬었는데, 처음 렌더링 될 때 windowWidth가 0으로 초기화돼서
모바일 모드가 아닌데도 햄버거가 생기더라구요. (물론 resize하면 지워지긴 함)

그래서 초기값으로 window.innerWidth를 주었답니다.


useEffect의 의존성 배열로 windowWidth를 주어, windowWidth가 바뀔 때마다 useEffect가 실행되도록 하였고,
resize 이벤트에 핸들러를 등록해서 업데이트 되도록 했습니다!!

컴포넌트가 꺼졌을 때 cleanup 함수도 까먹지 않고 적어줬습니다.

 

7. 더보기 버튼으로 데이터 더 받아오기


오래 걸렸습니다...

원래 무한 스크롤로 구현하려 했는데,
스크롤이 맨 밑에 도달했다는 걸 코드로 어떻게 짜야할지 모르겠어서 그냥 더보기 버튼으로 노선 틀었습니다.

<button 
    onClick={() => setPage(prev => prev + 1)}
    className="w-[80%] h-[50px] bg-[black] text-white rounded-2xl addBtn"
>더보기</button>

아까 page가 1씩 증가하면 데이터를 더 받아와서 추가한다고 언급했듯이,
그냥 더보기 버튼에 클릭 이벤트 발생 시 page를 증가시켜주는 함수만 넣어주면 됩니다!

 


이렇게 끝~~
나머지는 그냥 반응형 웹 좀 만져주고, 스타일 수정하고 이게 다입니다!

참고로 stylescsstailwindCSS 사용 중입니다.

tailwindCSS가 처음엔 코드가 너무 지저분해 보였는데,
너무 편하고... 오히려 클래스명 찾아 헤매지 않아도 돼서 편해요.

scss도 미디어 쿼리 적용할 때 편리하게 작성이 돼서 좋아용

 

내일은 또 어떤 미션이 올지..... ㅠ,ㅠ

https://github.com/xuuwon/React-moviepage

 

GitHub - xuuwon/React-moviepage

Contribute to xuuwon/React-moviepage development by creating an account on GitHub.

github.com