아직 TypeScript를 사용해 본 적이 없고 최근에 Next.js를 배워 두 개를 결합해서 사용해 보자 해서 간단한 프로잭트를 진행해 봤습니다.
니콜라스의 영상강의를 참고해서 만들었습니다
import Seo from '../components/Seo';
import { useRouter } from 'next/router';
import Link from 'next/link';
import axios from 'axios';
interface Movies {
datas: {
results: any;
};
}
// _app.tsx에서 pageProps를 index.tsx에 props로 줍니다.
// interface를 사용해서 조금더 간결하게 props Type를 명시해줬습니다.(아직 타입명시가 서툽니다.)
const Home = ({ datas: { results } }: Movies) => {
const router = useRouter();
const movieDatas = ({ id, title }: { id: number; title: string }) => {
router.push(`/movies/${title}/${id}`);
};
return (
<div>
<h1 className="title">Movies API Test Page</h1>
<div className="container">
<Seo title="Home" />
{results?.map((movie: any) => (
<div
onClick={() =>
movieDatas({ id: movie.id, title: movie.original_title })
}
className="movie"
key={movie.id}
>
<img
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={'Movie Img'}
/>
<h4>
<Link href={`/movies/${movie.original_title}/${movie.id}`}>
<a>{movie.original_title}</a>
</Link>
</h4>
</div>
))}
</div>
<style jsx>{`
.container {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 20px;
gap: 20px;
}
h1.title {
text-align: center;
}
.movie {
cursor: pointer;
}
.movie img {
max-width: 100%;
border-radius: 12px;
transition: transform 0.2s ease-in-out;
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
}
.movie:hover img {
transform: scale(1.05) translateY(-10px);
}
.movie h4 {
font-size: 18px;
text-align: center;
}
`}</style>
</div>
);
};
//Next.js의 특징인 SSR을 사용해 봤습니다.
export const getServerSideProps = async () => {
const data = await (await axios.get(`http://localhost:3000/api/movies`)).data;
return {
props: {
datas: data,
},
};
};
export default Home;
_app은 서버로 요청이 들어왔을 때 가장 먼저 실행되는 컴포넌트로 페이지에 적용할 공통 레이아웃의 역할을 합니다.
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import Layout from '../components/Layout';
// Component, pageProps 는 현제 보는 페이지의 컴포넌트와 getServerSideProps등으로 리턴한 props를 받아옵니다
function MyApp({ Component, pageProps }: AppProps) {
return (
// 페이지의 기본 레이아웃을 적용
<Layout>
// index.tsx page에서 SSR을 사용해 받아온 영화 데이터를 pageProps로 받아와
// index.tsx에 props를 줍니다
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
pages디렉토리 안에 /movies
디렉토리를 추가하면 http://localhost:3000/movies
url을 사용할수 있습니다. /movies
안에 [...params].tsx
파일을 만들면 movies/
뒤에 붙는 값을 params
라는 이름의 변수로 가져올수 있습니다.
파일 이름을 대괄호 [ ]로 감싸야합니다.
http://localhost:3000/디렉토리/[파일이름].tsx
import Seo from '../../components/Seo';
import axios from 'axios';
import Link from 'next/link';
export default function Detail({ movieDetail }: any) {
return (
<div className="container">
<Seo title={movieDetail?.original_title} />
<Link href={movieDetail?.homepage}>
<a className="homePage">
<h2>{movieDetail?.title}</h2>
</a>
</Link>
<img
src={`https://image.tmdb.org/t/p/w500${movieDetail?.poster_path}`}
alt="img"
className="mainImg"
/>
<div className="tagListBox">
<span className="tage">Tage</span>
<div className="tagList" id="tagList">
{movieDetail?.genres.map(
({ name: name }: { name: any }, index: any) => (
<div className="tageItem" key={index}>
{name}
</div>
),
)}
</div>
</div>
<div className="movieDatas">
<div className="movieData">
<div>
{`Country of origin : ${movieDetail?.production_countries[0].iso_3166_1}`}
</div>
<div>{`Companies : ${movieDetail?.production_countries[0].name}`}</div>
<div>{`Movie Release : ${movieDetail?.release_date}`}</div>
<div>{`Run Time : ${movieDetail?.runtime}m`}</div>
<div>{`Spoken Language : ${movieDetail?.spoken_languages.map(
(v: { english_name: any }) => v.english_name,
)}`}</div>
<div>{`Vote Average : ${movieDetail?.vote_average}`}</div>
<div>{`Audience : ${movieDetail?.popularity}`}</div>
</div>
</div>
<h2>Summary</h2>
<div className="overview">
<span>{movieDetail?.overview}</span>
</div>
<style jsx>{`
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
.mainImg {
width: 400px;
border-radius: 16px;
margin-bottom: 30px;
}
.mainImg:hover {
transform: scale(1.05);
transition: 0.3s;
}
.tagListBox {
width: 400px;
display: flex;
flex-direction: row;
margin-bottom: 20px;
align-items: center;
justify-content: start;
}
.tagListBox .tage {
font-size: 16px;
font-weight: 600;
background: #737373;
color: #fff;
padding: 8px;
border-radius: 16px;
margin-right: 10px;
}
.tagListBox .tagList {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: scroll;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none;
border-left: 2px solid #ddd;
padding-left: 10px;
}
.tagListBox .tagList::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
.tagListBox .tageItem {
font-size: 12px;
padding: 8px;
border-radius: 16px;
margin-right: 10px;
border: 1px solid #ddd;
white-space: nowrap;
}
.movieDatas {
width: 400px;
display: flex;
flex-wrap: nowrap;
background: #737373;
padding: 0 20px;
box-sizing: border-box;
margin: 20px 0;
border-radius: 16px;
}
.movieDatas .movieData {
width: 100%;
display: flex;
flex-direction: column;
padding: 10px 0;
}
.movieDatas .movieData div {
color: #fff;
width: 90%;
margin: 10px 0;
padding-bottom: 7px;
border-bottom: 1px solid #fff;
font-size: 14px;
}
.homePage {
margin: 20px 0;
width: 100%;
text-align: center;
}
.homePage:hover {
transform: scale(1.1);
color: gray;
transition: 0.3s;
}
.overview {
width: 400px;
line-height: 22px;
border: solid 2px #737373;
padding: 20px 30px;
border-radius: 18px;
word-break: normal;
}
`}</style>
</div>
);
}
// movies/ 뒤에 붙는 값을 params라는 변수로 가져와서
// API 요청에 변수를 넣어 영화 상세정보를 가져옵니다.
export async function getServerSideProps({ params: { params } }: any) {
const movieDetail = await (
await axios.get(`http://localhost:3000/api/movies/${params[1]}`)
).data;
return {
props: {
movieDetail,
},
};
}
_app.tsx
에 적용시켜 모든페이지에 공통된 레이아웃을 출력했습니다.
import NavBar from './NavBar';
// children Type 명시
export default function Layout({ children }: { children: JSX.Element }) {
return (
<>
// 페이지 최상단에 NavBar과 하단에 Page를 출력
<NavBar />
<div>{children}</div>
</>
);
}
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function NavBar() {
// useRouter을 이용해 현제 페이지에 맞는 카테고리 텍스트 에 색상을 부여
const router = useRouter();
return (
<nav>
<img src="/vercel.svg" alt="imgs" />
<div>
<Link href="/">
<a className={router.pathname === '/' ? 'active' : ''}>Home</a>
</Link>
<Link href="/about">
<a className={router.pathname === '/about' ? 'active' : ''}>Search</a>
</Link>
</div>
<style jsx>{`
nav {
display: flex;
gap: 10px;
flex-direction: column;
align-items: center;
padding-top: 20px;
padding-bottom: 10px;
box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px,
rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;
}
img {
max-width: 100px;
margin-bottom: 5px;
}
nav a {
font-weight: 600;
font-size: 18px;
}
.active {
color: tomato;
}
nav div {
display: flex;
gap: 10px;
}
`}</style>
</nav>
);
}
// HTML의 Head테그와 똑같습니다.
import Head from 'next/head';
// Page에 넣어 title를 props로 받아와 페이지마다 title를 변경, title props Type 명시.
export default function Seo({ title }: { title: string }) {
return (
<Head>
<title>{title} | Next Movies</title>
</Head>
);
}
API키를 숨기기 위해서 사용.
// 뭣모르고 뒤에 세미클론 붙였다가 한동안 애먹었었습니다.
MOVIE_API_KEY=API_KEY
중요한부분
source : 들어오는 요청 경로 패턴입니다.
destination : 변경할 경로입니다.
redirects : URL요청시 새페이지로 다시 라우팅.
rewrites : URL요청시 변경된 값만 표시.
/** @type {import('next').NextConfig} */
// .env에 숨긴 API_KEY
const API_KEY = process.env.MOVIE_API_KEY;
module.exports = {
reactStrictMode: true,
// 리더렉션(redirects)은 요청경로(source)를 다른경로(destination)로 연결해 줍니다
async redirects() {
return [
{
// source에서 /old-blog/:path* 를 요청시
source: '/old-blog/:path*',
// destination의 '/new-sexy-blog/:path*' 로 연결
// '/old-blog/:path*' 뒤에 :path* 는 /old-blog/ 뒤에 붙은 값을 destination의 :path* 에 출력
destination: '/new-sexy-blog/:path*',
// 클라이언트/검색 엔진에 리디렉션을 영구적으로 캐시하는지 여부
permanent: false,
},
];
},
// rewrites를 사용하면 들어오는 요청 경로를 다른 대상 경로에 매핑할 수 있습니다(API 사용).
async rewrites() {
return [
{
// Page나 component에서 API를 source URL처럼 요청하면 destination의 URL을 반환해 줍니다
source: '/api/movies',
// .env에 숨긴 API_KEY
destination: `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`,
},
{
// /api/movies 뒤에 영화 id값을 :id로 받아와 destination에 추가해 상세데이터를 받아오도록 작성
source: '/api/movies/:id',
destination: `https://api.themoviedb.org/3/movie/:id?api_key=${API_KEY}`,
},
];
},
};
궁금한 영화 이미지를 클릭하면 onClick 이벤트를 통해 useRouter로 주소 url에 title
과 id
값 을 넣어 http://localhost:3000/movies/:title/:id
url로 페이지 라우팅을 합니다 그러면 pages/movies/[...params].tsx
에서 pages/movies/
뒤에 있는 title, id 값을 params
라는 변수로 받아올수 있습니다.
사실 영화 이미지에 title 값을 넣을 필요는 없는데 그냥 내버려 뒀습니다.
Next.js는 디렉토리 구조만 잘 짜면 개발이 많이 편한것 같네요.
이거 API 키 어디서 받아야하나요?