Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[서버 사이드 렌더링 - 1단계] 해리(최현웅) 미션 제출합니다. #19

Merged
merged 11 commits into from
Oct 11, 2024
Merged
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"watch": ["src/server", "src/client"],
"ext": "js jsx json",
"ignore": ["dist", "node_modules"],
"exec": "npm run build:client && npm run build:server && node dist/server/server.js"
"exec": "npm run build:client && npm run build:server && node dist/server.js"
}
1,642 changes: 795 additions & 847 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"description": "",
"main": "index.js",
"scripts": {
"build:client": "webpack --config webpack.client.config.js",
"build:client": "rm -rf dist && webpack --config webpack.client.config.js",
"build:server": "webpack --config webpack.server.config.js",
"start": "concurrently \"npm run build:client -- --watch\" \"npm run dev:server\"",
"start": "npm run build:client && npm run build:server && node dist/server.js",
"dev": "nodemon",
"test": "echo \"Error: no test specified\" && exit 1"
},
Expand All @@ -26,8 +26,10 @@
"css-loader": "^7.1.2",
"dotenv": "^16.4.5",
"html-webpack-plugin": "^5.6.0",
"ignore-loader": "^0.1.2",
"nodemon": "^3.1.7",
"style-loader": "^4.0.0",
"webpack-cli": "^5.1.4"
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
}
}
4 changes: 2 additions & 2 deletions public/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ button.primary {
border-radius: 4px;
}

#wrap {
#root {
min-width: 1440px;
background-color: var(--color-bluegray-100);
}

#wrap h2 {
#root h2 {
font-size: 1.4rem;
font-weight: bold;
margin-bottom: 32px;
Expand Down
17 changes: 12 additions & 5 deletions src/client/App.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import React from "react";
import Home from "./components/Home";

function App() {
import Header from "./components/Header";
import MovieList from "./components/MovieList";
import Footer from "./components/Footer";

function App({ movies }) {
const bestMovie = movies[0];

return (
<div>
<Home />
</div>
<>
<Header bestMovie={bestMovie} />
<MovieList movies={movies} />
<Footer />
</>
);
}

Expand Down
13 changes: 13 additions & 0 deletions src/client/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import woowaCourseImage from "@images/woowacourse_logo.png";

export default function Footer() {
return (
<footer className="footer">
<p>&copy; 우아한테크코스 All Rights Reserved.</p>
<p>
<img src={woowaCourseImage} width="180" />
</p>
</footer>
);
}
40 changes: 40 additions & 0 deletions src/client/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";

import logoImage from "@images/logo.png";
import starEmptyImage from "@images/star_empty.png";
import { TMDB_BANNER_URL } from "../constants/api";

export default function Header({ bestMovie }) {
return (
<header>
<div
className="background-container"
style={{
backgroundImage: `url(${TMDB_BANNER_URL}${bestMovie.backdrop_path})`,
}}
>
<div className="overlay" aria-hidden="true"></div>
<div className="top-rated-container">
<h1 className="logo">
<img src={logoImage} alt="MovieList" />
</h1>
<div className="top-rated-movie">
<div className="rate">
<img src={starEmptyImage} className="star" />
<span className="rate-value">
{bestMovie.vote_average.toFixed(1)}
</span>
</div>
<div className="title">{bestMovie.title}</div>
<button
className="primary detail"
onClick={() => alert("Harry Mission Completed")}
>
자세히 보기
</button>
</div>
</div>
</div>
</header>
);
}
7 changes: 0 additions & 7 deletions src/client/components/Home.jsx

This file was deleted.

48 changes: 48 additions & 0 deletions src/client/components/MovieList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";

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

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

export default function MovieList({ movies }) {
return (
<div className="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
<ul className="thumbnail-list">
{movies.map(({ id, title, poster_path, vote_average }) => {
const thumbnailUrl = `${TMDB_THUMBNAIL_URL}${poster_path}`;
const roundedRate = vote_average.toFixed(1);

return (
<li key={id}>
<a href={`/detail/${id}`}>
<div className="item">
<img
className="thumbnail"
src={thumbnailUrl}
alt={title}
/>
<div className="item-desc">
<p className="rate">
<img
src={starEmptyImage}
className="star"
alt="star"
/>
<span>{roundedRate}</span>
</p>
<strong>{title}</strong>
</div>
</div>
</a>
</li>
);
})}
</ul>
</section>
</main>
</div>
);
}
5 changes: 5 additions & 0 deletions src/client/constants/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const BASE_URL = "https://api.themoviedb.org/3/movie";
export const TMDB_THUMBNAIL_URL =
"https://media.themoviedb.org/t/p/w440_and_h660_face";
export const TMDB_BANNER_URL =
"https://image.tmdb.org/t/p/w1920_and_h800_multi_faces";
4 changes: 3 additions & 1 deletion src/client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

hydrateRoot(document.getElementById("root"), <App />);
const { movies } = window.__INITIAL_DATA__;

hydrateRoot(document.getElementById("root"), <App movies={movies} />);
10 changes: 10 additions & 0 deletions src/server/apis/movie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FETCH_OPTIONS, TMDB_MOVIE_LISTS } from "../constants/api.js";

export const fetchMovies = async (movieType) => {
const fetchEndPoint = TMDB_MOVIE_LISTS[movieType];

const response = await fetch(fetchEndPoint, FETCH_OPTIONS);
const allMovieData = await response.json();

return allMovieData.results;
};
18 changes: 18 additions & 0 deletions src/server/constants/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const BASE_URL = "https://api.themoviedb.org/3/movie";

export const TMDB_BANNER_URL =
"https://image.tmdb.org/t/p/w1920_and_h800_multi_faces";
export const TMDB_MOVIE_LISTS = {
nowPlaying: BASE_URL + "/now_playing?language=ko-KR&page=1",
popular: BASE_URL + "/popular?language=ko-KR&page=1",
topRated: BASE_URL + "/top_rated?language=ko-KR&page=1",
upcoming: BASE_URL + "/upcoming?language=ko-KR&page=1",
};

export const FETCH_OPTIONS = {
method: "GET",
headers: {
accept: "application/json",
Authorization: "Bearer " + process.env.TMDB_TOKEN,
},
};
12 changes: 12 additions & 0 deletions src/server/constants/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { switchMoviePaths } from "../utils/object";

export const MOVIE_PAGE_PATH = {
home: "/",
nowPlaying: "/now-playing",
popular: "/popular",
topRated: "/top-rated",
upcoming: "/upcoming",
detail: "/detail/:movieId",
};

export const MOVIE_TYPE_KEY = switchMoviePaths(MOVIE_PAGE_PATH, "nowPlaying");
21 changes: 14 additions & 7 deletions src/server/main.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import "./config.js";
import express from "express";
import path from "path";
import { fileURLToPath } from "url";

import movieRouter from "./routes/index.js";

const app = express();
const PORT = 3000;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 정적 파일 제공
app.use("/static", express.static(path.join(__dirname)));

// 존재하지 않는 정적 파일에 대한 404 처리
app.use("/static", (req, res) => {
res.status(404).send("Resource not found");
});

app.use("/assets", express.static(path.join(__dirname, "../../public")));
// 메인 페이지 라우트 (React 앱 렌더링)
app.get("/", movieRouter);

app.use("/", movieRouter);
// 그 외 모든 경로에 대한 404 처리
app.use((req, res) => {
res.status(404).send("Page not found");
});

// Start server
// 서버 시작
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
45 changes: 23 additions & 22 deletions src/server/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { Router } from "express";
import App from "../../client/App";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

import React from "react";
import { Router } from "express";
import { renderToString } from "react-dom/server";
import App from "../../client/App";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { MOVIE_PAGE_PATH, MOVIE_TYPE_KEY } from "../constants/path";
import { fetchMovies } from "../apis/movie";

const router = Router();

router.get("/", (_, res) => {
const templatePath = path.join(__dirname, "../../../views", "index.html");
const renderedApp = renderToString(<App />);
router.use(MOVIE_PAGE_PATH.home, async (req, res) => {
const movies = await fetchMovies(MOVIE_TYPE_KEY[req.path]);

const template = fs.readFileSync(templatePath, "utf-8");
// const initData = template.replace(
// "<!--${INIT_DATA_AREA}-->",
// /*html*/ `
// <script>
// window.__INITIAL_DATA__ = {
// movies: ${JSON.stringify(popularMovies)}
// }
// </script>
// `
// );
const renderedHTML = template.replace("<!--${MOVIE_ITEMS_PLACEHOLDER}-->", renderedApp);
const renderedApp = renderToString(<App movies={movies} />);
const templatePath = path.resolve(__dirname, "index.html");
const template = fs.readFileSync(templatePath, "utf8");

res.send(renderedHTML);
res.send(
template
.replace('<div id="root"></div>', `<div id="root">${renderedApp}</div>`)
.replace(
"<!--${INIT_DATA_AREA}-->",
/*html*/ `
<script>
window.__INITIAL_DATA__ = {
movies: ${JSON.stringify(movies)}
}
</script>
`
)
);
});

export default router;
13 changes: 13 additions & 0 deletions src/server/utils/object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const switchMoviePaths = (originalPaths, rootPath) => {
const switchedPaths = {};

Object.entries(originalPaths).forEach(([key, value]) => {
if (value === "/") {
switchedPaths[value] = rootPath;
} else {
switchedPaths[value] = key;
}
});

return switchedPaths;
};
42 changes: 2 additions & 40 deletions views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,11 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../assets/styles/reset.css" />
<link rel="stylesheet" href="../assets/styles/main.css" />
<link rel="stylesheet" href="../assets/styles/modal.css" />
<link rel="stylesheet" href="../assets/styles/tab.css" />
<link rel="stylesheet" href="../assets/styles/thumbnail.css" />
<script src="./"></script>
<title>영화 리뷰</title>
<link rel="stylesheet" href="./static/styles/index.css" />
</head>
<body>
<div id="wrap">
<header>
<div class="background-container" style="background-image: url('${background-container}')">
<div class="overlay" aria-hidden="true"></div>
<div class="top-rated-container">
<h1 class="logo"><img src="../assets/images/logo.png" alt="MovieList" /></h1>
<div class="top-rated-movie">
<div class="rate">
<img src="../assets/images/star_empty.png" class="star" />
<span class="rate-value">${bestMovie.rate}</span>
</div>
<div class="title">${bestMovie.title}</div>
<button class="primary detail">자세히 보기</button>
</div>
</div>
</div>
</header>
<div class="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
<ul class="thumbnail-list">
<!--${MOVIE_ITEMS_PLACEHOLDER}-->
</ul>
</section>
</main>
</div>

<footer class="footer">
<p>&copy; 우아한테크코스 All Rights Reserved.</p>
<p><img src="../assets/images/woowacourse_logo.png" width="180" /></p>
</footer>
</div>
<!--${MODAL_AREA}-->
<div id="root"></div>
</body>
<!--${INIT_DATA_AREA}-->
</html>
Loading