<React๋ง ํด๋ณธ ์ด์ง์ Next.js์ Typescript๋ฅผ ์ด์ฉํ Netflix ์น์ฑ ํด๋ก ์ฝ๋ฉ>
NextJS์์ GetServerSideProps๋ฅผ ์ด์ฉํ์ฌ ๊น์ํ๊ฒ API์ ํต์ ํ๊ณ Data Fetch๊น์ง ํด๋ณด์
(feat.<style jsx>
)
Next.js ๊ฐ React.js ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ๋งํผ ๊ทธ์ ๋น์ฐํ๊ฒ ์ด์ ์ ๋ฆฌ์กํธ๋ก ๊ฐ๋ฐํ ๋์ฒ๋ผ ์ฝ๋๋ฅผ ์งฐ๋๋ฐ ์ ์ฒด๋ฅผ ์ ์ ์๋ ์ฌ๋ฌ ์ค๋ฅ๊ฐ ์๊ฒผ๋ค. ํ๋ก์ ํธ์ Next.js ๋ฅผ ์ ์ฉํ ๋ Next.js ์ ํน์ฑ์ ์ ๊ณ ๋ คํด๋ณด์..
_
ํ์์ฒ๋ผ axios ๋ฅผ ์ด์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ํ์นญํ๋ ๋ฐฉ๋ฒ์ ์ด๊ฒ์ ๊ฒ ์ค๋ฅ๊ฐ ์๊พธ ์๊ฒผ๊ณ , ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด @transtack/react-query
์์ Hydrate, QueryClient... ๋ฑ ์ฌ๋ฌ๊ฐ์ง๋ฅผ ์ํฌํธ์์ ์ฌ์ฉํด์ผํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์์๋ค.
๊ฐ์ธ์ ์ผ๋ก ์ ํธํ์ง ์๋ ๋ฐฉ์์ด๊ธฐ์ ๋๋ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ๋ด๋ฐฑํ๊ฒ Next.js ์์ ์๋ฒ์ฐ๊ฒฐ์ ํ๋ ๋ฐฉ๋ฒ์ ํํ๋ค.
๋ณธ ํฌ์คํ ์์ GetServerSideProps ๋ฅผ ๋ค๋ฃฌ ๋ถ๋ถ์ ์ฐธ๊ณ ํด์ฃผ์ธ์ !
_
๋ถ๋ช
์ฒ์ ๋น๋ํ ๋๋ ์์๊ฒ ๋น๋๋ ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋ง ๋๋ฅด๋ฉด ์คํ์ผ์ด ์ ์ฉ์ด ํ๋ ธ๋ค.
์ ์ฒด๋ฅผ ์ ์ ์๋ ๊ฒฝ๊ณ ๋ ํจ๊ป..!
Warning: Prop className
did not match. Server: "~" Client: "~"
๋ณธ ์ค๋ฅ๋ Nextjs ์ ํน์ฑ๋๋ฌธ์ ์๊ธด ์ค๋ฅ์๋ค.
๋ฐ๋ผ์ JS ํ์ผ ๋ด์ CSS๋ฅผ ๋ฃ์ด์ ์์ ํ๋ styled-component๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ก๊ณ ์นจํ ๋ ํด๋น JS๊ฐ ๋ก๋๋์ง ์๊ณ ํ๋ฆฌ๋ ๋๋ง๋ html๋ง ๋ ๋ค ๋ ๋๋ง ๋์ด๋ฒ๋ฆฌ๋ ๊ฒ์ด๋ค.
์ฌ์ค ๊ฒ์ํด๋ณด๋ฉด next.js ์์ styled-component๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํ๋ ๋ฐฉ์์ด ์ฌ๋ฌ๊ฐ ๋์จ๋ค.
(babel ์ค์น, _document.tsx ์ถ๊ฐ ๋ฑ๋ฑ..)
๊ทธ๋ ์ง๋ง.. Next.js์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ค ํ๋๊ฐ SSR ์ธ๋ฐ ๊ตณ์ด ์ด ํน์ง๊ณผ ์ถฉ๋๋๋ ๋ฌด์ธ๊ฐ๋ฅผ ํ๊ณ ์ถ์ง ์์๋ค.
ํด๊ฒฐ์ฑ ์ ์ฐพ์๋๋ค.
๋ณธ ํฌ์คํ ์์ "style jsx" ๋ฅผ ๋ค๋ฃฌ ๋ถ๋ถ์ ์ฐธ๊ณ ํด์ฃผ์ธ์
_
์์ธ์ง ๋ฐฐํฌ๊ฐ ์๋์๋ค.... ์๊ณ ๋ณด๋ " ๋น๋๊ฐ ์ ๋๋๋ผ๋ " ํ๋ก์ ํธ ๋ด์ ์ปดํ์ผ ์๋ฌ๊ฐ ์๊ธธ ์ฌ์ง๊ฐ ์กฐ๊ธ์ด๋ผ๋ ์๋ค๋ฉด ๋ฐฐํฌ๊ฐ ์๋๋ค๊ณ ํ๋ค.
๋์ ๊ฒฝ์ฐ ํ๋ก์ ํธ์์ Typescript ๋ฅผ ์ฌ์ฉํ๋ฉด์ ๋ช๊ฐ์ง props์ ํ์ ์ ์ง์ ํ์ง ์์์ ํ์ ์คํฌ๋ฆฝํธ๊ฐ ๊ฒฝ๊ณ ๋ฅผ ๋์ ๋๋ฐ ๊ทธ๊ฑฐ ๋๋ฌธ์ ๋ฐฐํฌ๊ฐ ์๋ ๊ฒ์ด์๋ค.
React, Next ํ๋ก์ ํธ์์ Typescript ์ค๋ฅ์์ด ์ฌ์ฉํ๊ธฐ
๋ฐฐํฌ์ ์๋ ๊ผผ๊ผผํ๊ฒ ํ์ธํ์...
๋ณธ ํ๋ก์ ํธ๋ ๋ง์ด ์๋ ค์ง The Movie Database API ๋ฅผ ์ฌ์ฉํ๋ค.
api url๋ค์ ๊ด๋ฆฌํ๋ ํ์ผ์ ํ์ํ url๋ค์ export ํด๋์.
// api.tsx
import { API_KEY } from "./assets/config";
const BASE_URL = `https://api.themoviedb.org/3/movie/`;
export const getNowPlaying = `${BASE_URL}/now_playing?api_key=${API_KEY}&language=en-US&page=1`;
export const getTopRated = `${BASE_URL}/top_rated?api_key=${API_KEY}&language=en-US&page=1`;
export const getPopular = `${BASE_URL}/popular?api_key=${API_KEY}&language=en-US&page=1`;
export const getUpComing = `${BASE_URL}/upcoming?api_key=${API_KEY}&language=en-US&page=1`;
export const getTopSearches = `${BASE_URL}/top_rated?api_key=${API_KEY}`;
async ์ await์ ์ ์ ํ ์ด์ฉํ์ฌ data๋ฅผ fetch ํด์ค๋ค.
๋ณธ ํจ์์์ return ํ๋ฉด fetch ๋ ๋ฐ์ดํฐ๋ค์ props ๊ฐ์ฒด๋ก ์ ๋ฌํ ์ ์๋ค!
// home.tsx
import { getNowPlaying, getTopRated, getPopular, getUpComing } from "../api";
...
export const getServerSideProps = async () => {
const nowPlayingResponse = await (await fetch(getNowPlaying)).json();
const nowPlayingMovies = nowPlayingResponse.results;
const topRatedResponse = await (await fetch(getTopRated)).json();
const topRatedMovies = topRatedResponse.results;
const popularResponse = await (await fetch(getPopular)).json();
const popularMovies = popularResponse.results;
const upComingResponse = await (await fetch(getUpComing)).json();
const upComingMovies = upComingResponse.results;
return {
props: {
nowPlayingMovies,
topRatedMovies,
popularMovies,
upComingMovies,
},
};
};
props๋ก ์๊น ๋ฆฌํดํด์ค ๊ฐ์ฒด๋ค์ ๋ฐ์์จ๋ค.
๋ฐ์ดํฐ๋ค์ ์ ๋ฌ๋ฐ์ props๋ฅผ ์ฌ์ฉํ๋ฏ์ด ํธํ๊ฒ ์ฌ์ฉํ๋ฉด ๋!
โ ๏ธ Typescript๋ฅผ ์ฐ๋๊ฒฝ์ฐ props์ ํ์ ์ ์ธํฐํ์ด์ค๋ก ์ ํด์ฃผ๋๊ฒ์ ์์ง ๋ง์ !
// home.tsx
import { IMovieInfo } from "../interface";
...
// HomeProps๋ ํ์
์คํฌ๋ฆฝํธ ์ฐ๋ ๊ฒฝ์ฐ๋ง !
interface HomeProps {
nowPlayingMovies: IMovieInfo[];
topRatedMovies: IMovieInfo[];
popularMovies: IMovieInfo[];
upComingMovies: IMovieInfo[];
}
export default function Home({
nowPlayingMovies,
topRatedMovies,
popularMovies,
upComingMovies,
}: HomeProps) {
return (
<>
<div>
<FirstMovie movies={upComingMovies} />
<TextInfo name={"Previews"} isPreview={true} />
<MovieList movies={upComingMovies} isPreview={true} />
<TextInfo name={"Now Playing"} isPreview={false} />
<MovieList movies={nowPlayingMovies} isPreview={false} />
<TextInfo name={"Top Rated"} isPreview={false} />
<MovieList movies={topRatedMovies} isPreview={false} />
<TextInfo name={"Popular"} isPreview={false} />
<MovieList movies={popularMovies} isPreview={false} />
</div>
</>
);
}
...
// MovieList.tsx
import Link from "next/link";
import { IMovieInfo } from "../../interface";
interface MovieListProps {
movies: IMovieInfo[];
isPreview: boolean;
}
export default function MovieList({ movies, isPreview }: MovieListProps) {
return (
<div className="container">
{movies.map((m: any) => (
<Link
href={{
pathname: `/movies/${m.id}`,
query: {
title: m.original_title,
poster: m.poster_path,
overview: m.overview,
},
}}
as={`/movies/${m.id}`}
key={m.id}
>
<img
className={isPreview ? "isCircle" : ""}
src={"http://image.tmdb.org/t/p/w500" + m.backdrop_path}
alt={m.title}
/>
</Link>
...ํ๋ต
<Link/>
์์ query ์ ๋ฌ<Link/>
๋ Next.js ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ผ๋ก ํ์ด์ง๋ฅผ ์ด๋ํ๊ฒ ํด์ค๋ค.
<Link href= `/movies/${m.id}`></Link>
๊ธฐ๋ณธํ์ ์ด๋ ๊ฒ ์ด๋ํ ๊ฒฝ๋ก๋ฅผ href ์ ๋ฃ์ด์ฃผ๋๊ฑด๋ฐ,
// MovieList.tsx
<Link
href={{
pathname: `/movies/${m.id}`,
query: {
title: m.original_title,
poster: m.poster_path,
overview: m.overview,
},
}}
as={`/movies/${m.id}`}
key={m.id}
>
์ด๋ ๊ฒ ์ด๋ํ๋ ํ์ด์ง ์ปดํฌ๋ํธ (์ฌ๊ธฐ์๋ /movies/[...movieData].tsx
)์ query ์ ๋ํ ์ ๋ณด๋ฅผ ํจ๊ป ์ ๋ฌ ๊ฐ๋ฅํ๋ค.
// movies/[...MovieData].tsx
import { useRouter } from "next/router";
export default function MovieDetail() {
const router = useRouter();
const { title, poster, overview } = router.query;
return (
<div>
<img src={`https://image.tmdb.org/t/p/w500/${poster}`} />
<button>โถ๏ธ Play</button>
<h2>{title}</h2>
<div>{overview}</div>
</div>
);
}
useRouter()
๋ฅผ ์ด์ฉํ์ฌ ์ฟผ๋ฆฌ์ ๋ณด๋ฅผ ๋ฐ์์ค๊ณ ์ฌ์ฉํด์ฃผ๋๊ฒ ๊ฐ๋ฅํ๋ค !
๋ง์ฝ query์ ๋ณด๋ฅผ ์๋ณด๋ด์ค๋ค๋ฉด ๊ฒฝ๋ก์ m.id ๋ฅผ ์ด์ฉํ์ฌ api์์ ์ํ ์ ๋ณด๋ฅผ ํ๋ฒ ๋ ์์ฒญํด์ผํ๋ค (์๋ฒ์ฐ๊ฒฐ์ ํ๋ฒ ๋... ใ ใ )
ํ์ง๋ง ์ด๋ ๊ฒ homeํ๋ฉด์์ ํ๋ฒ ์๋ฒ์ ํต์ ํด์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ค์ ํ์ด์ง์ query ๊ฐ์ฒด๋ก ์ ๋ฌํด์ค๋ค๋ฉด ์ถ๊ฐ์ ์ธ ์๋ฒํต์ ์์ด ๋ฐ๋ก ํ๋ฉด์ ๋ณด์ฌ์ค ์ ์๋ค !
<style jsx></style>
// TextInfo.tsx
import styled, { css } from 'styled-components';
import {ITextInfo} from '../../interfaces/interface'
export default function TextInfo({name, isPreview}:ITextInfo){
return(
<Container isPreview = {isPreview}>
{name}
</Container>
)
}
interface Props {
isPreview: boolean;
}
const Container = styled.div<Props>`
font-weight: 700;
margin-bottom: 14px;
font-size: ${props => props.isPreview ? "26.75px" : "20.92px"}
`
ํ์์ฒ๋ผ styled-component๋ก ์คํ์ผ์ ์ง ๋ค๋ฉด ์ด๋ ๊ฒ ๋๋ ํ์ผ์ด ์๋ค.
ํ์ง๋ง ์๊น ์ธ๊ธํ๊ฒ์ฒ๋ผ Next์์ ์คํ์ผ๋์ปดํฌ๋ํธ๋ฅผ ์ด์ฉํ๋ฉด ์๋ก๊ณ ์นจํ์ ๋ ์คํ์ผ์ด ์ ์ฉ๋์ง ์๋ ์ค๋ฅ๊ฐ ์๊ธฐ๊ธฐ ๋๋ฌธ์ ์๋ก์ด ๋ฐฉ์์ผ๋ก ์คํ์ผ์ ์ ์ฉํด์ค์ผํ๋ค.
๋ฐ๋ก๋ฐ๋ก style jsx !!
<style jsx>{` `}</style>
์ปดํฌ๋ํธ ์์ ๋ณธ ์ฝ๋๋ฅผ ๋ฃ์ผ๋ฉด ์ ๋ฐฑํฑ ์ฌ์ด์ css ๋ฅผ ์์ฑํด์ ์คํ์ผ์ ์ ์ฉํ ์ ์๋ค.
์ด ๊ฒฝ์ฐ JS ํ์ผ์ ๋ก๋ํ์ง ์๋๋ผ๋ pre-rendering ๊ณผ์ ์์ html ์ ๊ตฌ์ฑํ ๋ ์คํ์ผ ์ ์ฉ์ด ํจ๊ป ๋๊ธฐ ๋๋ฌธ์ ์ ์ค๋ฅ๋ ์๊ธฐ์ง ์๋๋ค
// TextInfo.tsx
import { ITextInfo } from "../../interface";
export default function TextInfo({ name, isPreview }: ITextInfo) {
return (
<>
<div className={isPreview ? "isPreview" : ""}>{name}</div>
<style jsx>{`
div {
font-weight: 700;
margin-bottom: 14px;
font-size: 20.92px;
}
.isPreview {
font-size: 26.75px;
}
`}</style>
</>
);
}
styled-component๋ฅผ ์ฌ์ฉํ๋ ๋ชจ๋ ํ์ผ๋ค์ ๋ค์๊ณผ ๊ฐ์ด style ํ๊ทธ๋ฅผ ์ด์ฉํ๋ ๋ฐฉ์์ผ๋ก ๋ฐ๊ฟ์ฃผ์๋ค ํ ํ ! ๋
ธ๊ฐ๋ค !
NextJs ์ ํน์ฑ์ ํด๋๊ตฌ์กฐ๊ฐ ๋งค์ฐ ์ค์ํ๋ค !
โโโ package-lock.json
โโโ package.json
โโโ public
โ โโโ favicon.ico
โ โโโ img --- ๊ฐ ์ปดํฌ๋ํธ์ ํ์ํ ์ด๋ฏธ์ง ์์
๋ค
โ โ โโโ Footer
โ โ โโโ Header
โ โโโ vercel.svg
โโโ src
โ โโโ api.tsx --- api ๊ฒฝ๋ก ๋ชจ์๋ ํ์ผ
โ โโโ assets
โ โ โโโ FooterInfo.json --- ํธํฐ ๋ฐ์ดํฐ ๋ฐ๋ก ๋ฝ์๋์ ํ์ผ
โ โ โโโ config.tsx --- API_KEY ๋ฃ์ด๋ ํ์ผ
โ โโโ components
โ โ โโโ common --- ๊ณตํต ์ปดํฌ๋ํธ
โ โ โ โโโ Footer.tsx
โ โ โ โโโ FooterItem.tsx
โ โ โ โโโ Header.tsx
โ โ โ โโโ Layout.tsx --- ๋ ์ด์์
โ โ โโโ home --- ํน์ ํ์ด์ง์์ ์ฐ์ด๋ ์ปดํฌ๋ํธ๋ค
โ โ โโโ FirstMovie.tsx
โ โ โโโ MovieList.tsx
โ โ โโโ TextInfo.tsx
โ โโโ interface.tsx --- ๊ณตํต์ ๊ธ๋ก ์ฐ์ด๋ ts interface
โ โโโ pages --- ๊ฐ ํ์ด์ง (ํ์ผ๋ช
์ ๊ฒฝ๋ก์ด๋ฆ๊ณผ ๋์ผ)
โ โโโ _app.tsx --- ๊ฐ์ฅ ๋จผ์ ๋ ๋๋ง ๋๋ ๊ธฐ๋ณธ ํ์ผ
โ โโโ home.tsx --- ๊ฒฝ๋ก : /home
โ โโโ index.tsx --- ๊ฒฝ๋ก : /
โ โโโ movies
โ โ โโโ [...movieData].tsx --- ๊ฒฝ๋ก :/movies/movie_id
โ โโโ search.tsx --- ๊ฒฝ๋ก : /search
โโโ styles
โ โโโ globals.css --- ์ ์ญ ์คํ์ผ
โโโ tsconfig.json
ํน์ ๊นํ๋ธ ์ฃผ์๋ฅผ ์์์์๊น์? ๊ณต๋ถํ๊ณ ์ถ์๋ฐ..