최적화 - 실습
// webpack.config.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// TODO: 환경변수 NODE_ENV에 따라 development나 production 값을 설정하세요
mode: "development",
entry: {
main: "./src/app.js"
},
output: {
filename: "[name].js",
path: path.resolve("./dist")
},
devServer: {
overlay: true,
stats: "errors-only",
proxy: {
"/api": "http://localhost:8081"
},
hot: true
},
module: {
rules: [
{
test: /\.(scss|css)$/,
use: [
process.env.NODE_ENV === "production"
? MiniCssExtractPlugin.loader // 프로덕션 환경
: "style-loader", // 개발 환경
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|jpg|svg|gif)$/,
loader: "url-loader",
options: {
name: "[name].[ext]?[hash]",
limit: 10000 // 10Kb
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader" // 바벨 로더를 추가한다
}
]
},
plugins: [
new webpack.BannerPlugin({
banner: `빌드 날짜: ${new Date().toLocaleString()}`
}),
new webpack.DefinePlugin({}),
new HtmlWebpackPlugin({
template: "./src/index.html",
templateParameters: {
env: process.env.NODE_ENV === "development" ? "(개발용)" : ""
},
minify:
process.env.NODE_ENV === "production"
? {
collapseWhitespace: true, // 빈칸 제거
removeComments: true // 주석 제거
}
: false,
hash: process.env.NODE_ENV === "production"
}),
new CleanWebpackPlugin(),
...(process.env.NODE_ENV === "production"
? [new MiniCssExtractPlugin({ filename: `[name].css` })]
: [])
]
// TODO: 여기에 최적화 설정을 구성하세요
};
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>검색<%= env %></title>
</head>
<body>
<div id="app">
<header>
<h2 class="container">검색</h2>
</header>
<div class="container">
<form class="FormView">
<input type="text" placeholder="검색어를 입력하세요" autofocus>
<button type="reset" class="btn-reset"></button>
</form>
<div class="content">
<div id="tabs"></div>
<div id="search-keyword"></div>
<div id="search-history"></div>
<div id="search-result"></div>
</div>
</div>
</div>
<!-- TODO: 외부 라이브러리 axios를 로딩할수 있도록 웹팩에서 파일을 복사하세요 -->
<script type="text/javascript" src="axios.min.js"></script>
</body>
</html>
server/index.js
const express = require("express");
const morgan = require("morgan");
const path = require("path");
const app = express();
app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "../dist")));
const port = process.env.PORT || 8081;
const keywords = [
{ keyword: "이탈리아" },
{ keyword: "세프의요리" },
{ keyword: "제철" },
{ keyword: "홈파티" }
];
const search = [
{
id: 1,
name: "[버거] 치즈버거처럼 생긴 새우버거",
image:
"https://images.unsplash.com/photo-1547584370-2cc98b8b8dc8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60"
},
{
id: 2,
name: "[샐러드] 맛있는 색깔의 콥셀러드",
image:
"https://images.unsplash.com/photo-1512621776951-a57141f2eefd?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60"
},
{
id: 3,
name: "[피자] 썸네일 주소가 잘못된 상품",
image: "http://foo.bar/image.jpg"
}
];
let history = [
{ keyword: "검색기록2", date: "12.03" },
{ keyword: "검색기록1", date: "12.02" },
{ keyword: "검색기록0", date: "12.01" }
];
app.get("/api/keywords", (req, res) => {
// res.header("Access-Control-Allow-Origin", "*");
res.json(keywords);
});
app.get("/api/history", (req, res) => {
res.json(history);
});
app.post("/api/history", (req, res) => {
keyword = (req.query.keyword || "").trim();
if (!keyword) return;
history.filter(item => item.keyword !== keyword);
history = [
{ keyword, date: "12.31" },
...history.filter(item => item.keyword !== keyword)
];
res.json(history);
});
app.delete("/api/history", (req, res) => {
const keyword = (req.query.keyword || "").trim();
history = history.filter(item => item.keyword !== keyword);
res.json(history);
});
app.get("/api/search", (req, res) => {
res.json(search);
});
// app.get("*", (req, res) => {
// res.sendFile(path.join(__dirname, "../dist/index.html"));
// });
app.listen(port, () => {
console.log(`서버가 구동되었습니다. localhost:${port}`);
});
app.use(express.static(path.join(__dirname, "../dist")));
위 코드를 간단히 설명드리자면 ../dist
폴더, 즉 우리가 웹팩으로 빌드할 때 결과물이 담기는 폴더를 정적 파일로 제공하는 코드이다.
그래서 이전에는 API만 제공했던 서버였는데 이제는 빌드한 프론트엔드 코드를 정적 파일로 제공하는 웹 서버 역할도 한다.
npm run build
dist
폴더에 정적 결과물이 내보내진다.
해당 결과물을 server
폴더에서 서버로 돌려보자.
npm start
지금까지는 8080
포트에서 웹팩 개발서버로 접속해서 확인했는데, 이제는 프론트엔드 코드를 웹팩에다 빌드하고 그 결과물을 노드서버가 제공해준다.
그래서 8081
로 접속해서 확인을 했고,
axios
는 우리가 해결해야될 문제이다.
아직은 없기 때문에 404 에러
로 응답된거고 나머지 main.js
, main.css
는 제대로 요청 후 응답되었다.
이미지도 제대로 로드되었다.
내 풀이
아래 코드들 추가했다.
// webpack.config.js
const mode = process.env.NODE_ENV || "development";
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode,
plugins: [
new CopyPlugin({
patterns: [
{
from: "./node_modules/axios/dist/axios.min.js",
to: "./axios.min.js"
}
]
})
],
optimization: {
minimizer:
mode === "production"
? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true // 콘솔 로그를 제거한다.
}
}
})
]
: []
},
externals: {
axios: "axios"
}
};
아 externals
설정을 안해줬구나.
이거 빼먹지 말자~!
다시 수정~!!!
위와 같이 모듈 "axios"
를 externals
에 설정하고 전역 변수로 axios
라고 설정해준다.
그럼 axios
는 빌드할 때 빠질 것이다.
대신 이 때문에 CopyWebpackPlugin을 쓰는 것이다. CopyWebpackPlugin은 써놓고 externals는 생각을 못했다. 주의하자.
ls -lh dist
빌드 전에 dist
폴더의 용량을 보자.
그리고 dist/index.html
파일을 확인해보면 axios
모듈을 먼저 로드 후에 main.js
파일을 로드하고 있는 것을 확인할 수 있다.