티스토리 뷰
컴포넌트 글을 보지 않았다면 먼저 봐주세요~~
https://ms3864.tistory.com/380
개요
저번에 만든 컴포넌트와 연동할 라우터를 만들어야 한다. 참고로 리액트 라우터와 유사하게(애초에 리액트 라우터는 context를 사용하기 때문에 다르게 만들 수 밖에 없다.) 만들 것이다.
구현할 것
무엇을 구현해야 할 지 생각해보자. 컴포넌트의 html을 string으로 구현했기 때문에 일단 리액트처럼 Link를 사용할 수는 없다. 그러면 그냥 a태그에 이벤트를 거는 방법도 있을것이고 새로운 태그에 이벤트를 걸어도 괜찮다. 그리고 /로 경로를 나눠서 상태로 가지고 있으면 좋겠다. 왜냐하면 params가 있는경우가 있기 때문이다. 그리고 리액트의 훅같은 함수가 있으면 더 편할것같다. 대충 pushstate, popstate를 이용해서 라우팅을 하면 될 것 같다. 아 query도 생각해서 만들자.
기본 틀 잡기
class Router {
constructor(target, routes, NotFoundPage) {
this.target = target;
this.routes = routes;
this.NotFoundPage = NotFoundPage;
this.set();
this.route();
this.addLinkChangeHandler();
this.addBackChangeHandler();
}
set() {
// 처음에 라우터 인스턴스가 생성될 때 해야할 세팅
}
route() {
// 경로를 찾는 메서드
}
addLinkChangeHandler() {
// a 태그의 이벤트를 바꾼다
this.target.addEventListener('click', e => {
const { target } = e;
const closest = target.closest('a');
if (!closest || closest.getAttribute('target')) return;
e.preventDefault();
const pathname = closest.getAttribute('href');
this.push(pathname);
});
}
addBackChangeHandler() {
window.addEventListener('popstate', () => {
this.route();
});
}
push(url) {
window.history.pushState(null, '', url);
this.route();
}
}
export default Router;
a태그의 이벤트를 바꿔주고 pushState 후에 라우트 실행. popState에도 라우트 실행.
route 메서드
route 메서드는 어떻게 구현해야 할까?? 위에서 말한대로 /로 나눠서 생각하면 될 것 같다. params가 있는 경우도 있기 때문에 그 경우는 for문에서 continue로 그냥 넘겨주면 될 것같다. 이때 상태값을 배열로 만들면 좋을 것 같다. 객체로 만들면 시간복잡도가 줄어들지만 parmas를 사용할 수 없다. 그리고 상태값은 꼭 router 클래스 안에 있을 필요가 없다. 분리하자. 그리고 pathname, query, params를 state로 정의하자. 이제 좀 개요가 잡힌다.
utils.js
const getPathname = () => {
return window.location.pathname;
};
const getQuery = () => {
const { search } = window.location;
const queries = new URLSearchParams(search);
const params = {};
queries.forEach((value, key) => {
params[key] = value;
});
return params;
};
const pathValidation = (currentPath, routePath) => {
if (currentPath.length !== routePath.length) return false;
const params = {};
let index = 0;
for (index = 0; index < currentPath.length; index++) {
if (/^:/.test(routePath[index])) {
params[routePath[index].slice(1)] = currentPath[index];
continue;
}
if (currentPath[index] !== routePath[index]) {
return false;
}
}
return params;
};
export { getPathname, getQuery, pathValidation };
router-context.js
import { getPathname, getQuery } from './utils';
class RouterContext {
constructor() {
this.state = { pathname: getPathname(), query: getQuery(), params: {}, push: () => {}, goBack: () => {} };
}
setState(nextState) {
this.state = { ...this.state, ...nextState };
}
}
export default new RouterContext();
router.js
import routerContext from './router-context';
route() {
const currentPath = this.routerContext.state.pathname.slice(1).split('/');
for (let i = 0; i < this.routes.length; i++) {
const routePath = this.routes[i].path.slice(1).split('/');
const params = pathValidation(currentPath, routePath);
if (!params) continue;
routerContext.setState({ params });
const Page = this.routes[i].component;
new Page(this.target);
return;
}
new this.NotFoundPage(this.target);
}
처음에 currentPath는 처음 /를 포함해서 split을 하면 '/user/123' => ['','user','123'] 이 되기 때문에 첫번째 글자를 자르는 것이다.
pathValidation 함수는 return을 false, 또는 params를 해주는데 경로가 다르다면 false를, 존재한다면 그 경로에서 찾은 parmas를 return해준다. 만약 params가 존재하지 않느다면 빈 객체를 반환한다. 실제 리액트 라우터에서는 조금 다르게 동작하는데 바닐라자바스크립트라서 그냥 간단하게만 구현했다. 그리고 Route의 두번째 인자인 routes는
this.routes = [
{ path: '/', component: PostListPage },
{ path: '/login', component: LoginPage },
{ path: '/signup', component: SignupPage },
{ path: '/post/:id', component: PostPage },
];
이런식으로 넣어주면 된다. 그리고 만약 모든 path와 일치하지 않는다면 NotFoundPage를 렌더링해주는 것이다.
조금 더 나아가보자
routerContext에서 state를 이용해 훅스를 만들고 싶다. pathname, query, params는 이미있는데 push와 goBack이 비어있다.사실 routerContext에서 직접 정의해줄수도 있지만 router에서 해주는게 재사용도 되고 조금 더 괜찮을 것 같다. 그래서 route의 set 함수에서 routerContext의 상태를 바꿔주자.
class Router {
constructor(target, routes, NotFoundPage) {
this.target = target;
this.routes = routes;
this.NotFoundPage = NotFoundPage;
this.routerContext = routerContext;
this.push = this.push.bind(this);
this.goBack = this.goBack.bind(this);
this.set();
this.route();
this.addLinkChangeHandler();
this.addBackChangeHandler();
}
set() {
routerContext.setState({ push: url => this.push(url) });
routerContext.setState({ goBack: () => this.goBack() });
}
예시코드
router.js
import routerContext from './router-context';
import { getPathname, getQuery, pathValidation } from './utils';
class Router {
constructor(target, routes, NotFoundPage) {
this.target = target;
this.routes = routes;
this.NotFoundPage = NotFoundPage;
this.routerContext = routerContext;
this.push = this.push.bind(this);
this.goBack = this.goBack.bind(this);
this.set();
this.route();
this.addLinkChangeHandler();
this.addBackChangeHandler();
}
set() {
routerContext.setState({ push: url => this.push(url) });
routerContext.setState({ goBack: () => this.goBack() });
}
route() {
const currentPath = this.routerContext.state.pathname.slice(1).split('/');
for (let i = 0; i < this.routes.length; i++) {
const routePath = this.routes[i].path.slice(1).split('/');
const params = pathValidation(currentPath, routePath);
if (!params) continue;
routerContext.setState({ params });
const Page = this.routes[i].component;
new Page(this.target);
return;
}
new this.NotFoundPage(this.target);
}
addLinkChangeHandler() {
this.target.addEventListener('click', e => {
const { target } = e;
const closest = target.closest('a');
if (!closest || closest.getAttribute('target')) return;
e.preventDefault();
const pathname = closest.getAttribute('href');
this.push(pathname);
});
}
addBackChangeHandler() {
window.addEventListener('popstate', () => {
routerContext.setState({ pathname: getPathname(), query: getQuery() });
this.route();
});
}
push(url) {
window.history.pushState(null, '', url);
routerContext.setState({ pathname: url, query: getQuery() });
this.route();
}
goBack() {
window.history.back();
}
}
export default Router;
router-context.js
import { getPathname, getQuery } from './utils';
class RouterContext {
constructor() {
this.state = { pathname: getPathname(), query: getQuery(), params: {}, push: () => {}, goBack: () => {} };
}
setState(nextState) {
this.state = { ...this.state, ...nextState };
}
}
export default new RouterContext();
routerHooks.js
import routerContext from './router-context';
const useHistory = () => routerContext.state;
const usePathname = () => routerContext.state.pathname;
const useQuery = () => routerContext.state.query;
const useParams = () => routerContext.state.params;
export { useHistory, useQuery, useParams, usePathname };
index.js
import App from './app';
import './styles/reset.css';
import './styles/index.css';
new App(document.getElementById('root'));
app.js
// import { Router } from 'ms-vanilla';
import Router from './lib/router';
import LoginPage from './pages/login-page';
import NotFoundPage from './pages/not-found-page';
import PostListPage from './pages/post-list-page';
import PostPage from './pages/post-page';
import SignupPage from './pages/signup-page';
import userStore from './store/user-store';
class App {
constructor(target) {
this.target = target;
this.routes = [
{ path: '/', component: PostListPage },
{ path: '/login', component: LoginPage },
{ path: '/signup', component: SignupPage },
{ path: '/post/:id', component: PostPage },
];
this.NotFoundPage = NotFoundPage;
this.render();
this.init();
}
render() {
new Router(this.target, this.routes, this.NotFoundPage);
}
init() {
if (localStorage.getItem('user')) userStore.autoLogin();
}
}
export default App;
'자바스크립트 > 바닐라 자바스크립트' 카테고리의 다른 글
자바스크립트 프로토타입 배경부터 실제 사용까지 (0) | 2021.12.24 |
---|---|
자바스크립트로 스크롤 이동하기 (0) | 2021.10.30 |
바닐라 자바스크립트에서 무한 스크롤 구현하기 (0) | 2021.10.23 |
바닐라 자바스크립트로 프로젝트 만들기(옵저버패턴) (2) | 2021.10.20 |
바닐라 자바스크립트로 프로젝트 만들기(컴포넌트) (0) | 2021.10.10 |