SPA는 Single Page Application(싱글 페이지 어플리케이션)의 약자입니다. 말 그대로, 페이지가 1개인 어플리케이션이란 뜻입니다.
간단한 어플리케이션을 SPA처럼 동작하게 만들어볼거에요.
tab
버튼을 누르면 해당 탭에 맞는 화면이 나타난다.
tab
버튼을 누르면 url
이 변경된다.
webpack
과 webpack.dev.server
를 활용해 개발 서버를 구동할 겁니당. 시작전 설정파일은 다음과 같습니다.
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: "./index.js",
resolve: {
extensions: [".js"],
},
devServer: {
port: 9000,
},
devtool: "source-map",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./index.html",
}),
],
};
마크업은 다음과 같습니다.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body
style="
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
"
>
<nav style="height: 200px">
<button class="home" data-path="/">home</button>
<button class="mypage" data-path="/mypage">mypage</button>
<button class="login" data-path="/login">login</button>
</nav>
<div
class="main"
style="background-color: blue; height: 500px; width: 500px"
></div>
</body>
</html>
저는 천천히 나아가는걸 좋아합니다. 그렇기 때문에 우선 탭버튼을 누르면 url이 변경되는 어플리케이션을 만들어볼게요. pushState
를 활용해보겠습니다. 그게뭔데 ?
document.querySelector("nav").addEventListener("click", (e) => {
const {
target: {
className,
dataset: { path },
},
} = e;
if (path) {
history.pushState(null, "의미없음그냥아무거나입력해봄", path);
}
});
이번에는 div.main
에 url에 맞는 텍스트를 넣어보도록 할게요.
pushState 메소드는 이벤트를 발생시키지 않는다. popState만이 이벤트를 발생시킨다.
pushState
이벤트를 잡을 수 있다면, 이벤트가 발생했을 때의 window.location.pathname
값을 기준으로 desc
객체에서 데이터를 뽑아와 어플리케이션에 텍스트를 렌더링하면 될테지만 pushState
는 이벤트를 발생시키지 않습니다.
따라서 pushState
를 호출할 때 화면을 그려놓고 url만 변경시켜주도록 하겠습니다.
const desc = {
"/": "홈 화면입니다.",
"/mypage": "마이페이지 화면입니다.",
"/login": "로그인 화면입니다.",
};
const render = (desc) => {
document.querySelector(".main").innerHTML = desc;
};
document.querySelector("nav").addEventListener("click", (e) => {
const {
target: {
dataset: { path },
},
} = e;
if (path) {
render(desc[path]); // 호출 순서에 집중하지마세요
history.pushState(null, "dsafasdf", path); // 호출 순서에 집중하지마세요
}
});
render(desc[window.location.pathname]); // 처음 주소에 맞게 렌더링한다.
위 같이 작성하면 다음과 같이 잘 될거에요. 크크크
하지만 이것은 정상적인 어플리케이션일까요? 아닙니다.
url 입력창에 주소를 입력후 접근하면 https://앱주소/
에는 정상적으로 동작하지만 https://앱주소/login
혹은 https://앱주소/mypage
에는 404 페이지를 보여주게 될겁니다.
또 https://앱주소/login
|| https://앱주소/mypage
위치로 탭 버튼을 통해 이동 후 새로고침하게되면 마찬가지로 404 페이지를 보여주게 될겁니다.
간단히 말해 리소스가 준비되지 않은 라우터에 접근하고 있으니 제대로 수행이 안되는 겁니다 !! 이러한 이슈를 해결해야겠죠?
다만, 브라우저의 새로고침 버튼을 클릭하면 https://localhost:8080/about와 같은 요청이 서버로 전달되는데. 이때 서버는 URL에 따라 해당 리소스를 HTML로 클라이언트에 응답해주어야 최초 화면이 표시될 수 있습니다.
webpack dev server
를 사용하는 경우, 너무 너무 간단합니다.
devServer: {
historyApiFallback: true,
},
이 스텝3 과정을 통해 리액트 앱을 로컬에서 실행시켰을때 잘못된 url에 접근해도 리소스를 받을 수 있음을 깨달아야 합니다 ! (ㅋㅋㅋ 저는 8개월간 몰랏음.. 미친.. 개바보 그냥)
When using the HTML5 History API, the index.html page will likely have to be served in place of any 404 responses. Enable devServer.historyApiFallback by setting it to true:
하지만 webpack dev server
로 구동되지 않는 프로덕션에서는 이 이슈를 해결할 수 있을까요? 당연히 안될겁니다. 그렇기 때문에 다른 방식을 사용하여 해결해야합니다.
CRA
는 어떻게 배포하라는지 먼저 알아볼까요 ? 링크링크
netflify
라는 플랫폼을 통해 어떤 path
에 접근하더라도 우리 어플리케이션을 서빙하게끔 배포해볼게요 !!
아래 사진에서 github
를 클릭해주시고
그 다음화면에서 내 레포지토리와 베이스 디렉토리를 설정할 수 있어요! (publish directory 또한 설정 가능) 지금의 경우라면 웹팩 빌드의 결과물이 담기는 폴더를 지정해주면 원할히 배포가 진행될 것 같아요
public
폴더를 프로젝트 폴더에 만들고, 이 곳에다 _redirects
라는 파일을 생성합니다.//모든 path에서도 index.html을 서빙할 수 있도록 파일에 내용작성합니다.
/* /index.html 200
copy-webpack-plugin
을 통해 public의 파일들을 그대로 빌드 폴더에 옮길 수 있게합니다. (설정은 다음과 같이 합니다)const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
resolve: {
extensions: [".js"],
},
devServer: {
port: 9000,
historyApiFallback: true,
},
devtool: "source-map",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/",
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./index.html",
}),
new CopyPlugin({
patterns: [{ from: "public", to: "" }],
}),
],
};
플러그인을 통해 빌드를 다시 수행하면 다음과 같이 빌드폴더에 _redirects 파일이 복사되게 됩니다.
결과물은 다음과 같습니다.
잘못된 path
에 라우팅되어도 리소스를 서빙할 수 있는 방법을 개발과 프로덕션 단에서 알아보았습니다! 무지성으로 리액트가 해주는 걸 냠냠 받아먹던 나에서 벗어날 준비가 되었을지도..????!?!