기록하는 개발자

[vanillaJs] 프레임워크없이 SinglePageApplication 만들기-3.동적라우팅 본문

Web/Javascript

[vanillaJs] 프레임워크없이 SinglePageApplication 만들기-3.동적라우팅

밍맹030 2022. 9. 3. 15:31
728x90

↓이전 글

https://mingmeng030.tistory.com/244

[vanillaJs] 프레임워크없이 SinglePageApplication 만들기-1. 라우팅 설정하기

폴더 구조 1. frontend 폴더 내부에 index.html 파일 생성 후 아래와 같이 구성 // index.html <!DOCTYPE html> SPA with no framework Home Posts Settings - url 이동에 사용하게 될 a 링크들에 대하여 data-li..

mingmeng030.tistory.com

https://mingmeng030.tistory.com/245

[vanillaJs] 프레임워크없이 SinglePageApplication 만들기-2. 라우팅 페이지 구성

https://mingmeng030.tistory.com/244 SPA with no framework Home Posts Settings - url 이동에 사용하게 될 a 링크들에 대하여 data-li.." data-og-host="mingmeng030.tistory.com" data-og-source-url="https:/..

mingmeng030.tistory.com


이전 글에서는 라우팅까지 적용하였다.
이제 동적라우팅으로 /post/:id로 이동할 수 있게 만들어 보자.

//index.js
import Home from "./views/Home.js";
import Post from "./views/Post.js";
import ViewPost from "./views/ViewPost.js";
import Settings from "./views/Settings.js";

const pathToRegex = (path) =>
  new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");

const getParams = (match) => {
  const values = match.result.slice(1);
  const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(
    (result) => result[1]
  );
  return Object.fromEntries(
    keys.map((key, i) => {
      return [key, values[i]];
    })
  );
};

const navigateTo = (url) => {
  history.pushState(null, null, url);
  router();
};
const router = async () => {
  const routes = [
    { path: "/", view: Home },
    { path: "/posts", view: Post },
    { path: "/posts/:id", view: ViewPost },
    { path: "/settings", view: Settings },
  ];

  const potentialMatches = routes.map((route) => {
    return {
      route: route,
      result: location.pathname.match(pathToRegex(route.path)),
    };
  });

  // potentialMatches의 결과 중 null이 아닌 것을 뽑아 match 변수에 저장
  let match = potentialMatches.find(
    (potentialMatch) => potentialMatch.result !== null
  );

  // match가 true인 것이 존재하지 않는 경우(유효하지 않은 경로)
  if (!match) {
    match = {
      route: routes[0],
      result: [location.pathname],
    };
  }
  const view = new match.route.view(getParams(match));

  document.querySelector("#app").innerHTML = await view.getHtml();
};

window.addEventListener("popstate", router);

document.addEventListener("DOMContentLoaded", () => {
  document.body.addEventListener("click", (e) => {
    if (e.target.matches("[data-link]")) {
      e.preventDefault();
      navigateTo(e.target.href);
    }
  });
  router();
});

1. path 정규표현식 생성

const pathToRegex = (path) =>
  new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");


1) "^" → ^로 시작한다.
2) path.replace(/\//g, "\\/") → /\//g를 "\\/"로 대체한다.
\ / → /가 온다.
\ g → 매칭되는 것을 모두 다 찾는다.

→ path에서 "/"를 모두 \/로 대체한다.

3) replace(/:\w+/g, "(.+)") + "$") → /:\w+/g를 "(.+)")로 대체한다.
: → :로 시작한다.
\w+ → \w(영문자, 언더스코어)로 이루어진 문자열이 한개 이상(+) 있다.
\ g → 매칭되는걸 모두 다 찾는다.

→ path에서 ":"로 시작하는 문자열을 모두 (.+)로 대체한다.

4) $
$ → 문자열의 끝을 의미

정규식표현 정리 글 참고( https://mingmeng030.tistory.com/246 )

[vanillaJs] 정규표현식을 예시와 함께 알아보자!

Regular Expression: Regex 정규 표현식, 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴이다. 1. 정규표현식 형식 / 패턴 / 플래그  - 슬래시(/)와 슬래시(/) 사이에는 매칭시킬 패턴을 써준

mingmeng030.tistory.com

console.log("/posts/:id");
console.log(pathToRegex("/posts/:id"));

↓위 코드 실행 결과이다.


2. potentialMatches 배열

  //기존 방식
  const potentialMatches = routes.map((route) => {
    return {
      route: route,
      isMatch: location.pathname === route.path,
    };
//정규 표현식 사용
const potentialMatches = routes.map((route) => {
    return {
      route: route,
      result: location.pathname.match(pathToRegex(route.path)),
    };
  });

 

 지난 글까지는 isMatch에 현재 location.pathname과 일치하면 true나 false를 넣어주는 방식이었다.

이번 글부터는 위에서 작성한 정규표현식을 사용하여 검사한다.

 

  console.log(potentialMatches);

 ↑ 위 코드 실행 결과

 

  → route 배열 중 현재의 location.pathname과 일치하는 요소는 result에 match 함수의 결과인 정보가 들어있지만

   그렇지 않은 요소들은 null이 들어가있다.

 

3. match 변수

 //기존 방식 
// potentialMatches의 결과 중 true인 것을 뽑아 match 변수에 저장 
  let match = potentialMatches.find(
    (potentialMatches) => potentialMatches.isMatch
  );
// 정규표현식 사용 후
// potentialMatches의 결과 중 null이 아닌 것을 뽑아 match 변수에 저장
  let match = potentialMatches.find(
    (potentialMatch) => potentialMatch.result !== null
  );
  console.log(match);

 

↑ 위 코드 실행 결과

  

→ 변수 match는배열 potentialMatches의 요소 중 현재의 location.pathname에 해당하는 요소를 저장한다.

 

4. 예외 처리 조건문

//기존 방식
if (!match) {
    match = {
      route: routes[0],
      isMatch: true,
    };
  }
  // 정규표현식 사용 후
  if (!match) {
    match = {
      route: routes[0],
      result: [location.pathname],
    };
  }

 

route : route 배열의 첫 번째 요소를 현재 route로 지정한다 → "/"가 location.pathname에 해당된다.
result : 현재 location.pathname을 result에 저장 실제 사용자가 입력한 pathname을 result에 저장한다.

5. getParams 함수

const getParams = (match) => {
  const values = match.result.slice(1);
  const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(
    (result) => result[1]
  );
  return Object.fromEntries(
    keys.map((key, i) => {
      return [key, values[i]];
    })
  );
};
  console.log(match);
  console.log(values);
  console.log(keys);


↑ 위 코드 실행 결과

/post/5 에 접속한 경우


1) values : 변수 match 내의 result 객체에 저장된 것 중 index가 1인 요소를 가져온다. → "5"

2) keys

 const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(
    (result) => result[1]
  );



- Array.from(match. route.path.matchAll(/:(\w+)/g))
: match 변수 내 result 객체에 저장된 path 문자열에서 ":"로 시작하고 한 글자 이상의 문자로 이루어진 것을 모두 찾아 배열로 만든다.

Array.from(match. route.path.matchAll(/:(\w+)/g))


↑ 위 코드 실행 결과


- Array.from(match. route.path.matchAll(/:(\w+)/g)).map((result)=>result[1]);
: 위 정규표현식을 통한 결과를 반환한 배열 중 index가 1인 요소를 가져온다. → "id"


3) return문

  return Object.fromEntries(
    keys.map((key, i) => {
      return [key, values[i]];
    })
  );


- [ key, values[i]를 반환한다. → [ "id", "5" ]
→ 현재 동적라우팅에 대해 "/posts/:id"의 id 값이 5임을 저장하는 배열이다.


6. 변수 view

  const view = new match.route.view(getParams(match));


- 현재 경로에 해당되는 view에 함수 getParams의 반환값을 인자로 전달한다.

7. AbstractView 및 기존에 작성한 view에 매개변수 추가

//AbstractView.js
export default class {
  constructor(params) {
    this.params = params;
  }

  setTitle(title) {
    document.title = title;
  }
  async getHtml() {
    return "";
  }
}

// Home.js
// Settings.js, Posts.js도 동일하게 적용
import AbstractView from "./AbstractView.js";

export default class extends AbstractView {
  constructor(params) {
    super(params);
    this.setTitle("Home");
  }
  async getHtml() {
    return `
      <h1>This is home page</h1>
      <p>express yourself</p>
      <p>
        <a href="/posts" data-link>View recent posts</a>
      </p>
    `;
  }
}

8. ViewPost.js 작성

import AbstractView from "./AbstractView.js";

export default class extends AbstractView {
  constructor(params) {
    super(params);
    this.setTitle("ViewPosts");
  }
  async getHtml() {
    return `
      <h1>ViewPosts</h1>
      <p>this is view page</p>
    `;
  }
}


위 과정을 그대로 따라가면 동적라우팅은 잘 적용된다.
그러나 주소창에 "post/숫자" 형태로 페이지를 이동하면 css가 적용되지 않는 이슈가 발생하고 있다.
서치해봐도 잘 나오지 않아 무엇이 문제인지 모르겠다..😢

그럼에도 늘 React로만 라우터 코드를 작성해왔어서 javascript로만 라우팅 코드를 작성해본 것은 좋은 경험이었다.
꼭 발판으로 삼아 javascript를 정복하고 말거야..

참고 영상 : https://www.youtube.com/watch?v=OstALBk-jTc
728x90