노션클론 리팩토링(1)

김영현·2023년 11월 5일
0

서론

과제에서 진행했던 노션 클론을 뚝딱 리팩토링 해보자!


겹치는 기능을 추상화하기

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차 목표다.

Component class를 조금 더 변화시켜보자

겹치는 기능은 대략적으로 추상화했지만, 분기가 있다.

  1. wrapper기능이 필요없는, 로직만 처리해주는 container컴포넌트가 존재할 수 있다.
    따라서 tagName이 있을때만 wrapper를 생성해주자.
  2. 라우터를 변화시켜보자. Component class를 상속받아 Router class를 만들고, 라우터가 필요한 컴포넌트들을 라우터 컴포넌트 내부에 렌더링 시켜보자!
    2-1. Component class는 인스턴스를 생성하면, 무조건 렌더링 되게 되어있다. 하지만 라우터에선 주소별로 렌더링 되느냐 마느냐를 결정한다.
    2-2. 다시 생각해보니 굳이 Router ClassComponent Class를 상속받을 필요가 없다.
    2-3. wrapper가 없는 컴포넌트도 존재한다... 따로분리하는게 좋겠지만 일단 분기처리만 해뒀다.
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 class 문제해결(arguments, ...args)로 매개변수 받아온 클래스의 매개변수 받아오기.

아래와 같은 구조로 라우터를 만들었다.

//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: "",
      })
    );
  }

콜백함수로 넘겨줄때 this 문제발생

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바인딩을 제거해주면 완벽!
바로 상위스코프인 Routerthis에 걸린다!


profile
모르는 것을 모른다고 하기

0개의 댓글