리액트/예제

영화 목록 가져오기 사이트만들기(타입스크립트)

lamarcK 2025. 4. 11. 06:43

영화 데이터를 가져와서 표시하는 전체 과정 단계

  1. API 통신 설정 (movieApi.ts)
    • API 키와 기본 URL 설정
    • axios를 사용한 데이터 요청 함수 정의
    • 영화 데이터 타입 정의
  2. 데이터 요청 및 받아오기 (movieApi.ts) 
    • getPopularMovies 함수 호출
    • API로부터 영화 데이터 배열 수신
    • response.data.results 형태로 데이터 반환
  3. 상태 관리 설정 (MovieList.tsx)
    • useState로 상태 변수 설정 (movies, loading, error)
    • useEffect로 컴포넌트 마운트 시 데이터 요청
    • 받아온 데이터를 setMovies로 상태에 저장
  4. 데이터 매핑 및 전달
    • movies 배열을 map 함수로 순회
    • 각 영화 데이터를 MovieCard 컴포넌트에 props로 전달
    • key 속성 부여하여 고유성 확보
  5. 개별 영화 표시 (MovieCard.tsx)
    • props로 받은 영화 데이터 구조분해할당
    • 영화 정보(제목, 포스터, 평점 등) 표시
    • 스타일링 적용
  6. 에러 처리 및 로딩 상태
    • 데이터 로딩 중 로딩 표시
    • 에러 발생 시 에러 메시지 표시
    • try-catch로 예외 처리
  7. 라우팅 설정 (필요한 경우)
    • 영화 상세 페이지 링크 설정
    • URL 파라미터 처리
    • 페이지 간 네비게이션
  8. 스타일링
    • styled-components로 컴포넌트 스타일링
    • 반응형 디자인 적용
    • 레이아웃 구성

실제 코드 흐름:

//movieApi.ts
import axios from 'axios';
import { Movie } from '../types/movie';

const API_KEY = import.meta.env.VITE_APP_TMDB_API_KEY;
const BASE_URL = 'https://api.themoviedb.org/3';

export const getPopularMovies = async (): Promise<Movie[]> => {
  try {
    const response = await axios.get(
      `${BASE_URL}/movie/popular?api_key=${API_KEY}&language=ko-KR`
    );
    console.log('API Response:', response.data); // 응답 확인용
    return response.data.results;
  } catch (error) {
    console.error('Error fetching movies:', error);
    throw error;
  }
};

// 영화 상세 정보를 위한 인터페이스
export interface MovieDetail extends Movie {
  genres: Array<{ id: number; name: string }>;
  runtime: number;
  budget: number;
  revenue: number;
  tagline: string;
  production_companies: Array<{
    id: number;
    name: string;
    logo_path: string | null;
  }>;
}

// 영화 상세 정보를 가져오는 함수
export const getMovieDetails = async (id: string): Promise<MovieDetail> => {
  const response = await axios.get(
    `${BASE_URL}/movie/${id}?api_key=${API_KEY}&language=ko-KR`
  );
  return response.data;
};

 


실제 예제

api 받아오기 : TMDB 사이트에서 받아옵니다.

https://developer.themoviedb.org/docs/authentication-application

 

해당 사이트의 기술문서인데 대충 api키 발급 장소에 대해 얘기하고 있습니다.

 

api 키 사용법 확인하기 : api키의 사용법을 확인합니다.

https://developer.themoviedb.org/docs/getting-started

 

1️⃣ API 키 (api_key) 사용법

The default way to authenticate. Application level authentication would generally be considered the default way of authenticating yourself on the API. Version 3 is controlled by one of either a single query parameter, api_key, or by using your access token as a Bearer token. You can request an API key by logging in to your account on TMDB and clicking here.  

해당 문서를 보면

  • API 버전 3 (Version 3) 에서는 두 가지 인증 방식 중 하나를 사용한다고 명확히 밝힌다.
    1. api_key를 쿼리 파라미터(query parameter) 로 URL에 직접 포함하는 방식
    2. 액세스 토큰(access token)을 Bearer 토큰 (Bearer token) 으로 사용하는 방식

 

  • 텍스트에 명시된 첫 번째 방법은 api_key를 단일 쿼리 파라미터 (single query parameter) 로 사용하는 것이다.
  • 즉, API 요청을 보내는 URL 주소의 끝에 ?api_key={API_키}와 같은 형식으로 API 키를 직접 포함시켜야 한다.

실제로 해당 사이트의 https://developer.themoviedb.org/reference/intro/getting-started 를 보면 각종 정보를 얻기위한 api 사용법이 기재되어있다.

 

사용해야하는 url 형태, 가져와지는 데이터의 response 등이 기재되어있다.


1. API 통신 설정 (movieApi.ts)

실제  fetch url을 참고하여 실제로 가져올 url 주소를 만든다.

총 2가지를 만들었는데 Now playing과 detail 부분이다.

Now Playing

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

 

Detail

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

이중 중복되는 url은 변수로 만들어서 관리하고 api키는 별도로 env 파일을 만들어서 가져오는 방식을 사용한다.

const BASE_URL = 'https://api.themoviedb.org/3';
const API_KEY = import.meta.env.VITE_APP_TMDB_API_KEY;

 

.env 파일

//api키입력
VITE_APP_TMDB_API_KEY=api키이름

 

결과적으로 만들어지는 url

//일반
`${BASE_URL}/movie/now_playing?api_key=${API_KEY}&language=ko-KR`
//디테일
`${BASE_URL}/movie/${id}?api_key=${API_KEY}&language=ko-KR`

 


2. 데이터 요청 및 받아오기 (movieApi.ts) 

2가지로 방법으로 나뉜다. 기본적으로 정보를 가져오는 것은 axios.get() 메서드를 사용한다.

  • HTTP GET 요청을 보내는 Axios 라이브러리의 메서드이다.

 

1. 영화의 기본적인 얼개만 가져오는 axios.get()

// TypeScript에서 추가: 반환 타입을 Promise<Movie[]>로 명시
export const getPopularMovies = async (): Promise<Movie[]> => {
  try {
    const response = await axios.get(
      `${BASE_URL}/movie/now_playing?api_key=${API_KEY}&language=ko-KR`
    );
    console.log('API Response:', response.data);
    return response.data.results;
  } catch (error) {
    console.error('Error fetching movies:', error);
    throw error;
  }
};

2. 영화의 디테일까지 전부 가져오는 가져오는 axios.get() 함수

// 영화 상세 정보를 가져오는 함수
export const getMovieDetails = async (id: string): Promise<MovieDetail> => {
  const response = await axios.get(
    `${BASE_URL}/movie/${id}?api_key=${API_KEY}&language=ko-KR`
  );
  return response.data;
};

둘다 해당 API 엔드포인트가 제공하는 결과(result)를 모두 반환한다.

 

둘다 async문이기 때문에 암묵적으로 result를 반환하는데 

try 블록 안의 return은 자동으로 resolve가 되고 catch 블록 안의 throw는 자동으로 reject가 된다.

  • 즉 try 부분이 return되면 result로 response.data.results;를 받고
  • throw 부분이 return되면 result로 error(axios error)를 받게된다.

2번째 함수도 리턴으로 response.data;를 반환하고 실패시 자동으로 reject(axios error 객체)를 반환한다.


➕인터페이스 설정 (movieApi.ts)

두 api모두 result는 API 엔드포인트가 제공하는 결과(result)를 모두 반환하지만 인터페이스를 사용해서 동일한 속성의 데이터에 타입을 명시적으로 지정할 수 있다.

만약에 속성이 동일하지 않으면 인터페이스가 지정되지 않는다(데이터는 사라지지 않고 남아있다. any 타입으로 지정되어있다.)

기본적으로 어떤 속성의 데이터가 넘어오는지 알아야 설정할 수 있기 때문에 api쪽의 데이터를 알고 있어야한다. 편의성을 의해서 지정되는 것이지 강제는 아니다.

1. Movie 인터페이스

Movie라는 이름의 인터페이스 설정이다.

export interface Movie {
    id: number;
    title: string;
    overview: string;
    poster_path: string;
    release_date: string;
    vote_average: number;

 

2. MovieDetail extends Movie 인터페이스

MovieDetail이라는 이름의 인터페이스 설정하고 Movie에 설정된 인터페이스까지 모두 상속했다는 말이다.

MovieDetail와 Movie의 인터페이스 모두를 가진다.

// TypeScript에서 추가: 인터페이스 정의
export interface MovieDetail extends Movie {
  // TypeScript에서 추가: 각 속성의 타입을 명시
  genres: Array<{ id: number; name: string }>;
  runtime: number;
  budget: number;
  revenue: number;
  tagline: string;
  production_companies: Array<{
    id: number;
    name: string;
    logo_path: string | null;  // TypeScript에서 추가: union 타입 사용
  }>;
}

 

 

 

 

실제 인터페이스 처리 방식

// API 응답 데이터가 여전히 모든 속성을 가지고 있음
const movieData = {
  id: 123,
  title: "영화제목",
  overview: "줄거리",
  poster_path: "/이미지경로.jpg",
  release_date: "2024-01-01",
  vote_average: 8.5,
  adult: false,          // <- 여전히 존재함
  backdrop_path: "...",  // <- 여전히 존재함
  genre_ids: [1, 2, 3]   // <- 여전히 존재함
};

// TypeScript에서는 Movie 인터페이스에 정의된 속성만 타입 체크
const movie: Movie = movieData; // 정상 동작

// 타입 체크만 제한될 뿐, 실제로는 모든 데이터에 접근 가능
console.log(movieData.adult);        // 가능
console.log((movie as any).adult);   // 타입 단언으로 접근 가능

//movieApi.ts
import axios from 'axios';

const BASE_URL = 'https://api.themoviedb.org/3';
const API_KEY = import.meta.env.VITE_APP_TMDB_API_KEY;

// TypeScript에서 추가: 반환 타입을 Promise<Movie[]>로 명시
export const getMovies = async (): Promise<Movie[]> => {
  try {
    const response = await axios.get(
      `${BASE_URL}/movie/now_playing?api_key=${API_KEY}&language=ko-KR`
    );
    console.log('API Response:', response.data);
    return response.data.results;
  } catch (error) {
    console.error('Error fetching movies:', error);
    throw error;
  }
};

// 영화 상세 정보를 가져오는 함수
export const getMovieDetails = async (id: string): Promise<MovieDetail> => {
  const response = await axios.get(
    `${BASE_URL}/movie/${id}?api_key=${API_KEY}&language=ko-KR`
  );
  return response.data;
};

// TypeScript에서 추가: 인터페이스 정의
export interface MovieDetail extends Movie {
  // TypeScript에서 추가: 각 속성의 타입을 명시
  genres: Array<{ id: number; name: string }>;
  runtime: number;
  budget: number;
  revenue: number;
  tagline: string;
  production_companies: Array<{
    id: number;
    name: string;
    logo_path: string | null;  // TypeScript에서 추가: union 타입 사용
  }>;
}

export interface Movie {
  id: number;
  title: string;
  overview: string;
  poster_path: string;
  release_date: string;
  vote_average: number;
}

 


HomePage.tsx

최초로 영화 목록을 불러오는 홈페이지

movieApi에서 정의한 함수와 인터페이스 사용

  • getMovies 함수
  • Movie 인터페이스
import { Movie } from '../services/movieApi';
import { getMovies } from '../services/movieApi';
 

 함수 내부에 useState 선언

const HomePage = () => {
  const [movies, setMovies] = useState<Movie[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  • 영화 정보를 저장할 상태 변수 : const [movies, setMovies] = useState<Movie[]>([]);
  • 인터페이스 Movie 받아서 초기값의 타입 정의

최초 마운트 시 동작하는 useEffect 선언

  useEffect(() => {
    // 비동기 함수 선언
    const fetchMovies = async () => {
      try {
        // 1. API 호출하여 데이터 가져오기
        const data = await getMovies();
        // 2. 받아온 데이터를 상태에 저장
        setMovies(data);
      } catch (err) {
        // 3. 에러 처리
        setError(err instanceof Error ? err.message : 'Failed to fetch movies');
        console.error('Error:', err);
      } finally {
        // 4. 로딩 상태 종료
        setLoading(false);
      }
    };

    // 함수 실행
    fetchMovies();
}, []); // 빈 배열은 컴포넌트가 처음 마운트될 때만 실행

fetchMovies 함수 정의

  1. async 사용 비동기 작업 완료시 까지 대기.
  2. api에서 데이터를 받아오는  getMovies 함수 호출.
  3. 호출 완료시 까지 await으로 대기
  4. 불러온 데이터를 movies 상태변수에 저장

에러 시 에러 메시지 반환

  1. API 호출 관련 에러, 네트워크 연결 실패, 서버 응답 오류 (404, 500 등) 등

finally

  • 로딩 상태 변수 저장 : const [loading, setLoading] = useState(true); → false로

fetchMovies 실행

  • 호출 안하면 내부 함수는 자동으로 실행되지 않음

if문을 통해서 로딩 중이라면 html요소를 반환

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;

그 외엔 정상적인 html 요소를 반환

  return (
    <div>
      <h1>Movies</h1>
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
        gap: '20px',
        padding: '20px'
      }}>
        {movies.map(movie => (
          <MovieCard key={movie.id} movie={movie} />
        ))}
      </div>
    </div>

그리드 컨테이너를 만들고 안 쪽에 <MovieCard/>를 프롭스로 호출 하여 movie 객체의 데이터를 props로 전달한다.

movies 배열을 map으로 순회하여 각 movie 객체마다 MovieCard 컴포넌트를 생성한다.

import { useState, useEffect } from 'react';
import { Movie } from '../services/movieApi';
import { getMovies } from '../services/movieApi';
import { MovieCard } from '../components/MovieCard';

const HomePage = () => {
  const [movies, setMovies] = useState<Movie[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // 비동기 함수 선언
    const fetchMovies = async () => {
      try {
        // 1. API 호출하여 데이터 가져오기
        const data = await getMovies();
        // 2. 받아온 데이터를 상태에 저장
        setMovies(data);
      } catch (err) {
        // 3. 에러 처리
        setError(err instanceof Error ? err.message : 'Failed to fetch movies');
        console.error('Error:', err);
      } finally {
        // 4. 로딩 상태 종료
        setLoading(false);
      }
    };

    // 함수 실행
    fetchMovies();
}, []); // 빈 배열은 컴포넌트가 처음 마운트될 때만 실행

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>Movies</h1>
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
        gap: '20px',
        padding: '20px'
      }}>
        {movies.map(movie => (
          <MovieCard key={movie.id} movie={movie} />
        ))}
      </div>
    </div>
  );
};

export default HomePage;

MovieCard.tsx

프롭스로 전달받은 데이터를 사용하는 컴포넌트 영화 목록의 카드 1개를 만드는 실질적인 컴포넌트다.

// MovieCard.tsx
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Movie } from '../services/movieApi';

interface Props {
  movie: Movie;
}

import { Movie } from '../services/movieApi';

  • Movie 인터페이스를 받아오는 부분이다.
  • interface Props { movie: Movie; } 부분에서 활용하기 위함이다.
interface Props {movie: Movie;}
  • 이전에 인터페이스를 한번이라도 정의한 데이터라면 인터페이스 정의를 일관성있게 동일하게 해줘야한다.
  • Props 인터페이스는 "이 컴포넌트가 받을 props의 타입"을 정의
  • movie라는 이름의 prop이 Movie 타입이어야 한다고 명시
  • TypeScript가 이를 기반으로 타입 체크를 수행

import { Link } from 'react-router-dom'; :

  • React Router의 Link 컴포넌트
  • 페이지 간 네비게이션을 위한 컴포넌트
  • 클릭 시 새로고침 없이 페이지 이동

import styled from 'styled-components';

  • CSS-in-JS 라이브러리
  • JavaScript/TypeScript 파일 안에서 CSS 스타일링
  • 컴포넌트 기반 스타일링

2개 모두 별도 서드파티(third-party) 라이브러리여서 설치해야한다.

# React Router 설치
npm install react-router-dom

# Styled Components 설치
npm install styled-components

const MovieCard

영화 카드 html을 만드는 컴포넌트다.

export const MovieCard = ({ movie }: Props) => {
  return (
    <Card>
      <Link to={`/movie/${movie.id}`}>
        <ImageContainer>
          <MoviePoster 
            src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} 
            alt={movie.title} 
          />
          <Rating>{movie.vote_average.toFixed(1)}</Rating>
        </ImageContainer>
        <Content>
          <Title>{movie.title}</Title>
          <ReleaseDate>
            {new Date(movie.release_date).toLocaleDateString()}
          </ReleaseDate>
        </Content>
      </Link>
    </Card>
  );
};
  • MovieCard 컴포넌트는 단일 movie 객체를 담고 있는 props에서 구조분해할당으로 movie를 추출한다.
  • 즉 외부의 괄호를 하나 제거한다.
    • 구조분해할당 전: props.movie.title, props.movie.id ...
    • 구조분해할당 후: movie.title, movie.id ...
  • 이후 movie 객체의 각 속성의 값(title의 "제목", id의 001 등)을 JSX 요소에 표시한다.

styled-components

<ImageContainer>, <MoviePoster> 등의 컴포넌트는 styled-components를 사용해 HTML 기본 태그(div, img 등)에 스타일을 입혀 새로운 이름의 컴포넌트로 만든 것이다.

const Card = styled.div`
  background: white;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 5px 10px rgba(0, 0, 255, 0.1);
  transition: transform 0.2s;

  &:hover {
    transform: translateY(-5px);
    transform: scale(1.2);
    z-index : 2
  }

  a {
    text-decoration: none;
    color: inherit;
  }
`;

const ImageContainer = styled.div`
  position: relative;
`;

const MoviePoster = styled.img`
  width: 100%;
  height: 375px;
  object-fit: cover;
`;

const Rating = styled.div`
  position: absolute;
  right: 10px;
  top: 10px;
  background: rgba(255, 0, 0, 0.8);
  color: white;
  padding: 4px 8px;
  border-radius: 4px;
  font-weight: bold;
`;

const Content = styled.div`
  padding: 16px;
`;

const Title = styled.h3`
  margin: 0;
  font-size: 1.1rem;
  margin-bottom: 8px;
`;

const ReleaseDate = styled.p`
  margin: 0;
  color: #666;
  font-size: 0.9rem;
`;

현재까지의 흐름

데이터는 위에서 아래로 흐르며, 각 컴포넌트는 필요한 데이터만 props로 전달받아 표시한다.

    A[movieApi.tsx] -->|API 호출| B[HomePage.tsx]
    B -->|영화 데이터 전달| C[MovieCard.tsx]

    subgraph "movieApi.tsx"
        A1[API KEY 설정]
        A2[axios 함수] //혹은 fetch 사용 가능
        A3[엔드포인트 설정]
    end

    subgraph "HomePage.tsx"
        B1[useState - 영화데이터]
        B2[useEffect - API 호출]
        B3[영화 목록 렌더링]
    end

    subgraph "MovieCard.tsx"
        C1[props 받기]
        C2[영화 정보 표시]
        C3[스타일링]
    end

    A1 --> A2
    A2 --> A3
    B1 --> B2
    B2 --> B3
    C1 --> C2
    C2 --> C3
  1. movieApi.tsx
    1. API KEY와 기본 URL 설정
    2. axios 함수로 데이터 요청
    3. 다양한 엔드포인트 설정 (인기영화, 최신영화 등)
  2. HomePage.tsx
    1. useState로 영화 데이터 상태 관리
    2. useEffect로 컴포넌트 마운트 시 API 호출
    3. 받아온 데이터로 영화 목록 렌더링
  3. MovieCard.tsx
    1. props로 개별 영화 정보 수신
    2. 영화 포스터, 제목 등 정보 표시
    3. CSS로 카드 스타일링

데이터 흐름:
API 호출 → 데이터 수신 → HomePage에서 상태 관리 → MovieCard에 props 전달 → 화면 표시


MovieDetailPage.tsx

영화 상세페이지를 보여주기 위한 컴포넌트

필요한 기능 import

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getMovieDetails, MovieDetail } from '../services/movieApi';
import styled from 'styled-components'; // 스타일링을 위해 사용
  1. import { useParams, useNavigate } from 'react-router-dom'
    1. React Router의 Hooks를 가져온다.
    2. useParams`: URL 파라미터를 가져오는 Hook (예: /movie/:id의 id 값)
    3. useNavigate`: 프로그래밍 방식으로 페이지 이동을 할 수 있게 하는 Hook
  2. import { getMovieDetails, MovieDetail } from '../services/movieApi'
    1. 영화 정보를 가져오는 API 관련 함수와 타입을 import
    2. getMovieDetails`: 영화 상세 정보를 가져오는 함수
    3. MovieDetail`: 영화 상세 정보의 타입 정의

필요한 변수 선언

const MovieDetailPage = () => {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const [movie, setMovie] = useState<MovieDetail | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

const { id } = useParams<{ id: string }>();

먼저 파라미터를 id로 정의하고 타입은 문자열로 한다는 얘기다.

파라미터(Parameter)는 데이터를 전달하는 값이나 변수를 의미한다. URL에서의 파라미터는 주로 두 가지 형태가 있다.

  • url 파라미터와 쿼리 파라미터 (Query Parameters)인데
  • 쿼리파라미터는 이전에 api키를 넣었던 ? 뒤쪽의 부분이다.

url 파라미터(Path Parameters)

// URL 예시: /movie/123
/movie/:id  // ':id'가 파라미터

// 실제 사용 예
/movie/123  // id = 123
/movie/456  // id = 456
/users/john // id = john

 

쿼리 파라미터 (Query Parameters)

// URL 예시: /search?keyword=avatar&year=2009
/search?keyword=avatar  // keyword가 파라미터
/products?category=books&sort=price  // category와 sort가 파라미터

 

실제 링크가 있다면 https://n.news.naver.com/mnews/article/001/0015325541?rc=N&ntype=RANKING

https://          // 프로토콜
n.news.naver.com  // 도메인
/mnews/article    // 경로(path)
/001/0015325541   // URL 파라미터(path 파라미터)
?                 // 쿼리 파라미터 시작
rc=N              // 첫 번째 쿼리 파라미터
&                 // 쿼리 파라미터 구분자
ntype=RANKING     // 두 번째 쿼리 파라미터

이렇게 나눌 수 있다.

 


const navigate = useNavigate() : useNavigate를 사용하겠다는 말이다.

  • React Router에서 제공하는 Hook으로, 프로그래밍 방식으로 페이지 이동을 할 수 있게 해주는 도구이다.
    • 페이지 새로고침 없이 이동 (SPA 특징)
    • 프로그래밍 방식의 라우팅 가능
    • 뒤로가기/앞으로가기 지원
    • 상태(state) 전달 가능

간단히 말하면 특정 경로로 이동하거나 뒤로가거나 앞으로가거나 하는 것이 새로고침을 거치지 않고 한페이지 내에서 가능하도록 하는 기능이다.

예를 들어 기존의 URL이동은 책의 1페이지를 보고 있다가 다른 책을 가져와서 그곳의 1페이지로 이동하는 방식이라면

useNavigate는 원래의 책에서 페이지만 다른 곳을 펼치는 것이다.

때문에 이런 방식이 SPA 방식(Single Page Application)이라고 할 수 있다.

const navigate = useNavigate();

// 1. 특정 경로로 이동
navigate('/home');              // /home으로 이동
navigate('/about');            // /about으로 이동
navigate(`/movie/${movieId}`); // /movie/123 같은 동적 경로로 이동

// 2. 뒤로가기/앞으로가기
navigate(-1);     // 뒤로가기 (브라우저의 뒤로가기 버튼과 동일)
navigate(1);      // 앞으로가기
navigate(-2);     // 2페이지 뒤로

// 3. 상태와 함께 이동
navigate('/home', { state: { from: 'movie' } });

실제 사용 예시

function MoviePage() {
  const navigate = useNavigate();

  return (
    <div>
      {/* 뒤로가기 버튼 */}
      <button onClick={() => navigate(-1)}>
        뒤로 가기
      </button>

      {/* 특정 영화로 이동하는 버튼 */}
      <button onClick={() => navigate(`/movie/${movieId}`)}>
        영화 상세보기
      </button>

      {/* 홈으로 이동하는 버튼 */}
      <button onClick={() => navigate('/')}>
        홈으로
      </button>
    </div>
  );
}

getMovieDetails(id)로 id를 기준으로 api 결과를 받아오기

useEffect 사용해서 fetchMovieDetail 함수 실행

  useEffect(() => {
    const fetchMovieDetail = async () => {
      try {
        if (!id) throw new Error('Movie ID is required');
        const data = await getMovieDetails(id);
        setMovie(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchMovieDetail();
  }, [id]);

기본적으로 id를 getMovieDetails에 전달하고 return값을 setMovie(data);로 data라는 상태값을

movie에 저장하는 역할을 한다.

const [movie, setMovie] = useState(null); <<이부분에 저장

// 영화 상세 정보를 가져오는 함수
export const getMovieDetails = async (id: string): Promise<MovieDetail> => {
  const response = await axios.get(
    `${BASE_URL}/movie/${id}?api_key=${API_KEY}&language=ko-KR`
  );
  return response.data;
};

이전에 api부분에서 만든 함수인데 TMDB API에 영화 id를 전달하여 해당 영화의 상세 정보를 요청하고, 받아온 데이터를 반환하는 함수이다.

html 구성

movie에 저장된 값을 사용해서 htlm을 구성하는 부분이다.

movie = api로 부터 받아온 객체

예시 : {movie.title} - movie의 title 속성 값 할당

return (
    <Container>
      <BackButton onClick={() => navigate(-1)}>← Back</BackButton>
      
      <MovieHeader>
        <PosterImage 
          src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} 
          alt={movie.title} 
        />
        <MovieInfo>
          <h1>{movie.title}</h1>
          <Tagline>{movie.tagline}</Tagline>
          
          <InfoSection>
            <h3>Overview</h3>
            <p>{movie.overview}</p>
          </InfoSection>

          <InfoGrid>
            <InfoItem>
              <Label>Release Date</Label>
              <Value>{new Date(movie.release_date).toLocaleDateString()}</Value>
            </InfoItem>
            
            <InfoItem>
              <Label>Runtime</Label>
              <Value>{movie.runtime} minutes</Value>
            </InfoItem>

            <InfoItem>
              <Label>Rating</Label>
              <Value>{movie.vote_average.toFixed(1)} / 10</Value>
            </InfoItem>
          </InfoGrid>

          <GenreList>
            {movie.genres.map(genre => (
              <GenreTag key={genre.id}>{genre.name}</GenreTag>
            ))}
          </GenreList>
        </MovieInfo>
      </MovieHeader>

      <AdditionalInfo>
        <h2>Production Companies</h2>
        <CompanyList>
          {movie.production_companies.map(company => (
            <CompanyItem key={company.id}>
              {company.name}
            </CompanyItem>
          ))}
        </CompanyList>
      </AdditionalInfo>
    </Container>
  );
};

Styled Components

// Styled Components
const Container = styled.div`
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  border: 5px solid;
  border-image: linear-gradient(120deg, rgb(0, 42, 105) 0%, rgb(89, 167, 255) 100%) 1;
`;

const BackButton = styled.button`
  padding: 8px 16px;
  margin-bottom: 20px;
  border: none;
  background-color:rgb(200, 200, 240);
  border-radius: 25px;
  cursor: pointer;
  
  &:hover {
  background-color:rgb(161, 161, 255);  }
`;

const MovieHeader = styled.div`
  display: flex;
  gap: 40px;
  margin-bottom: 40px;
  
  @media (max-width: 768px) {
    flex-direction: column;
  }
`;

const PosterImage = styled.img`
  max-width: 300px;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;

const MovieInfo = styled.div`
  flex: 1;
`;

const Tagline = styled.p`
  font-style: italic;
  color: #666;
  margin-bottom: 20px;
`;

const InfoSection = styled.div`
  margin-bottom: 20px;
`;

const InfoGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 20px;
`;

const InfoItem = styled.div`
  background-color: #f8f8f8;
  padding: 15px;
  border-radius: 8px;
`;

const Label = styled.div`
  font-size: 0.9em;
  color: #666;
  margin-bottom: 5px;
`;

const Value = styled.div`
  font-weight: bold;
`;

const GenreList = styled.div`
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
`;

const GenreTag = styled.span`
background:rgb(222, 221, 255);
  padding: 5px 10px;
  border-radius: 15px;
  font-size: 0.9em;
`;

const AdditionalInfo = styled.div`
  margin-top: 40px;
`;

const CompanyList = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
`;

const CompanyItem = styled.div`
  background-color: #f8f8f8;
  padding: 15px;
  border-radius: 8px;
  text-align: center;
`;

App.tsx

만들어진 컴포넌트들을 호출하는 컴포넌트

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
// - BrowserRouter: 라우팅 기능을 제공하는 최상위 컴포넌트 (Router라는 별칭으로 사용)
// - Routes: 여러 Route를 감싸는 컨테이너
// - Route: 개별 경로를 정의하는 컴포넌트

react-router-dom 기능으로 해당 페이지가 최상위 컴포넌트라는 것을 확인하고 각 페이지 별로 route를 부여한다.

import HomePage from './pages/HomePage.tsx'
import MovieDetailPage from './pages/MovieDetailPage.tsx'

각 경로에서 보여줄 페이지 컴포넌트들을 가져옴

<Route path="/" element={<HomePage />} />
// - 메인 페이지: "/" 경로에서 HomePage 컴포넌트를 보여줌

<Route path="/movie/:id" element={<MovieDetailPage />} />
// - 영화 상세 페이지: "/movie/123" 같은 경로에서 MovieDetailPage 컴포넌트를 보여줌
// - :id는 동적 파라미터

라우트를 정의한다.

상세 페이지로 이동하는 부분은 Moviecard에서 정의한 <Link to={`/movie/${movie.id}`}> 부분으로 기능한다.

<Link>로 감싼 부분이 모두 링크의 범위가 된다.

export const MovieCard = ({ movie }: Props) => {
  return (
    <Card>
      <Link to={`/movie/${movie.id}`}>
        <ImageContainer>
          <MoviePoster 
            src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} 
            alt={movie.title} 
          />
          <Rating>{movie.vote_average.toFixed(1)}</Rating>
        </ImageContainer>
        <Content>
          <Title>{movie.title}</Title>
          <ReleaseDate>
            {new Date(movie.release_date).toLocaleDateString()}
          </ReleaseDate>
        </Content>
      </Link>
    </Card>
  );
};

 

결과물

 

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import HomePage from './pages/HomePage.tsx'
import MovieDetailPage from './pages/MovieDetailPage.tsx'

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/movie/:id" element={<MovieDetailPage />} />
      </Routes>
    </Router>
  )
}

export default App

실제 실행하는 main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)