본문 바로가기

Web

Vanilla Javascript 라우터 구현

1. 컨셉

하나의 라우터 엘리먼트를 생성한 뒤 라우터 엘리먼트의 자식노드로 정해둔 컴포넌트를 추가한다.

경로이동시 히스토리 변수에 해당 경로를 추가하여, 추후 이전 경로에 사용한다.

 

 

2. 구현

 (1)  webpack.config.js

  devServer: {
    contentBase: path.join(__dirname, "public"),
    historyApiFallback: true,
    port: 8080,
  },

기존의 webapck.config에서 historyApiFallBack을 추가하여 history기능이 가능하도록 하였습니다.

 

 

(2)  utils/selecter.ts

// 엘리먼트 검색함수
export function $(key, elemnent = null) {
  if (!key) {
    return false;
  }
  if (!elemnent) {
    return document.querySelector(key);
  } else {
    return elemnent.querySelector(key);
  }
}

// 엘리먼트 삽입후 컴포넌트 생성함수
export function createComponent(type, element, name = null, component) {
  const dom = document.createElement(type);
  if (!!name) {
    dom.classList.add(name);
  }
  element.appendChild(dom);
  new component(dom);
}

// 모든 자식노드 삭제함수
export function removeAllChild(parent: any) {
  while (parent.hasChildNodes()) {
    parent.removeChild(parent.firstChild);
  }
}

 

 

(2)  router/index.ts

import {$, createComponent, removeAllChild} from "../utils/selector";
import ItemAppender from "../components/ItemAppender";
import Item from "../components/Item";
import ItemFilter from "../components/ItemFilter";
import Count from "../components/Count";
let history = [];

// 이전경로 호출함수
export const getHistory = () => {
  if (history.length === 1) {
    return false;
  }
  history = history.slice(0, -1);
  return history[history.length - 1];
};

// 이전경로로 이동후 해당경로가 히스토리에 다시 쌓이기 때문에
// 같은 경로만 계속 바라보게 되는 문제를 해결하기 위해
// 현재경로를 pop시키는 함수입니다.
export const syncHistory = () => {
  history.pop();
};

// 이전경로 추가함수
export const addHistory = value => {
  history.push(value);
};

export function subRoute(path) {
  const {$el} = this;
  removeAllChild($el);

  switch (path) {
    case "/item": {
      createComponent("header", $el, "append", ItemAppender);
      createComponent("main", $el, "item", Item);
      createComponent("footer", $el, "filter", ItemFilter);
      break;
    }
    case "/count": {
      createComponent("div", $el, "count", Count);
      break;
    }
    default: {
      break;
    }
  }

  addHistory(path);
}

subRoute함수가 실행될때 마다 모든 자식노드를 삭제후 시작합니다.

그 후 경로에 따라 앨리먼트를 삽입하고 그 앨리먼트 이용해서 컴포넌트를 생성합니다.

라우팅시 각 경로는 addHistory에 의해서 History에 추가되어 이전 경로에 사용됩니다.

 

 

(3)  components/Header.ts

import Component from "../core/Component";
import {getHistory, syncHistory} from "../router";

export default class Header extends Component {
  template() {
    return `
        <button class="item">Item</button>
        <button class="count">Count</button>
        <button class="back">이전</button>
    `;
  }

  created() {
    const {subRoute} = this.props;
    subRoute("/item");
  }

  setEvent() {
    const {subRoute} = this.props;
    this.addEvent("click", ".item", () => {
      subRoute("/item");
    });

    this.addEvent("click", ".count", () => {
      subRoute("/count");
    });

    this.addEvent("click", ".back", () => {
      const path = getHistory();
      if (path) {
        subRoute(path);
      }
      syncHistory();
    });
  }
}

created 라이프사이클을 추가하여 초기 실행시 item경로가 호출되게 하였으며,

각 메뉴 클릭시 해당경로로 라우팅시킵니다.

이전버튼 클릭시 이전경로를 불러와서 라우팅시키며, 위에서 설명 했던것과 같이 히스토리 경로를 sync시켜 줬습니다.

 

 

 

(4)  Page/Root.ts

import Component from "../core/Component";
import Header from "../components/Header";
import {subRoute} from "../router";
import {$} from "../utils/selector";

export default class Root extends Component {
  template() {
    return `
      <header class="header"></header>
      <div class="sub-route"></div>
    `;
  }

  mounted() {
    const header = $(".header");
    this.$el = $(".sub-route");

    new Header(header, {
      subRoute: subRoute.bind(this),
    });
  }
}

Root페이지를 따로 만들어서 URL에 따른 다른 페이지를 구현하였습니다.

subRoute함수는 bind를 걸어 this 호출 스코프를 변경 하였습니다.

 

 

(5)  Page/NotFound.ts

import Component from "../core/Component";
export default class NotFound extends Component {
  template() {
    return `
      <div>NotFound</div>
    `;
  }
}

지정하지 않은 URL로 접속하였을경우 나오는 페이지입니다.

 

 

(6)  App.ts

import Component from "./core/Component";
import {$} from "./utils/selector";
import Root from "./page/Root";
import NotFound from "./page/NotFound";

export default class App extends Component {
  template() {
    return `
      <div class="router-dom"></div>
    `;
  }

  mounted() {
    const {pathname} = window.location;
    const router = $(".router-dom");
    switch (pathname) {
      case "/": {
        new Root(router);
        break;
      }
      default: {
        new NotFound(router);
        break;
      }
    }
  }
}

각 URL에 따른 페이지 라우팅입니다.

 

3.  전체코드

https://github.com/yg1110/Component/tree/router

 

GitHub - yg1110/Component

Contribute to yg1110/Component development by creating an account on GitHub.

github.com

 

'Web' 카테고리의 다른 글

Scope  (0) 2022.03.19
웹페이지 동작 원리 & 브라우저 랜더링 과정  (0) 2022.03.18
Vanilla Javascript Component Core 구현  (0) 2021.09.26
WebPack을 이용한 Typescript 번들링  (0) 2021.08.09
Class  (0) 2020.05.01