프로젝트명
영화 웹 사이트 (Movies web)
기간 / 인원
23.12.04 ~ 12.11 (1주) / 1인
소개
Next.JS와 영화 무료 API를 활용하여 만든 영화 웹 사이트입니다.
배포 링크
🔗 : Movies-web (Vercel로 배포하였습니다.)
Home<!-- --> | Next Movies
movies-web-ebon.vercel.app
Github 링크
https://github.com/ghvhdh333/Movies-Web
GitHub - ghvhdh333/Movies-Web: Next.js로 영화 사이트 만들기
Next.js로 영화 사이트 만들기. Contribute to ghvhdh333/Movies-Web development by creating an account on GitHub.
github.com
기술 스택
[ FE ]
Next.JS, HTML, Styled-jsx(CSS)
[ Environment ]
VScode, Git, Github, Vercel
회고
사용자에게 API_KEY를 숨김
# 1
- API_KEY를 공개하여 API 호출을 통해 데이터를 불러옵니다.
- 단점 : 사용자에게 API_KEY가 공개되어, 보안상 위험을 받습니다.
// 기존 코드
// index.js
const API_KEY = '~~~';
const [movies, setMovies] = useState();
useEffect(() => {
(async () => {
const { results } = await (
await fetch(`https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`)).json();
setMovies(results);
})();
}, []);
export default function Home () {
...
return (
...
{!movies && <h2 className="loading">Loading...</h2>}
{movies?.map((movie) => (
<Link href={`/info/${movie.id}`} key={movie.id}>
<div className="movie">
<img src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} />
<h4>{movie.original_title}</h4>
</div>
</Link>
))}
...
)
}
# 2 (# 1 개선)
- env와 gitignore, rewrites를 활용하여 사용자에게 API_KEY를 비공개합니다.
: rewrites()를 활용하면 유저가 보는 사이트 내에서는 API_KEY를 숨겨주지만,
github에 업로드될 때 API_KEY도 같이 업로드되기에
.env와 gitignore 활용하여 github에 업로드되지 않도록 구현하였습니다.
아래와 같은 코드 작성 후 유저에게 API_KEY가 공개될 위험이 현저히 줄어들었습니다.
- 단점 : React.js의 작업이 끝나기 전까지 사용자가 loading 화면을 봅니다. (👇 아래 영상 참고 )
(짧은 시간이지만 사용자는 loading 화면을 봅니다.)
// 변경한 코드
// index.js
const [movies, setMovies] = useState();
useEffect(() => {
(async () => {
const { results } = await (
await fetch(`/api/movies`)).json();
setMovies(results);
})();
}, []);
...
// next.config.js
module.exports = {
reactStrictMode: true,
// url을 변경하여 보여준다. url을 숨길 수 있다. (API KEY 등을 숨겨야 하는 경우에 사용된다.)
async rewrites() {
return [
{
source: "/api/movies",
destination: `https://api.themoviedb.org/3/movie/popular?api_key=${process.env.NEXT_PUBLIC_API_KEY}`,
},
];
},
};
// .env
NEXT_PUBLIC_API_KEY = ~~~
# 3 (#2 개선)
- SSR로 API를 호출하여 데이터를 불러옵니다. (Sever-Side-Rendering)
: 서버에서 작업이 다 끝나면 한 번에 화면을 보여줍니다.
사용자가 loading 화면을 보지 않으며, 아래 코드를 줄일 수 있었습니다.
// .index.js
{!movies && <h2 className="loading">Loading...</h2>} // 코드 삭제함
- 단점 : 서버 작업의 지연시간만큼 사용자가 흰 화면만 보게 됩니다. (👇 아래 영상 참고 )
(서버에서 작업이 다 끝나면 한 번에 화면을 보여주기에 loading 화면이 나오지 않습니다.)
// index.js
// getServerSideProps 함수로 인해 api의 응답받은 results를 받아온다.
export default function Home({ results }) {
...
{results?.map((movie) => (
<Link href={`/info/${movie.id}`} key={movie.id}>
<div className="movie"}>
<img src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} />
<h4>{movie.original_title}</h4>
</div>
</Link>
))}
...
}
// 서버에서 동작한다.(SSR)
export async function getServerSideProps(){
const { results } = await (await fetch(`http://localhost:3000/api/movies/popular`)).json();
return {
props: {
results,
},
};
}
#4 (#3 에러 핸들링)
배포 단계에서 500에러가 계속 발생하여 CSR로 변경해서 데이터를 불러옴
export default function Home() {
const [homeData, setHomeData] = useState([]);
const getHomeData = async () => {
try {
// 서버 api 호출
const response = await axios.get(`/api/movies/popular`, {
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "69420",
},
});
setHomeData(response.data.results);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getHomeData();
}, []);
return (
...
)
}
CSS를 Styled-jsx로 작성
Tailwind.CSS로 할 수 있었지만, 처음 보는 방법이기에 적용해보고 싶었습니다.
Styled-jsx는 자동적으로 랜덤 한 숫자를 붙여 class를 만들어주고, 페이지 별로 고유하게 작동하므로
다른 페이지에서 같은 class명을 사용하더라도 다른 className이 지정되어 CSS가 다르게 적용됩니다.
Tailwind.CSS와 비슷하게 JS파일 안에 작성이 가능하다는 장점이 있습니다.
사용해 본 결과
장점으로는 페이지 별로 적용되므로 Class명을 크게 신경 쓸 필요가 없고,
하나의 파일에서 작업할 수 있다는 장점이 있습니다.
단점으로는 JS파일에 CSS까지 같이 들어가다 보니 파일이 너무 커지고,
CSS가 자동완성이 되지 않기에 작성하기가 많이 불편하였습니다.
Styled-jsx를 사용하기보다는 Tailwind CSS를 사용할 것 같습니다.
import Link from "next/link";
import { useRouter } from "next/router";
// CSS를 style jsx 방법으로 작성하였다.
export default function NavBar(){
const router = useRouter();
return (
<nav>
<Link href='/'>
<h1 className="title">Movies</h1>
</Link>
<div>
<Link href="/">
<span className={router.pathname === "/" ? "active" : ""}>Home</span>
</Link>
<Link href="/about">
<span className={router.pathname === "/about" ? "active" : ""}>About</span>
</Link>
</div>
<style jsx>{`
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px 20px 30px;
box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px,
rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;
}
.title {
display: inline;
margin: 0;
}
nav div {
display: flex;
gap: 20px;
}
nav span {
font-weight: 600;
font-size: 18px;
}
.active {
color: tomato;
}
`}</style>
</nav>
)
}
페이지 별로 Title 변경
# 1
- 페이지별로 하드코딩하여 Title 변경함
- 단점 : 현재는 페이지 수가 적기에 가능하지만,
수 백, 수 천 개의 페이지가 있다면 페이지 별로 일일이 하드코딩으로 작성할 수 없으며, 비효율적입니다.
// 기존 방식
// index.js
<Head>
<title>Home | Next Movies</title>
</Head>
// about.js
<Head>
<title>About | Next Movies</title>
</Head>
# 2 (# 1 개선)
- router.pathname을 활용하여 현재 유저가 위치하고 사이트를 이용하여 title 제목을 변경하였습니다.
: router.pathname으로 현재 위치하고 있는 페이지의 정보를 가져와서 작성하였기에,
이전 코드에 비해 훨씬 간결하고, 작업 효율도 증가하였습니다.
(엣지 포인트로 Home 페이지는 '/'로 고정되어 있기에 if문을 통해 "Home"을 재할당 해주었습니다.)
// 변경한 방식
// components/Seo.js
import Head from "next/head";
import { useRouter }from "next/router";
// Title 변경하는 컴포넌트
// router.pathname을 사용하여 현재 유저가 위치하고 있는 page를 구할 수 있다.
export default function Seo() {
const router = useRouter();
let title = [...router.pathname.slice(1)]
.map((el, index) => {
return index === 0 ? el.toUpperCase() : el
}).join('');
if(title === "") {
title = "Home";
}
return (
<Head>
<title>{title} | Next Movies</title>
</Head>
)
}
// index.js
<Seo/>
// about.js
<Seo/>
영화 아이템 클릭 시 영화 정보 사이트로 이동
# 1 (선택)
- useState, useEffect를 사용하여 API를 호출하여 데이터를 불러온다.
: moive item을 클릭하면 URL에 id값이 동적으로 들어간다.
그리고, /info/[id] 페이지로 이동하고, router로 영화의 id값을 가져온 뒤, API를 호출하여 id에 맞는 영화 정보를 가져온다.
- 장점 : 필요한 데이터만을 불러올 수 있다.
- 단점 : API 호출 수가 증가한다. (사용자가 많아질수록 사용 횟수가 점점 증가하고, 그만큼 많은 비용이 발생한다.)
- 선택 이유 : 가져올 정보가 많아 모든 정보를 URL query를 통해 전달받기엔 적합하지 않다고 생각했습니다.
// index.js
export default function Popular({result}) {
return (
...
{result?.map((movie) => (
<Link href={`/info/${movie.id}`} key={movie.id}>
<div className="movie">
<img src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} />
<h4>{movie.original_title}</h4>
</div>
</Link>
))}
...
);
}
export async function getServerSideProps(){
const { results } = await (await fetch(`http://localhost:3000/api/movies/popular`)).json();
return {
props: {
results,
},
};
}
// info/[id].js
export default function Info() {
const router = useRouter();
const { id } = router.query;
const [movie, setMovie] = useState([]);
useEffect(() => {
if(id){
(async () => {
const data = await (
await fetch(`/api/moive/info/${id}`)).json();
setMovie(data);
})();
}
}, [id]);
return (
<div className='container'>
<Seo />
{movie.length !== 0 && id ?
<>
<img src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} />
<section className="movie_detail">
<div className="detail_items">
<div className="detail_box">Title</div>
<span className="detail_text">{movie.title}</span>
</div>
<div className="detail_items">
<div className="detail_box">Movie ID</div>
<span className="detail_text">{movie.id}</span>
</div>
<div className="detail_items">
<div className="detail_box">Language</div>
<span className="detail_text">{movie.original_language}</span>
</div>
<div className="detail_items">
<div className="detail_box">Release Date</div>
<span className="detail_text">{movie.release_date}</span>
</div>
<div className="detail_items">
<div className="detail_box">Runtime</div>
<span className="detail_text">{movie.runtime} min</span>
</div>
<div className="detail_items">
<div className="detail_box">Vote Average</div>
<span className="detail_text vote_average">{movie.vote_average}</span>
<span> / 10</span>
</div>
<div className="detail_items">
<div className="detail_box">Vote Count</div>
<span className="detail_text">{movie.vote_count} 🙋♂️</span>
</div>
<div>
<div className="detail_box">Overview</div>
<div className="detail_overview">{movie.overview}</div>
</div>
</section>
</>
: <h2 className="loading">Loading...</h2>
}
</div>
);
}
# 2 (다른 방법 시도)
- URL query를 사용하여 데이터를 넘겨준다. (URL에 정보를 숨겨서 보낼 수 있다.)
: movie list를 클릭하면 onClick 이벤트가 발생하여 해당 이벤트 발생으로 인해 URL 쿼리에 정보를 담고,
info/[id] 페이지로 이동하고, router로 데이터를 가져온다.
- 장점 : API 호출 수를 줄일 수 있다. (= 비용을 아낄 수 있다.)
- 단점 : 1. 직접 URL를 입력하여 /info/[id]로 접속하면, router.query에 값이 없어 화면을 보여주지 못한다.
2. 개인적인 생각으로는 유저가 폼 박스 등을 submit 하였을 때나 URL에 담을 데이터양이 작을 때 사용하면 좋을 것 같다.
// index.js
export default function Home({ results }) {
const router = useRouter();
const onClick = (id, title, language, release_date, runtime, vote_average, vote_count, overview) => {
router.push(
{
pathname : `/info/${id}`,
query : {
id,
title,
language,
release_date,
runtime,
vote_average,
vote_count,
overview,
},
},
`/info/${id}`
)
};
return (
...
{results?.map((movie) => (
<Link href={`/info/${movie.id}`} key={movie.id}>
<div className="movie" onClick={() => onClick(
movie.id,
movie.original_title,
movie.original_language,
movie.release_date,
movie.runtime,
movie.vote_average,
movie.vote_count,
movie.overview, )}>
<img src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} />
<h4>{movie.original_title}</h4>
</div>
</Link>
))}
...
// info/[id]
// 간단히 요점만 적어보았습니다.
export default function Info() {
const router = useRouter();
return (
<div>
<h1>{router.query.title}</h1>
<h4>{router.query.id}</h4>
<h4>{router.query.language}</h4>
<h4>{router.query.release_date}</h4>
<h4>{router.query.runtime}</h4>
<h4>{router.query.vote_average}</h4>
<h4>{router.query.vote_count}</h4>
<h4>{router.query.overview}</h4>
</div>
)
}
# 3 (# 1 + # 2)
- URL query를 사용하여 데이터를 넘겨주며, 새로고침 시 API를 호출하여 데이터를 가져온다. (#1, 2의 장점 + 단점보완)
: 1. 영화 아이템을 클릭하여 /info/[id]에 접속한 경우 ( === # 2)
- movie item을 클릭하면 onClick 이벤트가 발생하여 URL query에 정보를 담고,
/info/[id] 페이지로 이동한 뒤, router로 데이터를 가져온다.
2. 새로고침 또는 직접 URL에 접속한 경우
- useState, useEffect 등을 사용하여 기존의 API를 호출 방식처럼 데이터를 불러온다.
: movie item을 클릭하면 URL에 id값이 동적으로 들어간다.
그리고, /info/[id] 페이지로 이동하고, router로 영화의 id값을 가져온 뒤, API를 호출하여 id에 맞는 영화 정보를 가져온다.
- 장점 : API 호출 수를 줄일 수 있다. (비용 절감), 필요한 데이터만을 불러올 수 있다.
- 단점 : #1과 #2의 단점을 상쇄시킵니다.
배포 문제
- React에서 배포했던 것처럼 AWS의 S3로 배포하려고 했으나, 예상치 못한 문제가 발생하여 배포하지 못하고 있습니다.
스스로 생각해 본 결과, AWS(S3)는 정적 웹 사이트 호스팅인데
위의 코드에서 /info/[id]와 같이 동적 라우팅이 있어서 배포가 정상적으로 되지 않은 것 같습니다.
해당 문제를 최대한 빨리 해결하고, 배포할 수 있도록 노력하겠습니다.
-> 해당 문제를 해결하고 Vercel로 배포하였습니다.
느낀 점
- 첫 Next.JS 사용
: 이번 미니 프로젝트를 통해 Next.Js를 처음 사용해 봤습니다.
하지만 React와 크게 다르지 않았으며, 라우팅 방식과 CSR, SSR인지, 프레임워크와 라이브러리인지 여부의 차이였으며,
CSR, SSR와 프레임워크, 라이브러리의 차이에 대해 더욱 이해할 수 있었던 시간이었습니다.
- CSR, SSR에 대한 생각
: React(CSR), Next.js(SSR & CSR)라고 생각하고 있는데, SSR이 사용자 입장에서 더욱 좋은 것 같다.
단, 웹, 앱 등을 만드는 곳이 기업이므로, 내가 만약 웹, 앱을 만드는 기업인데 SSR과 CSR 중 하나만 사용해서 만들어라고 한다면
검색엔진에 엄청 유리해야 되는 프로젝트가 아니고서야 SSR보다는 CSR를 사용해서 작업할 것 같다.
왜냐하면 CSR이 SSR에 비해 검색엔진에 불리하지만, SSR은 서버에서 렌더링 후 유저에게 보여주는 형식이기에
서버의 사용량이 CSR보다 많다. 서버의 사용량이 곧 회사의 돈(비용)이기 때문이라는 생각이 들었다.
(물론, Next.js나 React에서 두 가지(CSR, SSR)를 섞어서 사용하는 것이 제일 좋을 것 같다.)