MovieDetailPage 부분
1. 파일 상단에 'use client' 지시문 추가
**제목
'use client'
Next.js 13 이상에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트이다.
React hooks를 사용하는 컴포넌트는 클라이언트 컴포넌트로 지정해야 하므로 'use client' 지시문이 필요
// 기존 React Router
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Next.js
import { useParams, useRouter } from 'next/navigation';
const params =useParams();
const id = params?.id as string; // ?가 없을 경우
const router = useRouter();
2. 라우팅 관련 훅 변경
// 기존 React Router
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Next.js
import { useParams, useRouter } from 'next/navigation';
const params =useParams();
const id = params?.id as string; // ?가 없을 경우
const router = useRouter();
- const { id } = useParams<{ id: string }>(); → const params = useParams();
- useParams()가 반환하는 것이 객체이기 때문에 변수명을 바꿔준다.(이름은 사실 상관없다. id 사용해도 괜찮음)
- 이 객체는 URL의 모든 동적 파라미터를 포함한다.
- 동적 파라미터(Dynamic Parameters)는 URL에서 변할 수 있는 값을 의미한다.
- 쉽게 말해서, URL의 일부분이 고정되지 않고 상황에 따라 달라질 수 있는 부분이다.
- http://localhost:3000/MovieDetailPage/1045938 만약에 이렇다면
- http://localhost:3000/MovieDetailPage/ 부분까지가 고정된 url이고 숫자부분이 동적 파라미터이다.
- Next.js는 폴더 구조가 url의 경로이기도 하기 때문에 폴더 구조로 동적 파라미터를 설정한다.
//기본 동적 라우트 [id] 부분
/pages/MovieDetailPage/[id]/page.tsx
//[id] : 대괄호 안의 내용이 동적 파라미터
//여러 동적 파라미터 [category]/[id] 부분
/pages/[category]/[id]/page.tsx
URL 예시: /action/1045938
params: { category: 'action', id: '1045938' }
- const id = params?.id as string;
- Next.js의 useParams()는 제네릭을 지원하지 않기 때문에 타입 단언을 별도로 해줘야한다.
- params.id에만 타입 단언이 필요한데, 불필요하게 전체 객체에 타입 단언을 하지 않기 위해 params.id에만 적용한다.
- params의 값이 null 또는 undefined일 수 있기 때문에 옵셔널 체이닝(Optional Chaining) 연산자인 '?'를 사용해서 값이 null이거나 undefined일때 undefined를 반환해서 오류를 없애준다.
- const navigate = useNavigate(); → const router = useRouter();
- useRouter()는 Next.js의 내장 훅으로, 페이지 간 네비게이션을 제어하는데 사용되며 navigate 보다 더 많은 기능을 제공한다.
// Next.js | React Router
router.push('/home') | navigate('/home')
router.replace('/home') | navigate('/home', { replace: true })
router.back() | navigate(-1)
router.forward() | navigate(1)
router.refresh() | [대응되는 기능 없음]
router.prefetch() | [대응되는 기능 없음]
3. 페이지 파일 위치
- 기존 : src/pages/MovieDetailPage.tsx
- Next.js : /app/movie/[id]/page.tsx
4. 네비게이션 로직 변경
// 기존
<BackButton onClick={() => navigate(-1)}>
// Next.js
<BackButton onClick={() => router.back()}>
- navigate(-1) → router.back() : 네비게이션 방식에서 라우터 방식으로 뒤로가기 구현
5. 타입스크립트 통합
import { Params } from 'next/dist/shared/lib/router/utils/route-matcher'
Next.js는 자체 타입 정의를 제공하므로, 이를 활용하는 것이 권장된다.
6. 이미지 처리
// 기존
<PosterImage
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
/>
const PosterImage = styled.img`
max-width: 300px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;
// Next.js
import Image from 'next/image'
<MoviePoster
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
width={0}
height={0}
sizes="400px"
style={{
width: 'auto',
height: '375px',
}}
/>
- import Image from 'next/image' : Next.js의 내장 이미지 최적화 컴포넌트이다.
- Image 컴포넌트로 감싸고 설정을 해준다.
- 기존 styled-components 스타일을 그대로 사용하면 Next.js의 이미지 최적화 기능을 사용할 수 없게된다. 때문에 최적화가 필요없고 편의성을 위한다면 기존 스타일을 사용하고 그렇지 않다면 Image 컴포넌트를 사용하면 된다.
- 자동 이미지 최적화
- img 태그는 원본 이미지를 그대로 불러와서 큰 이미지 파일을 그대로 로드하게 된다.
- Image 컴포넌트는 원본 이미지를 실제 표시할 크기(300px)에 맞게 서버에서 자동으로 리사이징하면서 이미지 용량도 최적화한다.(예: 200KB로 압축)
- WebP와 같은 최신 이미지 포맷 지원
- 지연 로딩 기능 제공 : 기존의
태그는 지연 로딩을 수동으로 구현해야 한다.
- 레이아웃 시프트 방지 : 레이아웃 시프트(Layout Shift) : 이미지가 로드되기 전과 후에 페이지 레이아웃이 흔들리는 현상
- 자동 이미지 최적화
또한 기본적으로 Image 컴포넌트는 width와 height가 명시적으로 존재해야한다.
<MoviePoster
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
width={375px}
height={400px}
sizes="400px"
/>
이렇게 명시적으로 width={375px}, height={400px} 크기가 정해져야한다.
<MoviePoster
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
width={0}
height={0}
sizes="400px"
style={{
width: 'auto',
height: '375px',
}}
/>
- 혹은 width={0}, height={0}으로 Next.js Image의 기본 레이아웃 계산을 비활성화한 다음
- style 속성에 따라 크기가 결정되도록 할 수 있다.
- 이 경우엔 이미지의 원본 비율을 유지하면서 이미지의 크기가 결정된다.
- Next.js Image 컴포넌트의 필수 속성
- src (이미지 경로)
- alt (대체 텍스트)
- width
- height
- 추가 속성
- sizes ={크기} : 이 경우 불러오는 이미지의 대략적인 크기를 정한다. 만약에 없다면 100vw를 기본으로 잡아서 너무 큰 이미지를 로드하게 되어 로딩이 느려질 수 있다.
- priority={true} : 페이지 로드 시 즉시 로딩
- quality={75} : 1-100 사이의 이미지 품질
- placeholder="blur" : 이미지 로딩 중 표시할 블러 효과
- loading="lazy" : 지연 로딩 (기본값)
'use client' // 추가됨
import { useState, useEffect } from 'react';
import { getMovieDetails, MovieDetail } from '@/services/movieApi';
import styled from 'styled-components'; // 스타일링을 위해 사용
import { useParams, useRouter } from 'next/navigation';
import Image from 'next/image'
const MovieDetailPage = () => {
const params =useParams();
const router = useRouter();
const id = params?.id as string;
const [movie, setMovie] = useState<MovieDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
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]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!movie) return <div>Movie not found</div>;
return (
<Container>
<BackButton onClick={() => router.back()}>← 이전</BackButton>
<MovieHeader>
<Image
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
width={300}
height={450}
/>
<MovieInfo>
<h1>{movie.title}</h1>
<Tagline>{movie.tagline}</Tagline>
<InfoSection>
<h3>개요</h3>
<p>{movie.overview}</p>
</InfoSection>
<InfoGrid>
<InfoItem>
<Label>출시일</Label>
<Value>{new Date(movie.release_date).toLocaleDateString()}</Value>
</InfoItem>
<InfoItem>
<Label>상영 시간</Label>
<Value>{movie.runtime} 분</Value>
</InfoItem>
<InfoItem>
<Label>평점</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>제작사</h2>
<CompanyList>
{movie.production_companies.map(company => (
<CompanyItem key={company.id}>
{company.name}
</CompanyItem>
))}
</CompanyList>
</AdditionalInfo>
</Container>
);
};
// 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 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;
`;
export default MovieDetailPage
2. MovieCard tsx부분
import 부분 변경
// MovieCard.tsx
(제거) import { Link } from 'react-router-dom';
(추가) import Link from 'next/link';
(추가) import Image from 'next/image';
Link 컴포넌트 속성 변경
<Card>
(삭제)<Link to={`/movie/${movie.id}`}>
(변경)<Link href={`/movie/${movie.id}`}>
스타일링 수정
(삭제) const MoviePoster = styled.img`
(변경) const MoviePoster = styled(Image)`
//Next.js
const MoviePoster = styled(Image)`
object-fit: cover;
`;
//width와 height는 Image 컴포넌트에 작성 되야 하기 때문에 삭제
이미지 처리 방식 변경
//기존
<MoviePoster
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
/>
//Next.js
<MoviePoster
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
width={500}
height={375}
priority
/>
next.config.js 파일에 이미지 도메인 설정
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
domains: ['image.tmdb.org'],
},
};
export default nextConfig;
- Next.js의 Image 컴포넌트는 보안상의 이유로 외부 이미지 도메인을 명시적으로 설정해야 한다.
- 때문에 도메인 주소인 image.tmdb.org부분을 config 파일에 입력해줘야한다.
- 다수의 도메인의 경우 쉼표로 구분해서 넣어주면 된다.
movieApi.ts 부분
환경 변수 설정
(제거) const API_KEY = import.meta.env.VITE_APP_TMDB_API_KEY;
(추가) const API_KEY = process.env.NEXT_PUBLIC_TMDB_API_KEY;
.env.local 파일 생성
NEXT_PUBLIC_TMDB_API_KEY=api키 번호
- Vite의 .env 파일에서 Next.js의 .env.local 파일로 변경
파일 경로
moviedetail의 경우 id를 전달하기 때문에
app/MovieDetailPage/[id]/page.tsx
구조여야한다.
'nextjs > 마이그레이션 예제' 카테고리의 다른 글
Next.js로 마이그레이션하기 - React Router에서(영화 목록1) (0) | 2025.04.12 |
---|