과제에서 진행했던 노션 클론을 뚝딱 리팩토링 해보자!
import { validateState } from "../utils/validateState.js";
export default class Component {
state;
constructor({ $target, tagName }) {
this.$target = $target;
this.wrapper = document.createElement(tagName);
this.$target.appendChild(this.wrapper);
this.createTemplate();
this.setEvent();
}
render() {
const content = this.createTemplate();
this.wrapper.innerHTML = content;
this.renderChild();
}
createTemplate() {
return "";
}
setEvent() {
this.addEvent();
}
addEvent(eventType, selector, callback) {
this.wrapper.addEventListener(eventType, (e) => {
if (!e.target.closest(selector)) return false;
callback(e);
});
}
renderChild() {}
setState(nextState) {
const prevState = this.state;
if (!isEqaul(prevState, nextState)) {
this.state = validateState(this.state, nextState);
this.render();
}
}
}
황준일 개발자님의 코드를 많이 참고하였다. 사실 이전 TodoApp만들기에서도 활용했었는데, 리팩토링 해보는건 처음이다!
여기에 Redux식 코드도 추가하고싶지만, 일단 생성자 함수로 만들어진 나의 더러운 코드를 정화하는것이 1차 목표다.
겹치는 기능은 대략적으로 추상화했지만, 분기가 있다.
wrapper
기능이 필요없는, 로직만 처리해주는 container컴포넌트가 존재할 수 있다.import { validateState } from "../utils/validateState.js";
export default class Component {
state;
constructor({ $target, tagName = null }) {
this.$target = $target;
this.wrapper = tagName ? document.createElement(tagName) : null;
this.wrapper && this.$target.appendChild(this.wrapper);
this.setEvent();
this.render();
}
render() {
if (this.wrapper) {
const content = this.createTemplate();
this.wrapper.innerHTML = content;
}
this.renderChild();
}
createTemplate() {
return "";
}
setEvent() {
this.addEvent();
}
addEvent(eventType, selector, callback) {
if (!this.wrapper) {
return;
}
this.wrapper.addEventListener(eventType, (e) => {
if (!e.target.closest(selector)) return false;
callback(e);
});
}
renderChild() {}
setState(nextState) {
const prevState = this.state;
if (!isEqaul(prevState, nextState)) {
this.state = validateState(this.state, nextState);
this.render();
}
}
}
아래와 같은 구조로 라우터를 만들었다.
//Router클래스 밑에 Route에 컴포넌트와 path, state등을 넣어준다...
new Router(
new Route({
$target: this.$app,
path: "documents",
component: DocumentPage,
initialState: "",
}),
new Route({....})
);
//Route.js
export default class Route {
constructor($target, path, component, initialState) {
this.path = path;
this.$target = $target;
this.initialState = initialState;
this.component = component;
}
}
//Router.js
export default class Router {
constructor() {
this.routesMap = new Map();
this.routes = Array.from(arguments);
...아래코드더있음.
이렇게 arguments
객체로 Router의 constructor로 받아온 Route들을 가져오려했다.
그런데, 이상하게 가져와짐..! $target, path, component, initialState
이렇게 가져와지는게 아니라
$target
아래에 모두 받아와진다.
rest 파라미터도마찬가지다
constructor로 받아올때 구조분해할당을 제대로 하지 않았었다...예전에 같은 실수를 한 번 했는데 또했네 ㅠㅠㅋㅋ
//Route.js
export default class Route {
constructor({ $target, path, component, initialState }) {
this.path = path;
this.$target = $target;
this.initialState = initialState;
this.component = component;
}
}
//Router.js
export default class Router {
constructor( ...args ) {
말이 좀 길지만 위의 코드보면 이해가 간다.
아무튼...저기서 변형시켜서 결국 Router의 타겟도 라우팅되는 컴포넌트의 타겟과 같다.
고로 $target
하나만 받아와서 여기저기 나눠쓰면되는데...
new Router(
{ $target: this.$app },
new Route({
path: "documents",
component: DocumentPage,
initialState: "",
})
);
//Router.js
export default class Router {
constructor() {
console.log(arguments);
이렇게 받아와서 arguments
를 콘솔에 찍어봤는데...
웩....
왤까 열심히 고민해보자. js의 동작원리를 정확히 알고있었다면 헤메지 않았을텐데...
이번 기회에 arguments와 레스트 파라미터에 대해 아주 제대로 배워가야지
라고 생각했건만. Componet class constructor 내부에서 renderChild를 실행해버렸기에...
export default class App extends Component {
constructor($target, tagName) {
super($target, tagName);
this.$app = document.getElementById("app");
}
//NavPage는 항상 렌더되야한다
renderChild() {
new Nav({
$target: this.$target,
});
new Router(
{ $target: this.$app },
new Route({
path: "documents",
component: DocumentPage,
initialState: "",
})
);
}
}
그걸 상속받은 App은 이미 renderchild
를 실행했고, $app
은 아직 선언 이전이다...
그냥 routing
메서드 하나 추가해서 실행해주었다.
export default class App extends Component {
constructor($target, tagName) {
super($target, tagName);
this.$app = document.getElementById("app");
this.routing();
}
//NavPage는 항상 렌더되야한다
renderChild() {
new Nav({
$target: this.$target,
});
}
routing() {
new Router(
{ $target: this.$app },
new Route({
path: "documents",
component: DocumentPage,
initialState: "",
})
);
}
Map.get
으로 꺼내오는데, undefined
가 뜰때가 있다...
Cannot read properties of undefined (reading 'routesMap')
....
export default class Router {
constructor({ $target }, ...routes) {
this.routesMap = new Map(); // path를 찾으면 {component, initialState}가 나온다
this.routes = routes;
this.$target = $target;
this.addRoutesInMap();
this.addRouteEvent();
}
addRoutesInMap() {
this.routes.forEach((route) =>
this.routesMap.set(route.path, {
component: route.component,
initialState: route.initialState,
})
);
console.log(this.routesMap.get("documents"));
}
handleRoute() {
const [path, pathData] = getPathData();
const { component, initialState } = this.routesMap.get(path) || {
//routes.Map에 없을때 에러처리용
component: ErrorPage,
initialState: "",
};
if (path === "") {
this.$target.innerHTML = "";
return;
}
new component({ $target: this.$target, initialState });
}
addRouteEvent() {
initRouter(this.handleRoute);
}
}
구조는 이렇다.
this.routeMap
이 정의되지 않을때가 있나? 하고 생각해보니
콜백으로 this.handleRoute
를 전달해줄 때 this
바인딩 문제가 있겠다 생각했다.
addRouteEvent() {
initRouter(() => this.handleRoute());
}
이렇게 오늘 배운 화살표 함수로 this
바인딩을 제거해주면 완벽!
바로 상위스코프인 Router가 this
에 걸린다!