Skip to content

Commit

Permalink
[서버 사이드 렌더링 - 2단계] 빙봉(김윤경) 미션 제출합니다. (#55)
Browse files Browse the repository at this point in the history
* fix: 뼈대 코드 수정 및 샘플 코드 제공

* feat: MovieList 컴포넌트 구현

* feat: Header 컴포넌트 구현

* feat: Footer 컴포넌트 구현

* feat: Header, MovieList, Footer 컴포넌트를 App 컴포넌트에 추가

* feat: TMDB API 호출을 위한 상수 및 fetchMovieItems 함수 추가

* fix: 서버와 클라이언트 초기 데이터를 동기화하여 Hydration 오류 해결

* chore: react-router-dom 설치

* feat: 영화 상세 정보를 가져오는 fetchMovieDetail 함수 추가

apis 디렉터리를 common 디렉터리 하위로 이동

* 클라이언트에서 환경 변수 사용이 가능하게 webpack 설정 추가

* feat: 영화 상세 정보를 보여주는 모달 컴포넌트 구현

* feat: MoviePage, MovieDetailPage 구현

* feat: 클라이언트 라우팅 설정 및 적용

* feat: 영화 상세 페이지 서버사이드 렌더링 추가

* feat: 클라이언트에서 영화 상세 정보 페칭 로직 구현

- 사용자가 영화나 '자세히 보기' 버튼을 클릭할 때 영화 상세 정보를 가져오는 로직 추가

* fix: Header 배경 이미지 url 수정

* remove: 중복 선언한 apis 디렉터리 삭제

---------

Co-authored-by: Cron <[email protected]>
  • Loading branch information
Yoonkyoungme and woowahan-cron authored Oct 23, 2024
1 parent a13525f commit efb4ec2
Show file tree
Hide file tree
Showing 17 changed files with 289 additions and 51 deletions.
41 changes: 40 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"dependencies": {
"express": "^4.21.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
"@babel/preset-env": "^7.25.4",
Expand Down
12 changes: 5 additions & 7 deletions src/client/App.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from "react";
import { Outlet } from "react-router-dom";

import MovieList from "./components/MovieList";
import Header from "./components/Header";
import Footer from "./components/Footer";

function App({ movies }) {
function App() {
return (
<div id="wrap">
<Header bestMovie={movies[0]} />
<MovieList movies={movies} />
<>
<Outlet />
<Footer />
</div>
</>
);
}

Expand Down
15 changes: 11 additions & 4 deletions src/client/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import React from "react";
import { TMDB_THUMBNAIL_URL } from "../constants";
import { useNavigate } from "react-router-dom";

import logoImage from "@images/logo.png";
import starEmptyImage from "@images/star_empty.png";

export default function Header({ bestMovie }) {
const navigate = useNavigate();

if (!bestMovie || bestMovie.length === 0) {
return null;
}

const { poster_path, vote_average, title } = bestMovie;
const { id, backdrop_path, vote_average, title } = bestMovie;

return (
<header>
<div
className="background-container"
style={{
backgroundImage: `url('${TMDB_THUMBNAIL_URL}/${poster_path}')`,
backgroundImage: `url(https://image.tmdb.org/t/p/w1920_and_h800_multi_faces${backdrop_path})`,
}}
>
<div className="overlay" aria-hidden="true"></div>
Expand All @@ -30,7 +32,12 @@ export default function Header({ bestMovie }) {
<span className="rate-value">{vote_average.toFixed(1)}</span>
</div>
<div className="title">{title}</div>
<button className="primary detail">자세히 보기</button>
<button
className="primary detail"
onClick={() => navigate(`/detail/${id}`)}
>
자세히 보기
</button>
</div>
</div>
</div>
Expand Down
34 changes: 16 additions & 18 deletions src/client/components/MovieList.jsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import React from "react";
import { useNavigate } from "react-router-dom";

import { TMDB_THUMBNAIL_URL } from "../constants";

import starEmptyImage from "@images/star_empty.png";

export default function MovieList({ movies }) {
return (
<div className="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
{movies && (
<ul className="thumbnail-list">
{movies.map((movie) => (
<li key={movie.id}>
<MovieItem movie={movie} />
</li>
))}
</ul>
)}
</section>
</main>
</div>
<>
{movies && (
<ul className="thumbnail-list">
{movies.map((movie) => (
<li key={movie.id}>
<MovieItem movie={movie} />
</li>
))}
</ul>
)}
</>
);
}

function MovieItem({ movie }) {
const { title, poster_path, vote_average } = movie;
const navigate = useNavigate();

const { id, title, poster_path, vote_average } = movie;

return (
<div className="item">
<div className="item" onClick={() => navigate(`/detail/${id}`)}>
<img
className="thumbnail"
src={`${TMDB_THUMBNAIL_URL}/${poster_path}`}
Expand Down
67 changes: 67 additions & 0 deletions src/client/components/MovieModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";

import { fetchMovieDetail } from "../../common/apis/movies";

import { TMDB_THUMBNAIL_URL } from "../constants";
import starEmptyImage from "@images/star_empty.png";

const MovieModal = ({ movie: initialMovie }) => {
const navigate = useNavigate();
const { id: movieId } = useParams();

const [movie, setMovie] = useState(initialMovie || null);

useEffect(() => {
const fetchMovie = async () => {
const data = await fetchMovieDetail(movieId);

setMovie(data);
};

if (!movie) {
fetchMovie();
}
}, [movie, movieId]);

if (!movie) {
return <div>Loading...</div>;
}

const { title, poster_path, genres, vote_average, overview } = movie;

return (
movie && (
<div className="modal-background active" id="modalBackground">
<div className="modal">
<button
className="close-modal"
id="closeModal"
onClick={() => navigate("/")}
>
<img src="/static/images/modal_button_close.png" alt="Close" />
</button>
<div className="modal-container">
<div className="modal-image">
<img src={`${TMDB_THUMBNAIL_URL}${poster_path}`} alt={title} />
</div>
<div className="modal-description">
<h2>{title}</h2>
<p className="category">
{genres.map((genre) => genre.name).join(", ")}
</p>
<p className="rate">
<img src={starEmptyImage} className="star" alt="Rating" />
<span>{vote_average.toFixed(1)}</span>
</p>
<hr />
<p className="detail">{overview}</p>
</div>
</div>
</div>
</div>
)
);
};

export default MovieModal;
11 changes: 8 additions & 3 deletions src/client/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React from "react";

import { hydrateRoot } from "react-dom/client";
import App from "./App";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import routes from "./routes";

const { movies } = window.__INITIAL_DATA__;
const router = createBrowserRouter(routes);

hydrateRoot(document.getElementById("root"), <App movies={movies} />);
hydrateRoot(
document.getElementById("root"),
<RouterProvider router={router} />
);
13 changes: 13 additions & 0 deletions src/client/pages/MovieDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

import MovieModal from "../components/MovieModal";
import MoviePage from "./MoviePage";

export default function MovieDetailPage({ movies, movie }) {
return (
<>
<MoviePage movies={movies} />
<MovieModal movie={movie} />
</>
);
}
20 changes: 20 additions & 0 deletions src/client/pages/MoviePage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

import Header from "../components/Header";
import MovieList from "../components/MovieList";

export default function MoviePage({ movies }) {
return (
<>
<Header bestMovie={movies[0]} />
<div className="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
<MovieList movies={movies} />
</section>
</main>
</div>
</>
);
}
26 changes: 26 additions & 0 deletions src/client/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

import App from "./App";
import MoviePage from "./pages/MoviePage";
import MovieDetailPage from "./pages/MovieDetailPage";

const { movies, movie } = window.__INITIAL_DATA__ || {};

const routes = [
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <MoviePage movies={movies} />,
},
{
path: "detail/:id",
element: <MovieDetailPage movies={movies} movie={movie} />,
},
],
},
];

export default routes;
2 changes: 2 additions & 0 deletions src/server/apis/constants.js → src/common/apis/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const TMDB_MOVIE_LISTS = {
nowPlaying: BASE_URL + "/now_playing?language=ko-KR&page=1",
};

export const TMDB_MOVIE_DETAIL_URL = "https://api.themoviedb.org/3/movie/";

export const FETCH_OPTIONS = {
method: "GET",
headers: {
Expand Down
21 changes: 21 additions & 0 deletions src/common/apis/movies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
TMDB_MOVIE_LISTS,
FETCH_OPTIONS,
TMDB_MOVIE_DETAIL_URL,
} from "./constants.js";

export const fetchMovieItems = async (category = "nowPlaying") => {
const url = TMDB_MOVIE_LISTS[category];
const response = await fetch(url, FETCH_OPTIONS);
const data = await response.json();

return data;
};

export const fetchMovieDetail = async (id) => {
const url = `${TMDB_MOVIE_DETAIL_URL}${id}?language=ko-KR`;
const response = await fetch(url, FETCH_OPTIONS);
const data = await response.json();

return data;
};
9 changes: 0 additions & 9 deletions src/server/apis/movies.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/server/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ app.use("/static", (req, res) => {
});

// 메인 페이지 라우트 (React 앱 렌더링)
app.get("/", movieRouter);
app.use("/", movieRouter);

// 그 외 모든 경로에 대한 404 처리
app.use((req, res) => {
Expand Down
Loading

0 comments on commit efb4ec2

Please sign in to comment.