티스토리 뷰

728x90
SMALL

강의내용

 

이번 강의에서는 저번시간에 만들었던 todo list를 리팩터링하면서 기능, 성능을 추가한다.

 

html

먼저 js로 관리하던 dom 코드를 html파일로 이동시켰다. 리액트는 dom을 js로 관리한다. 자바스크립트로 모든것을 다루는 것은 분명히 장점이 있지만 그건 리액트에 dom api를 위임하고 jsx를 이용해서 가독성을 높였기 때문이다. 그런게 없는 일반 자바스크립트에서 기본 dom 코드가 있는 것은 오히려 코드의 가독성을 낮춘다. 

 

extends set

set을 사용하는 코드가 있었는데 클래스 자체에서 set을 extends하게 변경했다. 그러면 다음과 같이 super로 set에 접근할 수 있다. 테트리스 글에서 올리긴 했는데 이건 진짜 set으로 동작한다. es6에서 나온 클래스로만 할 수 있는 방법이다. prototype과 생성자 함수로는 구현할 수 없는 형태다. 클래스는 부모로부터 오브젝트가 만들어지기 때문에(홈 오브젝트) 이런식의 접근이 가능한 것이다.

const Folder = class extends Set {
  addTask(task) {
    if (!task instanceof Task) err('invalid task');
    super.add(task);
  }

 

추가적으로  Folder는 set 그 자체가 되었기 때문에 기본 메서드의 동작을 막아야한다. 그래서 다음과 같이 오버라이드를 막아준다.

  add() {
    err('...');
  }
  delete() {
    err('...');
  }
  clear() {
    err('...');
  }
  values() {
    err('...');
  }

 

드래그앤 드롭 구현

사실 드래그앤드롭은 그렇게 어려운 기능은 아니다. dom api가 이미 지원하고 있고 보통은 라이브러리를 사용한다. 그래서 구현하는 방법은 적지 않겠다. 객체지향관점에서 설명하겠다. 보통은 Datatransfer를 이용해서 드래그앤드롭을 구현한다.(항상은 아니다) 그런데 여기에는 객체제를 넣을 수 없다. json으로 변환하고 parsing하는 것도 방법이긴 한데 여기서는 상위 컨텍스트에서 변수에 객체를 저장하고 drop할 때 그 변수에 접근한다. id같은걸로 판단할 수도 있지만 객체지향에서는 객체 그 자체로 판단하는 것이 좋다고 했다. 음... 최근에 드래그앤 드롭을 구현하긴 했는데 라이브러리 두개가 연관되어 있고 알아서 변경점을 찾아줘서 내가 실제 코드로 적을일은 없었다. 나중에 직접 구현하게 된다면 객체 그 자체를 옮겨야겠다.

 

풀링, 증분 렌더링

랜더링할때 innerHTML로 전체를 새롭게 그리는데 이건 좋은 방법이 아니다. 하지만 강사님은 이건 라이브러리에서 해주기 때문에 도메인부분을 신경쓰라고 했다. 이번에는 예시로 풀링과 증분 렌더링을 구현했다. 바뀐 부분만 체크해서 리렌더링하고 폴더를 바꿀 때 task의 정보를 배열에 저장하고 다시 그 폴더로 간다면 저장된 폴더를 가져온다. 그리고 당연히 이런 개별적인 처리는 코드가 엄청나게 복잡해진다. 이런 간단한 코드임에도 작성하는게 쉽지 않고 디버깅도 어렵다. 그래서 디자인패턴을 적용해야한다. (리액트라던가 뷰라던가) 나도 예전에 성능을 위해서 바뀐 부분만 변경시키는 코드를 작성했는데 프로젝트가 커지면 커질수록 알아볼 수가 없게 된다. 이런일이 발생한 원인은 역할 책임의 분리가 되지 않아서다. dom과 관련된 코드, 리렌더링을 최적화시키는 코드는 외부에 위임을 해야한다. 물론 DomRenderer 자체가 dom과 관련된 부분만 모아둔 코드이지만 추상화되어있지 않은 날것의 코드이기 때문에 제약사항이 많은 것이다. 그래서 다음 강의에서는 view 패턴을 배운다.

const DomRenderer = class extends Renderer {
  constructor(parent, app) {
    super(app);
    this.taskEl = [];

  _render() {
    const folders = this.app.getFolders();
    let moveTask, tasks;
    if (!this.currentFolder) this.currentFolder = folders[0];

    let oldEl = this.folder.firstElementChild,
      lastEl = null;
    folders.forEach((folder) => {
      let li;
      if (oldEl) {
        li = oldEl;
        oldEl = oldEl.nextElementSibling;
      } else {
        li = el('li');
        this.folder.appendChild(li);
        oldEl = null;
      }
      lastEl = li;
      li.innerHTML = folder.getTitle();
      li.style.cssText = `font-size: ${this.currentFolder === folder ? '20px' : '12px'}`;
      li.onclick = () => {
        this.currentFolder = folder;
        this.render();
      };
      li.ondrop = (e) => {
        e.preventDefault();
        folder.moveTask(moveTask, this.currentFolder);
      };
      li.ondragover = (e) => {
        e.preventDefault();
      };
    });
    if (lastEl) {
      while ((oldEl = this.task.firstElementChild)) {
        this.task.removeChild(oldEl);
        this.taskEl.push(oldEl);
      }
    }
    if (!this.currentFolder) return;
    tasks = this.currentFolder.getTasks();
    if (tasks.length === 0) {
      while (this.task.firstElementChild) {
        this.task.removeChild(this.task.firstElementChild);
      }
    } else {
      (oldEl = this.task.firstElementChild), (lastEl = null);
      tasks.forEach((t) => {
        let li;
        if (oldEl) {
          li = oldEl;
          oldEl = oldEl.nextElementSibling;
        } else {
          li = this.taskEl.length ? this.taskEl.pop() : el('li');
          this.task.appendChild(li);
          oldEl = null;
        }
        lastEl = li;
        const { title, isCompleted } = t.getInfo();
        li.setAttribute('draggable', true);
        li.innerHTML = (isCompleted ? 'completed ' : 'process ') + title;
        li.addEventListener('click', (e) => {
          // e.preventDefault();
          t.toggle();
          this.render();
        });
        li.addEventListener('dragstart', (e) => {
          // e.preventDefault();
          moveTask = t;
        });
      });
      if (lastEl) {
        while ((oldEl = lastEl.nextElementSibling)) {
          this.task.removeChild(oldEl);
          this.taskEl.push(oldEl);
        }
      }
    }
  }

 

이벤트 중복 제거

바뀌는 부분만 리렌더링하면서 이벤트 중복 문제가 발생했다. 그래서 그냥 addEventListener를 지워주고 onclick 등으로 변경했다. 참고로 이벤트 중복 제거는 좀 어렵다. 

 

load, save 구현

localstorage에 load, save를 구현한다. 여기서도 역할과 책임의 중요성이 나온다. 각각의 클래스에서는 각자의 역할과 책임만 가진다.

 

DomRenderer에서는 load와 sav이벤트를 할당한다. 그리고 save할때는 app자체를 저장한다. 이건 다른 클래스에서 알 필요가 없는 사실이다. 반면에 load는 조금 다르다. App.load를 통해서 역할을 위임한다.

const DomRenderer = class extends Renderer {
  constructor(parent, app) {
    super(app);
    const [load, save] = Array.from(parent.querySelectorAll('button'));
    // create를 먼저해라. view, list, delete 순서.
    load.onclick = (e) => {
      const v = localStorage['todo'];
      if (v) {
        this.app = App.load(JSON.parse(v));
        this.render();
      }
    };
    save.onclick = (e) => {
      localStorage['todo'] = JSON.stringify(this.app);
    };

 

App

App에서는 load를 할 때 폴더에 역할을 위임한다.

const App = class extends Set {
  static load(json) {
    const app = new App();
    json.forEach((f) => {
      app.addFolder(Folder.load(f));
    });
    return app;
  }
  toJSON() {
    return this.getFolders();
  }

 

Folder

Folder에서도 마찬가지로 Task에 역할을 위임한다.

const Folder = class extends Set {
  static load(json) {
    const folder = new Folder(json.title);
    json.tasks.forEach((t) => {
      folder.addTask(Task.load(t));
    });
    return folder;
  }

 

Task

const Task = class {
  static load(json) {
    const task = new Task(json.title, json.isCompleted);
    return task;
  }

 

https://github.com/yoonminsang/code-spitz/blob/main/function-and-oop/to-do/index.js

 

728x90
LIST
댓글
공지사항