[우아한테크코스 #7] Browser History API를 통해 SPA처럼 라우팅을 적용해보자

SPA는 Single Page Application(싱글 페이지 어플리케이션)의 약자입니다. 말 그대로, 페이지가 1개인 어플리케이션이란 뜻입니다.

간단한 어플리케이션을 SPA처럼 동작하게 만들어볼거에요.

Test Project의 기능 요구 사항

  • tab 버튼을 누르면 해당 탭에 맞는 화면이 나타난다.

  • tab 버튼을 누르면 url이 변경된다.

Test Project의 개발 환경

webpackwebpack.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>

STEP 1. path만 변경해보기

저는 천천히 나아가는걸 좋아합니다. 그렇기 때문에 우선 탭버튼을 누르면 url이 변경되는 어플리케이션을 만들어볼게요. pushState를 활용해보겠습니다. 그게뭔데 ?

document.querySelector("nav").addEventListener("click", (e) => {
  const {
    target: {
      className,
      dataset: { path },
    },
  } = e;
  if (path) {
    history.pushState(null, "의미없음그냥아무거나입력해봄", path);
  }
});

STEP 2. 해당하는 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로 클라이언트에 응답해주어야 최초 화면이 표시될 수 있습니다.

STEP 3. 개발 단에서 이슈 해결하기

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:

STEP 4. 프로덕션에서 이슈 해결하기

하지만 webpack dev server 로 구동되지 않는 프로덕션에서는 이 이슈를 해결할 수 있을까요? 당연히 안될겁니다. 그렇기 때문에 다른 방식을 사용하여 해결해야합니다.

CRA는 어떻게 배포하라는지 먼저 알아볼까요 ? 링크링크

STEP 4 - bonus. Netflify를 사용하여 배포하기

netflify라는 플랫폼을 통해 어떤 path에 접근하더라도 우리 어플리케이션을 서빙하게끔 배포해볼게요 !!

1. release 브랜치를 만들어 webpack 빌드 폴더를 원격 레포지토리에 올려주세요

저는 개발 브랜치인 master 브랜치에는 빌드 폴더를 ignore 해주고, release 브랜치에만 빌드 폴더를 올리도록 해주었어요.

2. netflify에 내 레포지토리/release 브랜치를 메인으로 하는 어플리케이션을 등록해주세요

아래 사진에서 github를 클릭해주시고

그 다음화면에서 내 레포지토리와 베이스 디렉토리를 설정할 수 있어요! (publish directory 또한 설정 가능) 지금의 경우라면 웹팩 빌드의 결과물이 담기는 폴더를 지정해주면 원할히 배포가 진행될 것 같아요

3. 어플리케이션이 등록되면, 자동으로 배포를 수행합니다.

4. 하지만 지금 배포 서버에는 서빙작업이 안되어 있기 때문에 이를 수행한 후 재 배포해야합니다.

  • 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 파일이 복사되게 됩니다.

5. 마지막으로 commit & push를 해 release 브랜치를 최신화하고 release 브랜치 내용을 재 배포합니다.

결과물은 다음과 같습니다.

정리

잘못된 path에 라우팅되어도 리소스를 서빙할 수 있는 방법을 개발과 프로덕션 단에서 알아보았습니다! 무지성으로 리액트가 해주는 걸 냠냠 받아먹던 나에서 벗어날 준비가 되었을지도..????!?!

Ref

좋은자료 같아서 모아둠

좋은 웹페이지 즐겨찾기