귀여운 눈높이에서 작성된, 🐰

하루 한 걸음씩, 성장 하는 중 입니다 🫶🏻

Studying/미니 프로젝트

mini project - 영화 페이지 만들기 (2단계)

creamymood 2025. 5. 5. 02:25

떠나고 싶포요..

 

 


 

2단계 내용 대충 휘익~ 훑고

api 받아와서 그거 데이터 바꾸는거구만 쉽네 ~ 했는데

오산 중에 오산. 왕 어렵잖하 ? ...

 

그냥 뚝딱 되는게 아니었음.. 

 

하지만 재밌다. ㅋ 프론트엔드 체질 .. ? 

 

useEffect며, fetch며, filter, map .. params, 등등등....

리액트에서 중요한 개념은 다 꼭 알아야할 아주 어렵고 중요한 파트 였다.. ⭐︎

 

역시나, 부캠에서 제시해준 단계별 구현 단계가 있어서 크---게 어렵지는 않았다.

프로젝트 시작 전에, 이렇게 구현 단계를 정리해보는 습관을 가져야겠다.

 


일단 큰 구성은 아래 처럼 했다.

 

1. api 받아오기 (api 받아올 비동기 함수는 컴포넌트 분리 따로 하지 않았음)

2. 받아온 api를 상태로 값 저장하고

3. 그 값을 map을 통해 우선 화면에 뿌려주기

4. 그리고 filter를 통해서 걸러낸 걸 다시 map으로 뿌려주는걸로.

(오! 근데 url에서 직접 필터링하는 쿼리 파라미터를 제공하는 api사이트도 있다. 해당 프로젝트에서 이용한 사이트도 마찬가지.)

 

4. 영화 id값에 맞는 디테일 페이지로 가주기 ~


구현단계 1 🐰

내가 한 방식 :1단계는 크게 어렵지 않게,, 시키는 대로 토큰 발급 받고 .env파일에 저장:)

요구 사항

  • TMdb API 읽기 액세스 토큰를 발급받고 프로젝트 루트 디렉토리에 .env 파일을 생성하여 저장합니다.
  • .env 파일을 .gitignore 파일에 추가하여 .env 파일이 github에 업로드 되지 않도록 설정합니다.
  • 환경변수로 API 읽기 액세스 토큰를 이용할 수 있도록 설정합니다.
더보기

.evn파일이란?

 

.env 파일은 환경 변수(environment variables) 를 저장하는 파일이에요. 보통 애플리케이션에서 필요한 비밀번호, API 키, 데이터베이스 연결 정보 등 중요한 정보를 코드에 직접 넣지 않고 외부 파일에 저장하여 관리하는 데 사용됩니다.

.env 파일의 특징:

  1. 비공개 정보 관리: API 키나 비밀번호와 같은 민감한 정보를 코드에 하드코딩하지 않고 .env 파일에 저장하여 보안성을 높입니다.
  2. 환경 설정: 여러 환경(개발, 배포 등)에서 사용할 설정을 쉽게 관리할 수 있습니다. 예를 들어, 개발 환경에서는 localhost를, 배포 환경에서는 실제 서버 주소를 사용할 수 있죠.
  3. 프로젝트 외부에서 관리: .env 파일은 코드와 분리되어 있기 때문에 코드만 공유해도, 중요한 정보는 다른 사람에게 노출되지 않아요.

예시:

API_KEY=your-api-key-here
DB_PASSWORD=your-database-password

어떻게 사용하나요?

  1. .env 파일 만들기: 프로젝트 루트 디렉토리에 .env 파일을 만들어 위와 같이 환경 변수를 정의합니다.
  2. 환경 변수 읽기: Node.js나 Python 같은 언어에서는 dotenv 패키지를 사용해 .env 파일을 읽고, 코드에서 해당 값을 사용할 수 있게 합니다.

 

주의사항:

  • .env 파일은 버전 관리 시스템(Git 등) 에 추적되지 않도록 .gitignore에 추가해야 합니다.
  • .env 파일은 보안상 중요한 정보가 포함되므로, 외부에 유출되지 않도록 주의해야 합니다.

.env 파일을 사용하는 것은 코드와 환경 설정을 분리하여 보안과 관리의 편리함을 주는 좋은 방법입니다!

 

Q : 이 때, env파일에 내가 적을 키 값의 형식이 따로 있을까 ?

> 이번에 내가 적은 코드 : ex :

//Api key (read only)
API_KEY = '여기에다가 발급받은 키 작성'
더보기

네, .env 파일에 적는 키 값 형식에는 몇 가지 규칙이 있습니다.

보통 KEY=VALUE 형식으로 저장하는데, 이때 몇 가지 중요한 점들이 있어요.

.env 파일 형식 규칙:

  1. 키와 값은 =로 구분:
    • KEY=VALUE
    • 예: API_KEY=your-api-key-here
  2. 공백 없이 작성:
    • 키와 값 사이에 공백을 넣지 않아야 해요. 예를 들어, API_KEY = your-api-key-here처럼 공백을 넣으면 안 됩니다.
  3. 주석:
    • 주석을 추가하려면 #을 사용합니다. 주석은 값에 영향을 미치지 않아요.
    • 예: # This is a comment
  4. 따옴표 사용:
    • 값에 공백이나 특수 문자가 포함될 수 있으면, 값을 따옴표(' 또는 ")로 감쌀 수 있습니다.
    • 예: DATABASE_URL="postgres://user:password@localhost:5432/mydatabase"
  5. 멀티라인 값:
    • 값이 길어서 여러 줄로 작성해야 할 경우에는 \로 줄을 바꿀 수 있습니다.
    • 예:
    • LONG_TEXT="This is a very long text \ that continues to the next line"

예시 .env 파일:

# API key for the service
API_KEY=your-api-key-here

# Database credentials
DB_HOST=localhost
DB_USER=myusername
DB_PASSWORD=mysecurepassword
DB_NAME=mydatabase

# URL for external service
EXTERNAL_API_URL="https://api.example.com/v1"

환경 변수 사용:

  1. 빈 값: 값을 비워두고 싶은 경우, KEY= 처럼 값을 공백으로 설정할 수 있어요.
    • 예: DEBUG_MODE=
  2. 특수 문자: 특수 문자가 포함된 값을 사용할 때는 반드시 따옴표로 감싸는 것이 좋습니다. 예를 들어, URL이나 패스워드에 &나 = 같은 문자가 포함될 수 있어요.
    • 예: API_SECRET="my$ecreTpa$$word"

 

Q: 근데 .gitignore파일에 적을 때 .env 그냥 이렇게만 적으면 돼 ?

더보기

"읽기 토큰(Read token)"도 결국에는 문자열이기 때문에, 일반적인 .env 파일의 KEY=VALUE 형식을 따르면 됩니다.

예를 들어, GitHub API 같은 곳에서 받은 읽기 전용 토큰이라면 이렇게 저장할 수 있어요:

READ_TOKEN=your-read-token-value-here

이름 짓는 팁:

  • 대문자로 작성하는 게 일반적인 관례입니다.
  • 언더스코어(_) 로 구분합니다.
  • 읽기 전용임을 알리고 싶다면 READ_ONLY_TOKEN 또는 PUBLIC_API_TOKEN처럼 더 구체적인 이름도 좋아요.

예시:

READ_TOKEN=abc123xyz
READ_ONLY_TOKEN=sk_test_123456789
PUBLIC_API_TOKEN=ghp_xxxreadonlyxxx

 

💡 보안 중요 포인트: 이 토큰이 비록 읽기 전용이라도, 외부에 노출되면 공개 정보 조회나 요금 발생 등 문제가 생길 수 있어요, 반드시 .gitignore에 .env를 포함시키는 걸 잊지 마세요!

 


구현단계 2 🐰 : API 호출해서 더미데이터를 응답 데이터로 변경 및 화면에 렌더링

내가 한 방식 :

데이터 받아오기 위해, api로 패치해오는데 그걸 useState를 써서, 처음 컴포넌트가 마운트 될 때 한번 패치해오고.

그 데이터 정보로써, 무비카드에 하나하나 맵 돌리면서 뿌릴건데,

그 무비카드 컴포넌트를 따로 만들었기 때문에 프롭스로 전달해줄 거고!

아무튼 그 무비카드 컴포넌트도 그냥 쫘락 맵으로 뿌리는게 아니고 필터를 사용해서 성인 영화는 분리해줬고. 그렇게 필터링한 값으로 맵돌린다.

 

 

요구 사항

  • TMdb API를 사용하여 영화를 검색하고, 응답 데이터를 이용하여 메인 페이지를 구성합니다.
  • 영화 데이터를 메인 페이지에 표시할 때, adult 값이 **false**인 영화만 필터링하여 제공합니다.
    • 이를 통해 성인 콘텐츠를 제외하고, 사용자에게 안전한 영화 데이터를 노출할 수 있습니다.

 

1. API 데이터 패치: useState와 useEffect를 활용하여 컴포넌트가 마운트될 때 데이터 한 번 패치.
2. MovieCard 컴포넌트로 데이터 전달: 받은 데이터를 MovieCard 컴포넌트에 프롭스로 전달하고, map을 통해 각 영화를 표시.
3. 필터링: 성인 영화를 제외하거나 특정 조건에 맞는 데이터를 필터링하여 표시.

 

2단계 완성. 작성한 코드는 아래와 같습니다.

function Home() {

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

      useEffect(()=>{

        const options = {
          method: 'GET',
          headers: {
            accept: 'application/json',
            Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhNDVlYjcyMzNmYmQ1ZDQyMWU1ZGZkODkxMzZjZDIxMiIsIm5iZiI6MTc0NjI1MjE4OS44MzksInN1YiI6IjY4MTViMTlkMGM3ODNiODVmZTkwN2YwNCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.KqsWPPvivWInRUx-3jc9ptyzddgYaKciT7aDe2U7HvI'
          }
        };
        
        fetch('https://api.themoviedb.org/3/movie/popular?language=en-US&page=1', options)
          .then(res => res.json())
          .then(data => {
            console.log(data)
            setMovies(data.results)
          })
          .catch(err => console.error(err));


      },[])
      
    return (
      <>
     
          {<main>
                
                  <div className="flex flex-wrap ">
                  {movies.filter(movie => movie.adult === false)
                          .map((movie)=> {
                            return(
                              <Link to={`details/${movie.id}`}>
                              <MovieCard key={movie.id}
                                poster = {movie.poster_path}
                                title = {movie.title}
                                rating = {movie.vote_average}
                              
                              />
                            </Link>
                          )
                          })}
                </div>
             
          </main> }
        
     </>
    )
  }
 

 


오 근데 이건 그럼 어디 작성해 ? app??

 

네, 맞아요! 보통 이 코드는 App.js나 다른 컴포넌트 파일에 작성하면 됩니다.

useEffect와 fetch를 사용한 데이터 요청 로직은 React 컴포넌트 안에 넣어서 실행할 수 있기 때문에,

이를 App.js에서 구현하거나, 별도의 컴포넌트를 만들어서 사용할 수 있습니다.

 

예시 1: App.js에 직접 작성하기

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

const App = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const options = {
      method: 'GET',
      headers: {
        accept: 'application/json',
        Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhNDVlYjcyMzNmYmQ1ZDQyMWU1ZGZkODkxMzZjZDIxMiIsIm5iZiI6MTc0NjI1MjE4OS44MzksInN1YiI6IjY4MTViMTlkMGM3ODNiODVmZTkwN2YwNCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.KqsWPPvivWInRUx-3jc9ptyzddgYaKciT7aDe2U7HvI'
      }
    };

    fetch('https://api.themoviedb.org/3/authentication', options)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  ///
 

예시 2: 별도의 컴포넌트로 분리하기

데이터 요청과 로직을 별도의 컴포넌트로 분리하여 App.js에서 불러올 수도 있습니다.

이렇게 하면 코드가 더 깔끔해지고, 재사용 가능하게 됩니다.

MovieAPI.js (별도의 컴포넌트로 분리)

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

const MovieAPI = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const options = {
      method: 'GET',
      headers: {
        accept: 'application/json',
        Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhNDVlYjcyMzNmYmQ1ZDQyMWU1ZGZkODkxMzZjZDIxMiIsIm5iZiI6MTc0NjI1MjE4OS44MzksInN1YiI6IjY4MTViMTlkMGM3ODNiODVmZTkwN2YwNCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.KqsWPPvivWInRUx-3jc9ptyzddgYaKciT7aDe2U7HvI'
      }
    };

    fetch('https://api.themoviedb.org/3/authentication', options)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);
///
 

App.js에서 불러오기

import React from 'react';
import MovieAPI from './MovieAPI';  // MovieAPI 컴포넌트 불러오기

const App = () => {
  return (
    <div>
      <h1>Welcome to the Movie API App!</h1>
      <MovieAPI />  {/* MovieAPI 컴포넌트를 화면에 렌더링 */}
    </div>
  );
};

export default App;

요약:

  1. App.js에 직접 작성: 데이터를 직접 App.js에서 요청하고 처리하는 방법입니다.
  2. 컴포넌트 분리: 데이터를 요청하는 부분을 MovieAPI.js와 같은 별도의 컴포넌트로 분리하여 App.js에서 불러오는 방법입니다. 이렇게 하면 코드가 더 깔끔하고 재사용 가능해집니다.

두 가지 방법 모두 사용 가능합니다. 코드의 규모가 커지면 두 번째 방식처럼 컴포넌트를 분리해서 사용하는 것이 더 좋습니다!

 

>>우선 나는 분리하지 않고 사용했지만, 아마 3단계나 4단계에서 분리하지 않을까? 하는 생각.


구현 단계 중 하나인 

"TMdb API를 사용하여 인기 영화 목록을 가져와서 더미 데이터로 사용했던 movieListData.json 파일을 실제 API 응답 데이터로 대체합니다."

그러면.. 일단 기존 더미데이터 제이슨 파일은 삭제하고,  내가 컴포넌트를 분리 하고 싶다면 새로운컴포넌트를 만들어서.. 그 데이터를 이제 맵을 돌리던가 하면 되는건가?

CORRECT !

기존 흐름 (더미 데이터 사용)

  1. movieListData.json을 import로 불러옴
  2. Home 컴포넌트에서 그 데이터를 movies.map()으로 순회
  3. 각 Movie 컴포넌트에 props로 전달해서 렌더링

지금 흐름 (API로 데이터 받기)

  1. 데이터를 받아오는 비동기 함수 (예: fetchMovies)를 useEffect 안에 작성
    → 이건 별도 컴포넌트에서 작성할 수도 있고, Home 안에서 직접 작성할 수도 있어요
  2. 받아온 데이터를 상태로 저장 (useState)
    → 예: const [movies, setMovies] = useState([]);
  3. Home 컴포넌트에서 movies.map()으로 순회
    →  기존 더미 데이터처럼 똑같이 Movie 컴포넌트에 props로 전달해서 렌더링

 

⭐︎ 핵심

  • 더미 데이터 대신 API 응답 결과(data.results)를 setMovies로 상태에 넣고
  • 그 상태 변수(movies)를 그대로 map() 돌려서 UI에 출력하는 것!

지금처럼 구성하면, 예전처럼 더미 데이터에서 온 것처럼 map() 돌리기만 하면 됩니다.
컴포넌트 분리는 자유롭게 할 수 있고, 데이터 받아오는 부분만 useEffect에서 잘 처리하면 돼요.


오~ 그런데 호출 분리를 하려고 보니까, 지금은 이해하기 조금 어려운 난관에 부딪혔다.

예를 들면,

useEffect(() => {
  const fetchMovies = async () => {
    const res = await fetch('...');
    const data = await res.json();
    setMovies(data.results);
  };

  fetchMovies();
}, []);

이런 fetchMovies 비동기 함수 부분을 따로 컴포넌트 분리 해서 사용하고 싶었는데.

그렇다면 내 예상 코드는..

useEffect(() => {
  fetchMovies();
  };

  fetchMovies();
}, []);

이런 식이었는데 .. ? 

 

실제로 api함수를 분리하게 되면 아래와 같은 코드다

// api.js
export const fetchMovies = async () => {
  const res = await fetch('...');
  const data = await res.json();
  return data.results;
};
// Home.jsx
import { useState, useEffect } from 'react';
import { fetchMovies } from './api';

const Home = () => {
  const [movies, setMovies] = useState([]);

  useEffect(() => {
    const loadData = async () => {
      const results = await fetchMovies();
      setMovies(results);
    };

    loadData();
  }, []);

  return (
    <div>
      {movies.map(movie => (
        <p key={movie.id}>{movie.title}</p>
      ))}
    </div>
  );
};

여기서 Home 컴포넌트 안에 loadData가 갑자기 생뚱맞다는 것.

moviesFatch가 비동기 함순데 왜 또 다시 ??

 

그랬더니, 이유는 아래와 같다.

 

더보기

 fetchMovies 자체가 비동기 함수니까 useEffect 안에서 그냥 fetchMovies()만 호출하면 되는 거 아니야? 

라는 의문이 들 수 있어요. 그런데 중요한 이유 때문에 loadData() 같은 중간 async 함수를 다시 만들고 호출하는 거예요.

 왜 useEffect 안에서 바로 await fetchMovies() 를 못 쓰는 걸까?

useEffect 함수 자체는 동기 함수여야 해요.
즉, useEffect(() => { ... }, []) 안에 들어가는 콜백 함수는 async를 직접 붙일 수 없어요.

예를 들어 이건  잘못된 코드예요:

useEffect(async () => {
  const results = await fetchMovies();
  setMovies(results);
}, []);
  • 이렇게 쓰면 React가 useEffect의 반환값을 Promise로 오해해서 경고를 줘요.
  • useEffect는 클린업(clean-up) 함수를 반환하기 때문에, Promise를 반환하면 안 돼요.

 

그래서 이렇게 쓰는 거예요:

useEffect(() => {
  const loadData = async () => {
    const results = await fetchMovies();
    setMovies(results);
  };

  loadData(); // 비동기 함수는 이렇게 별도로 정의하고 호출
}, []);
  • loadData()는 그냥 useEffect 안에서 사용하는 임시 async 함수
  • fetchMovies()는 외부에서 가져온 비동기 API 함수
  • 그리고 setMovies(results)로 상태 업데이트!

 


그래서 지금은 분리 없이 그냥 App에서 바로, 호출 함수도 작성 할 예정

 

맞아요, 작은 프로젝트에서는 굳이 API 함수나 더미 데이터를 별도 파일로 분리하지 않고,

Home 컴포넌트 안에서 직접 데이터 호출 함수를 작성해서 사용하는 방식도 충분히 괜찮아요.

💡추후 규모가 커지면?

  • api/ 폴더를 만들어서 fetchData.js 같은 파일로 분리하거나
  • data/ 폴더에 dummyData.js 파일로 더미 데이터 관리하면 유지보수에 좋음.

하지만 지금처럼 단순한 구조에서는 컴포넌트 안에서 직접 데이터 처리하는 방식도 매우 실용적이에요.

 

 

API관련 새롭게 알게 된 것

갑자기 궁금한게.. api로 데이터를 받아와서 내 프로젝트를 만들잖아?

근데 그 api를 주는 사이트에서 만약 데이터 변화가 일어난다면, 그게 내 프로젝트에도 계속 동기화가 되는건가 ?

 

그니까 내 말은, 일단 데이터 받아온 뒤 그걸 내 프로젝트를 구성한다.

근데 그 api를 제공하는 (예를 들어 영화 사이트)에서 다른 유저들이 영화 정보를 추가 한다거나 할때..

내 프로젝트에도 그게 반영이 되나 ?  이거였음.

 

 

아직 더 공부가 필요하지만..

 

더보기

정확한 대답은:

 → "네 프로젝트가 API를 통해 ‘매번 새로 데이터를 받아오도록 구성돼 있다면’, 영화 정보가 추가되면 너의 프로젝트에도 반영돼. 하지만 한 번 받아온 데이터를 그냥 저장만 해놓고 쓰면, 새로 추가된 정보는 반영되지 않아."

 

 

 상황:

  • 너는 어떤 영화 사이트 API에서 영화 정보를 가져옴
  • 그걸 기반으로 웹페이지나 앱을 만들었음

그런데:

  • 다른 유저들이 그 영화 사이트에서 새 영화를 등록함

👩🏻‍💻 너의 프로젝트가 이런 구조라면:

// 예시: 버튼을 누르거나, 페이지 열릴 때마다 데이터를 API에서 가져오는 경우
axios.get("https://api.movie-site.com/movies").then((res) => {
  // 최신 영화 데이터로 화면 구성
});

→ 이런 경우라면, 그 사이트에 영화가 새로 추가되면 네 프로젝트도 API 요청 시마다 자동으로 반영됨!

 반면 이런 구조라면:

  • 한 번만 API 호출해서 가져온 데이터를 JSON으로 저장하거나, 데이터베이스에 저장해놓고 계속 그것만 씀
     👉 이 경우에는 영화 사이트에서 정보가 바뀌어도 네 프로젝트는 그걸 모르고 반영 못 해.

 

axios로 데이터 받는 것과 JSON 파일 저장하는 것의 차이

 axios로 데이터를 받는 경우 (실시간 요청)

  • axios로 API 요청을 보낼 때 → 서버에서 실시간으로 데이터를 받아와.
  • 그 데이터는 사실 JSON 형식으로 반환되지만, 중요한 점은 매번 API를 호출할 때마다 최신 데이터가 반환된다는 거야.

예시 코드:

axios.get("https://api.movie-site.com/movies").then((res) => {
  // API에서 최신 영화 데이터 받아옴
  const movieData = res.data; // 이건 최신 정보
});

→ API에서 데이터를 매번 새로 요청하기 때문에, 영화 사이트에서 새 영화가 추가되거나 수정되면, 다음 요청 시 그 변경 사항이 반영되어 받아와.

 

JSON 파일로 저장하는 경우 (정적 파일)

  • 한 번만 API에서 받아온 데이터를 JSON 파일로 저장해두고, 그걸 계속 사용한다고 하면.
  • JSON 파일을 저장할 때 그 시점의 데이터만 담고 있는 거야.
  • 파일이 한 번 저장된 후에는 API의 데이터가 변경되더라도, 그 파일은 변경되지 않아서 새 영화가 추가된 사실을 모르게 돼.

예시 코드:

// API에서 데이터를 받아와 JSON 파일로 저장했다고 가정
const movieData = require('./movies.json'); // 예전 데이터

→ 여기선 movies.json 파일에 저장된 데이터가 변하지 않기 때문에, 영화 사이트에서 새 영화가 추가되거나 변경돼도 너의 프로젝트에는 반영되지 않음.

 

 

 

 


구현단계 3 🐰 : 클릭한 MovieCard에 맞는 MovieDetail로 이동하도록 라우팅

내가 한 방식 :

 무비컴포넌트를 클릭하면 디테일로 이동하게 하기 위해서 링크를 달아주고, 링크에는 id값으로 이동하게끔 적어줬고 디테일페이지에서는 params이용해서 id값 추출해오고.. 그 id값을 토대로, 디테일정보를 다시 패치 해올건데, 그걸 해온 데이터로 또 디테일 페이지를 꾸려준다. 근데 중요한점은 디테일은 영화 객체 하나이니까, 초기 상태값에 빈 배열이 아닌 null을 작성해줘야하는데 null에는 오류가 자주 나니까 렌더링이 잘 안될때를 대비하여 조건부렌더링도 작성해준다~

 


흐름 한 눈에 보기

  1. 인기 영화 목록 받아오기
    💬 API 예: https://api.themoviedb.org/3/movie/popular
    💬 이 안에는 여러 영화 객체들이 있고, 각각 id, title, poster_path 같은 정보가 있음
    💬 그걸로 .map() 돌려서 메인화면 꾸미기
  2. 영화를 클릭하면 해당 영화의 id를 주소에 넘김
    💬 예: 쿵푸팬더의 id가 20이라면
    💬 클릭 시: /movies/20 같은 URL로 이동
  3. 디테일 페이지에서는 그 id로 디테일 데이터 fetch
    💬 API 예: https://api.themoviedb.org/3/movie/20
    💬 받아온 데이터로 상세 정보 화면 꾸미기 (줄거리, 평점, 포스터 등)

예시 흐름 요약:

// 메인 화면: 인기 영화 리스트
/movie/popular → map → 영화 카드들 보여줌 → 클릭 → /movies/20 이동

// 디테일 화면: 해당 id로 디테일 fetch
/movie/20 → 영화 디테일 정보 fetch → 상세 페이지 렌더

완성된 코드 ! 

 

import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";


function MovieDetail() {
    const {id} = useParams()
    const [movies,setMovies] = useState(null)

    useEffect(()=>{
        const options = {
            method: 'GET',
            headers: {
              accept: 'application/json',
              Authorization: 'Bearer 비밀이지롱. 본인 키 값 적으면 됩니다.'
            }
          };
          
          fetch(`https://api.themoviedb.org/3/movie/${id}?language=en-US`, options)
            .then(res => res.json())
            .then(data => {
                console.log(data)
                setMovies(data)
            })
            .catch(err => console.error(err));
    },[id])

        if (!movies) return <div>Loading...</div>;

    return(
        <div>
            <img src={`https://image.tmdb.org/t/p/w500${movies.poster_path}`} alt="movie poster" />
            <h1 className='text-[pink]'>{movies.title}</h1>
            <h3>{movies.vote_average}</h3>
            {movies.genres.map((genre)=>{
                return <li key={genre.id}>{genre.name}</li>
            
            })}
            <h3>{movies.overview}</h3>
        </div>
    )
}

export default MovieDetail;

코드 설명:

1. 상태 초기값에서 (null)을 쓴 이유는 아래와 같다.

더보기

👩🏻‍💻인기 영화 리스트일 때:

  • 여러 개의 영화 → 배열 형태로 옴 → 예: [{...}, {...}, {...}]
  • 그래서 초기값은 **빈 배열 []**로 쓰는 게 자연스러움
  • const [movies, setMovies] = useState([]); // 여러 영화

👩🏻‍💻영화 디테일일 때:

  • 단 하나의 영화 정보 → 객체 하나 → 예: { id: 123, title: "Oppenheimer", ... }
  • 그래서 초기값은 null 또는 빈 객체 {} 둘 다 가능은 하지만,
  • 일반적으로는 null을 써서 **"아직 데이터 없음"**을 더 명확하게 표현함
  • const [movie, setMovie] = useState(null); // 영화 하나

 

 

2. if (!movies) return <div>Loading...</div>; 이 부분을 넣어준 이유는 아래와 같다.

안넣으면 오류나 ! 그냥, 초기상태가 null 이면 조건부 렌더링을 넣어주자 ~ 

더보기

 

if (!movie) return <div>Loading...</div>;

이 코드는 "movie 데이터가 아직 없을 때", 즉 API에서 데이터를 받아오기 전에 화면에 "Loading..."이라는 글자를 보여주는 거야.

  이 코드가 하는 일:

“movie가 아직 없으면(=null이나 undefined면), 화면에 'Loading...'이라는 글자를 보여줘!” 라는 뜻이야.

 

✨코드 분석

1. !movie

  • 느낌표 !는 “not(아니다)”라는 뜻이야.
  • movie가 null, undefined, false, 0, "" 이런 Falsy 값이면 true가 돼.
  • 즉, movie에 아무 데이터가 아직 없을 때를 의미해.

2. return <div>Loading...</div>;

  • 이건 JSX 문법이야.
  • 이 줄이 실행되면 컴포넌트가 더 아래로 가지 않고 여기서 끝나, 즉 "로딩 중"만 보여줘.

좀 더 자세히:

  • movie는 처음에 이렇게 선언했지:→ 처음에는 null이야. 아무것도 없는 상태!
  • const [movie, setMovies] = useState(null);
  • fetch로 데이터를 받아오고 나면:→ movie 값이 채워져서 실제 영화 정보가 들어와.
  • setMovies(data);
  • 그런데 그 전까지는 null이라서, 바로 아래처럼 쓰면 오류가 나:
  • <img src={movie.backdrop_path} /> // ← null에 접근하니까 에러!

그래서 이 코드를 먼저 써주는 거야:

if (!movie) return <div>Loading...</div>;

→ "movie가 없으면, 화면에 로딩 중이라고만 보여줘! 나머지 코드는 실행하지 마!" 라는 뜻이야.

 

이걸 왜 쓰냐면?

비동기 데이터(fetch)를 가져올 때, 처음에는 movie에 값이 없어.

  • 그때 movie.title, movie.backdrop_path 이런 거 쓰면 오류 나기 쉬워.
  • 그래서 데이터가 없을 땐 먼저 ‘로딩 중’ 화면을 보여주는 거야.

 

아 그러면 저게 첫 줄이 movie.path가 아니고 아무튼 리턴값의 첫 줄이 movie.title 이더라도 아무튼 초기 상태값이 null 이니까.. 오류가 나는거야 ?

정답은:

응! 리턴값에서 movie.title이든 movie.backdrop_path든 뭘 먼저 쓰든, movie가 null이면 무조건 오류 나.


예를 들어:

return (
  <div>
    <h1>{movie.title}</h1>
  </div>
)

근데 movie가 아직 null 상태면?

→ movie.title에서 바로 오류 나!

Cannot read properties of null (reading 'title')


왜 그런가 하면?

  • null.title 이라는 건 JS에서 말도 안 되는 일이기 때문이야.
    null은 객체가 아니니까 .title 같은 속성 접근이 불가능해.

그래서 꼭 필요한 게 이거야:

if (!movie) return <div>Loading...</div>;

이게 **"movie가 아직 null이면 일단 아무것도 안 보여주고 기다리자"**는 뜻이야.
그 다음에 fetch 성공하고 나서 movie에 값이 들어가면 그때 보여주는 거야.

 

그러면 초기 상태값이 null인 경우에는 거의 대부분 저런 조건부렌더링을 우선 붙여주는게 나아 ?

맞아, Jane! 
초기 상태값이 null인 경우에는 대부분 그에 대한 조건부 렌더링을 우선 붙여주는 게 정말 중요해!

왜냐면?

  1. 초기값이 null일 때는 데이터가 아직 없으니까, 화면에 뭔가 잘못 출력될 수 있어.
    • 예를 들어, movie가 null이면 movie.title을 직접 사용하려고 할 때 오류가 나니까, 조건문을 넣어서 null일 때는 Loading...과 같은 로딩 화면을 먼저 보여주는 게 안전해.
  2. 비동기 데이터는 언제 올지 모르니까 기다려줘야 해.
    • API 요청이나 비동기 데이터는 서버 응답을 기다려야 하기 때문에 로딩 상태를 처리해주는 게 좋다.
    • 만약 데이터를 받기 전에 화면에 접근하면 오류가 나거나, 렌더링이 제대로 안 될 수 있어.
  3. 조건부 렌더링을 통해 UI 흐름을 잘 관리할 수 있어.
    • if (!data) return <Loading />; 이런 식으로 처리하면 조건에 맞춰서 화면을 어떻게 보여줄지 명확하게 관리할 수 있어.

그러면 상태값이 null일 때 조건부 렌더링을 써야 하는 경우:

  • 비동기적으로 데이터를 받아오는 경우
  • 서버에서 데이터를 가져오기 전에 상태값이 null일 때
  • 화면에 표시할 데이터가 아직 없을 때

결론:

초기 상태값이 null이면 조건부 렌더링을 추가하는 게 반드시 필요해. 그렇지 않으면 오류가 발생하거나 화면에 잘못된 데이터가 표시될 수 있기 때문이야.

Jane은 지금 이 부분을 잘 다뤄가고 있어서 앞으로 더 쉽게 이런 문제들을 처리할 수 있을 거야! 👏


 

디테일 페이지에서 필요한 작업

  1. **react-router-dom**의 useParams() 훅으로 URL에서 영화 ID 추출
  2. import { useParams } from "react-router-dom"; const { id } = useParams();
  3. 영화 목록이 전역 상태(state)로 관리되고 있다면 (예: Context나 Redux 사용 시)
    • 바로 거기서 해당 id에 맞는 영화 찾아서 보여주면 돼요.
    const movie = movies.find(movie => movie.id === parseInt(id));
    
  4. 만약 전역 상태로 안 쓰고 있다면, 디테일 페이지에서도 다시 API 호출해서 전체 영화 목록 받아오고,
    • 거기서 id 매칭해서 하나만 추출해서 보여줘야 해요.

  • 전역 상태로 한 번 불러온 영화 목록을 재사용하면 API 중복 호출 줄이고, UX도 빠름
  • React Context나 zustand, Redux 같은 걸 사용해서 인기 영화 목록 전역으로 저장하면 좋아요

 

왜 굳이 전역으로 상태를 관리하면 좋을까?

더보기

지금 상황(인기 영화 목록 → 디테일 → 앞으로 검색 기능도 구현 예정)이라면 전역 상태로 영화 데이터를 관리하는 걸 추천해요. 지금부터 해두면 훨씬 수월해져요.


왜 지금 전역 상태 관리가 좋은지

  1. 중복 API 호출 방지
    • 메인에서 이미 받아온 데이터를 디테일에서도 쓸 수 있음
    • → 네트워크 비용 ↓, 속도 ↑
  2. 여러 컴포넌트에서 데이터 공유 편함
    • 검색 기능, 필터링, 즐겨찾기 등 추가하려면 공통된 상태 관리가 꼭 필요해짐
  3. 코드 유지보수 쉬움
    • 데이터 흐름이 명확해짐 → 디버깅, 확장 모두 쉬움

 지금 가능한 선택지

  1. Context API + useReducer (or useState)
    • 작은 프로젝트에 적합
    • 가볍고, 설정 간단
    • React 내장 기능만으로 가능
  2. Zustand (추천)
    • 설정 거의 필요 없음
    • 코드가 깔끔하고 가독성 좋음
    • 검색/필터링 같은 기능 구현할 때도 편함
  3. Redux
    • 기능은 강력하지만 설정 복잡
    • 아주 큰 프로젝트 아니면 조금 오버스펙일 수 있음

 예를 들어 Zustand 쓰면 이런 식

// store.js
import { create } from 'zustand';

const useMovieStore = create((set) => ({
  movies: [],
  setMovies: (movieList) => set({ movies: movieList }),
}));

export default useMovieStore;
// 메인 페이지
import useMovieStore from './store';
...
useEffect(() => {
  fetchPopularMovies().then(data => {
    setMovies(data.results);
  });
}, []);
// 디테일 페이지
const { id } = useParams();
const movie = useMovieStore((state) =>
  state.movies.find((movie) => movie.id === parseInt(id))
);

이제 점점 기능이 늘어날 예정이라면, 지금 이 타이밍에 전역 상태를 정리해두는 게 나중에 덜 고생하게 되는 길이에요.

Zustand나 Context 중에 뭐가 더 맞을지 고민 중이면 도와줄게요. 사용하고 있는 구조나 규모 알려줄래요?


const { id } = useParams(); // URL에서 id 값 추출 이게 무슨 url인지 얘가 어떻게 아는거야 ?

 

 useParams()는 현재 URL 경로에서 동적 파라미터를 추출하는데,

이 URL이 뭔지는 React Router의 설정(Route 설정)에서 알려줘야 해요.

 

예를 들어, 아래처럼 라우트를 설정했다고 해볼게요:

<Route path="/users/:id" element={<UserDetail />} />

여기서 중요한 포인트는:

  • path="/users/:id" 이렇게 :id라는 동적 경로 파라미터가 정의되어 있고,
  • /users/123 같은 URL이 브라우저에 들어오면,
  • React Router가 이 URL을 보고, UserDetail 컴포넌트를 렌더링하면서,
  • :id 자리에 있는 123을 useParams() 훅으로 넘겨주는 거예요.

💬 흐름 정리:

  1. 브라우저에서 URL이 /users/123임
  2. 라우터 설정: <Route path="/users/:id" element={<UserDetail />} />
  3. React Router가 UserDetail을 렌더링
  4. 이때 useParams()는 { id: "123" }를 반환

🔍 결론

useParams()는 혼자서 URL을 "알 수 있는" 게 아니라,
라우터 설정이 먼저 있어야 하고,
거기서 정의한 경로 (/users/:id)와 실제 URL (/users/123)을 비교해서
매칭되는 값을 자동으로 추출하는 거예요.


아 내가 라우터에서 먼저 설정 해두었지 ~ 

 


새롭게 알게되고 공부한것 - fetch()와, 비동기 함수.

더보기

아 ~그럼 여기엔 따로 비동기 함수가 있는건 아니고. MovieList라는 컴포넌트 안에서 그냥 패치 해오는 동작이 있는거구나

 

 

맞아요, Jane! 완벽하게 이해했어요 

정확히 말하면:

 MovieList는 컴포넌트 이름이고, 그 안에서 fetch()로 비동기 작업을 하는 거예요.
fetch 자체는 비동기지만, 그걸 감싸는 비동기 함수 (async/await) 로 따로 만들진 않은 거죠.

 


예시로 비교해볼게요 

 지금 구조 (비동기 함수 없이 fetch 직접 사용)

useEffect(() => {
  fetch('https://api...') // 비동기 작업
    .then(res => res.json())
    .then(data => setMovies(data.results))
    .catch(err => console.error(err));
}, []);

비동기 함수를 따로 만들어서 사용하는 구조

useEffect(() => {
  const getMovies = async () => {
    try {
      const res = await fetch('https://api...');
      const data = await res.json();
      setMovies(data.results);
    } catch (err) {
      console.error(err);
    }
  };

  getMovies(); // 비동기 함수 실행
}, []);

둘 다 결과는 같고, 상황에 따라 코드 가독성이나 에러 핸들링 편의 때문에 async/await 구조를 쓰기도 해요.

혹시 이 구조로 바꿔보고 싶으면 내가 바꿔줄 수도 있어요 :)
계속 진행해볼까?

 

 

패치 자체는 비동기 함수는 아니고, 패치로써 비동기 작업을 하는 것.

혼자 연습할 때는 패치로 써봤고, 제출할 때는 async/await을 써볼 예정이다.


패치로 비동기로써 데이터를 받아오는 파트의 구조를 하나하나 뜯어 볼까요 !?

fetch('https://api.themoviedb.org/3/movie/popular?language=en-US&page=1', options)
      .then(res => res.json())
      .then(data => {
        console.log(data); // 디버깅용
        setMovies(data.results); // 영화 목록을 상태에 저장
      })
      .catch(err => console.error(err));

예를 들어 코드가 이렇다면..

설명은 아래와 같습니다.

fetch('https://api.themoviedb.org/3/movie/popular?language=en-US&page=1', options)

→ fetch()는 데이터를 요청하는 함수야.
이 줄은 "The Movie Database (TMDB)" API에서 인기 영화 목록을 가져오는 요청을 보내는 거야.
URL 안에는 다음 정보가 있어:

  • language=en-US: 영어로 된 정보를 가져와라
  • page=1: 1페이지(첫 번째 페이지)의 결과를 가져와라
    options는 이 요청에 필요한 추가 설정(예: API 키 등)을 담은 객체야.

.then(res => res.json())

→ then()은 비동기 작업이 성공했을 때 실행돼.
res.json()은 서버에서 받은 응답(response)을 JSON 형식으로 바꿔주는 작업이야.
즉, 이 줄은 "응답을 JSON으로 바꿔줘" 라는 뜻이야.


.then(data => {
  console.log(data); // 디버깅용
  setMovies(data.results); // 영화 목록을 상태에 저장
})

→ 이 then()은 위에서 JSON으로 바뀐 데이터를 받아서 처리해.

  • console.log(data);는 콘솔에 결과를 출력해서 확인할 수 있게 해주는 디버깅 용도야.
  • setMovies(data.results);는 가져온 영화 목록 중 results라는 배열을 **상태(state)**에 저장하는 거야.
    (예: React의 useState 훅을 쓰는 경우)

.catch(err => console.error(err));

→ 이 줄은 에러가 났을 때 실행돼.
예를 들어 API 호출에 실패했을 때!