티스토리 뷰

728x90
SMALL

배경

엄청 오랫동안 코드스피츠 강의를 듣지 못했다. 논건아니다... 다른 우선순위에 치여서 못했을 뿐... 진짜.... 어쨋든 다시 코드스피츠 강의를 조금씩 듣고있다. 듣다보니 리팩터링과 겹치는 내용들이 많다. 둘다 켄트백의 영향을 많이 받아서 그런것같다. 리팩터링, 객체지향의 사실과 오해, 코드스피츠가 점차 한 점으로 모이고 있는 느낌이 든다. 원래 어떤 분야든 공부를 하댜보면 전혀 다른 분야의 과목도 한 점으로 모인다고 하는데 조금 느껴진다.

강의 이론

5,6강에서는 todo list를 만든다. todolist를 만들면서 이것저것 설명을 하시는데 설명에 관한 것들을 먼저 적고 마지막에 코드와 관련된 얘기를 하겠다.

상태값 boolean

상태값으로는 boolean이 좋지 않다. 사실 나도 알고 있던 내용이다. true, false는 너무나도 제한적이다. 확장이 불가능하다. 물론 너무나도 확실한 경우에는 true, false를 사용하기도 한다. 음... 한 가지 예를 들어보면 user table에 is_admin을 boolean으로 관리한다고 생각해보자. 이러면 확장이 불가능하다. status로 변경하고 admin, participant로 사용해야만 한다. 만약 중간 관리자가 추가되어야한다면 is_admin은 테이블 구조를 바꿔야한다. db마이그레이션을 해본사람은 알지만 엄청~~나게 큰 작업이다. 만약 프론트에서 isAdmin을 사용하고 싶다면 user status를 기반으로 isAdmin을 추가해주면 된다.

가변형에 대한 의사결정

값을 사용할 것인지 객체를 사용할 것인지를 두고 가변형에 대한 의사결정을 미리 해야한다. 일단 쉬운것부터 생각해보자. 자바스크립트에서 객체는 immutable이고 원시형은 mutable이다. 왜 그럴까?? 잘 생각해보면 문자열도 배열이다.(이런걸 알아야되서 c언어를 배우는건가??) 그런데 문자열은 mutable이고 string배열은 immutable이다. 뭔가 이상하다. 사실 원리까지 알 필요는 없다. 이걸로 알 수 있는건 결국 가변형에 대한 의사결정에 따라 얼마든지 달라질 수 있다는 것이다. A라는 클래스를 만들고 비교하는 method를 만들면 mutable하게 값을 비교할 수도 있다. 내가 그동안 당연하게 여기던 것들이 정답이 아닐수도 있다. 그래서 결국 가변형에 대한 의사결정만 되면 구현하는건 얼마든지 가능하다.

 

immutable은 참조 안전성을 보장한다. 또한 다중 스레드에서 새 객체이기 때문에 중복 참조도 없어진다. 그러면 무조건 immutable로 구현하는게 맞을까?? 그렇지만은 않다. 웹 프론트로 예를 들면 함수형 프로그래밍을 사용해 불변성을 유지하기도 하지만(극히 일부 회사) 거의 짬뽕해서 사용한다. 사실 함수형프로그래밍은 내가 잘 모르기도 하고 난이도가 있기도 하다. 어쨋든 중요한건 상황에 따라 적절한 선택이 필요하다.

객체지향에서 내부 구현

객체지향에서 내부 구현은 중요하지 않다. 내장을 깔 필요도 없다. 선언형 프로그래밍, 추상화와 관련된 내용이다. 내장을 까야된다면 그건 잘못된 코드다. 또한 호스트가 중요하지 객체는 중요하지 않다. 그냥 호스트에게 잘 가면된다. 예를 들어서 특정 함수를 실행했을 때 결과값만 잘 받으면 된다. 내부에서 무슨 일이 발생하는지는 전혀 중요하지 않다.(중요하지 않다는건 상위 컨텍스트 입장에서 중요하지 않다는 것이지 내부에 함수 내부 코드를 막 짜도 된다는 얘기는 아니다.)

todo list

마이크로소프트의 투두리스트를 예시로 보면서 투두리스트를 만들었다. 대략적으로 설명하면 folder를 만들고 folder안에 task를 만들면 된다.

Task

const Task = class {
  constructor(title, isCompleted = false) {
    this.title = title;
    this.isCompleted = isCompleted;
  }
  setTitle(title) {
    this.title = title;
    // return new Task(title, this.isCompleted);
  }
  toggle() {
    this.isCompleted = !this.isCompleted;
    // return new Task(this.title, !this.isCompleted);
  }
  getInfo() {
    return { title: this.title, isCompleted: this.isCompleted };
  }
  // isEqual(task) {
  //   return task.title === this.title && task.isCompleted === this.isCompleted;
  // }
};

정말 간단한 Task 클래스를 만들었다. 먼저 constructor부터 살펴보자. title과 isCompleted라는 값을 받고 this에 할당한다. 이때 isCompleted는 boolean이다. 좋은 선택일까?? 장기적으로 봤을 때는 좋은 선택이 아니다. 지라만 보더라도 여러 상태를 가진다. todo, complete, dev to qa 처럼 말이다. 이걸 알고 있지만 간단하게 만들기 위해서 그냥 boolean으로 만들었다.

 

setTitle을 구현하는 방법은 두가지가 있다. 더 정확히는 가변형에 대한 의사결정을 먼저 해야한다. immutable로 할건지 mutable로 할건지 말이다. immutable로 구현하려면 새로운 Task 클래스를 만들어야한다. 그리고 mutable로 구현하려면 그냥 편하게 this.title을 변경시키면 된다. toggle도 마찬가지라서 설명은 생략한다.

 

그리고 만약 immutable로 구현하면 isEqual이라는 메서드를 만들어서 내부 값을 비교해 immutable이지만 값을 비교할 수 있다.

 

이번 실습에서는 mutable을 적용하기로 했다.

 

그리고 이제 잘 동작하는지 테스트를 해봐야한다. 테스트코드를 작성하지 않더라도 테스트는 필수적이다. 그냥 넘어가버리면 어디서 잘못됬는지 디버깅 하기 몇배는 어려워진다. 그리고 이렇게 테스트를 적을바엔 테스트 코드를 적는게 맞기도 하다. 다음과 같이 정말 간단하게 테스트를 했다. 

() => {
  let isOkay = true;
  const task = new Task('test1');
  isOkay = task.getInfo().title === 'test1' && task.getInfo().isCompleted === false;
  console.log('test1', isOkay);
  task.toggle();
  isOkay = task.getInfo().title === 'test1' && task.getInfo().isCompleted === true;
  console.log('test2', isOkay);
};

 

Folder

const err = (v) => {
  throw v;
};
// production할때는 console.log로 변경. 이방법 많이씀.

const Folder = class {
  constructor(title) {
    this.title = title;
    this.tasks = new Set();
  }
  // addTask(title) {
  //   this.tasks.add(new Task(title));
  // }
  addTask(task) {
    if (!task instanceof Task) err('invalid task');
    this.tasks.add(task);
  }
  removeTask(task) {
    if (!task instanceof Task) err('invalid task');
    this.tasks.delete(task);
  }
  // 대칭성. 켄트백. add할때 title만받는데 remove할때 title만 받으면 이상해져.
  getTasks() {
    return [...this.tasks.values()];
  }
  // 범용적인 명사 동사 등은 프레임워크나 상위 개발자들이 만들어놓는다. 우리는 구체화된 변수명을 사용하자. ex) getList. but component는 도메인적인 맥락 제거하는게 좋을때도있음.
  getTitle() {
    return this.title;
  }
};

간단하게 폴더 클래스를 만들었다.

 

addTask를 만드는 방법은 인자로 title을 받을것인지 task를 받을것이지 두가지가 있다. 뭐가 옳은 선택일까?? 둘다 장단점이 있는걸까??

title을 인자로 받는다고 해보자. 그러면 사용하는쪽에서 Task라는 클래스를 몰라도 된다. 최대한 데이터는 은닉시키는게 좋다. 그리고 title만 받으면 코드도 깔끔해진다. 그럼에도 불구하고 title을 인자로 받는건 잘못된 선택이다. 장단점이 있는게 아니라 아얘 하면 안되는 선택이다.

 

개발자는 코드를 만들때 지금 눈앞에 놓인 상황만 보고 만들면 안된다. 요구사항 변경은 수시로 있고 100퍼센트는 아니더라도 어느정도 대응이 가능하게 코드를 작성해야한다. open closed 원칙(확장은 열고 수정은 닫기)을 생각하자. 만약 folder에 title, tasks 말고 다른 값이 들어오면 어떻게 해야할까? addTask에 인자를 추가해줘야한다. 벌써 확장이 어려워진다. 또한 드래그앤드롭으로 A folder의 task를 B folder로 옮기는 기능이 추가되었다고 해보자. 이런 여러가지 경우에 대응하기가 너무 어렵다. 그래서 이건 무조건 task 인스턴스 전체를 받아야만 한다. 또한 켄트백 선생님의 말에 따르면 대칭성을 맞춰야한다고 한다. remove할때 title만 받는다면 이상해진다. 같은 title이 있을수도 있으니 말이다.

 

getTasks 메서드에서는 spread연산자를 사용해서 새로운 객체를 반환한다. 아까 mutable로 한다고 했는데 이렇게 한 이유는 버그를 막기 위해서다. Task와 Folder는 싱글톤으로 동작한다. 그렇다고 해서 getter로 받은 값에 다른 값을 할당할 수 있다는 것을 의미하지는 않는다.

 

강사님은 범용적인 명사나 동사를 사용하지 말라고 말한다. 구체적인 변수명을 사용하라고 한다. 사실 이건 경우에 따라 조금 다르다고 생각한다. 예를들어서 도메인 맥락이 없는 컴포넌트를 만들때는 일반적인 인터페이스를 사용해야한다. 근데 위의 경우엔 도메인적인 요소이기 때문에 구체적인 네이밍을 넣어도 좋은 것 같다. 사실 나느 지금 강사님과 조금 다른 관점에서 얘기를 했다. 강사님은 프레임워크나 선임 개발자들이 범용적인 네이밍을 만들었기 때문에 사용하지 말라고 하셨는데 지금은 기본 html과 자바스크립트에 type module을 사용할 수 있다. 그런 측면에서 봤을 때 네이밍이 겹칠것을 피해야할까?? 여기에 타입스크립트까지 적용을 하는데...  이부분은 잘 모르겠다. 옛날에 찍은 영상이라서 그런걸수도 있고 내가 부족해서 모를수도 있다.

 

여기서도 테스트하자.

() => {
  let isOkay = true;
  const task = new Task('test1');
  const folder = new Folder('folder1');
  isOkay = folder.getTasks().length === 0;
  console.log('test1', isOkay);
  folder.addTask(task);
  isOkay = folder.getTasks().length === 1 && folder.getTasks()[0].getInfo().title === task.title;
  console.log('test2', isOkay);
  folder.addTask(task);
  isOkay = folder.getTasks().length === 1 && folder.getTasks()[0].getInfo().title === task.title;
  console.log('test3', isOkay);
};

 

App

const App = class {
  constructor() {
    this.folders = new Set();
  }
  addFolder(folder) {
    if (!folder instanceof Task) err('invalid task');
    this.folders.add(folder);
  }
  removeFolder(folder) {
    this.folders.delete(folder);
  }
  getFolders() {
    return [...this.folders.values()];
  }
};

App은 그냥 생략하겠다. 딱히 설명이 필요한 부분이 없는 것 같다.

 

Renderer

const Renderer = class {
  constructor(app) {
    this.app = app;
  }
  render() {
    this._render();
  }
  _render() {
    throw err('must be overrided');
  }
};
const el = (tag) => document.createElement(tag);
const DomRenderer = class extends Renderer {
  constructor(parent, app) {
    super(app);
    this.el = parent.appendChild(el('section'));
    this.el.innerHTML = `
      <nav>
        <input type='text'/>
        <ul></ul>
      </nav>
      <section>
        <header>
          <h2></h2>
          <input type='text'/>
        </header>
        <ul></ul>
      </section>
    `;
    this.el.querySelector('nav').style.cssText = 'float:left; width:200px; border-right:1px solid #000;';
    this.el.querySelector('section').style.cssText = 'margin-left:210px;';
    const ul = this.el.querySelectorAll('ul');
    this.folder = ul[0];
    this.task = ul[1];
    this.currentFolder = null;
    const input = this.el.querySelectorAll('input');
    input[0].addEventListener('keyup', (e) => {
      if (e.keyCode !== 13) return;
      const v = e.target.value.trim();
      if (!v) return;
      // 쉴드패턴. 위에서 다 튕겨내고 아래는 동작하는 로직만있는거. 화이트 블랙처럼
      const folder = new Folder(v);
      this.app.addFolder(folder);
      e.target.value = '';
      this.render();
    });
    input[1].addEventListener('keyup', (e) => {
      if (e.keyCode !== 13 || !this.currentFolder) return;
      const v = e.target.value.trim();
      if (!v) return;
      const task = new Task(v);
      this.currentFolder.addTask(task);
      e.target.value = '';
      this.render();
    });
  }
  _render() {
    const folders = this.app.getFolders();
    if (!this.currentFolder) this.currentFolder = folders[0];
    this.folder.innerHTML = '';
    folders.forEach((folder) => {
      const li = el('li');
      li.innerHTML = folder.getTitle();
      li.style.cssText = `font-size: ${this.currentFolder === folder ? '20px' : '12px'}`;
      li.addEventListener('click', () => {
        this.currentFolder = folder;
        this.render();
      });
      this.folder.appendChild(li);
    });
    if (!this.currentFolder) return;
    this.task.innerHTML = '';
    this.currentFolder.getTasks().forEach((t) => {
      const li = el('li');
      const { title, isCompleted } = t.getInfo();
      li.innerHTML = (isCompleted ? 'completed ' : 'process ') + title;
      li.addEventListener('click', () => {
        t.toggle();
        this.render();
      });
      this.task.appendChild(li);
    });
  }
};

new DomRenderer(document.body, new App());
// 전체 렌더는 그냥 그리면 돼. 데이터만 다루는거야. 어차피 라이브러리들이 해주는거야 그거는.

테트리스에도 Renderer 클래스를 만들고 DomRenderer 비슷한 걸 만들었었다. 그래서 설명은 생략한다. 대충 템플릿메소드패턴을 이용했다는 것 정도만 보고 넘어가자.

 

좀 로직이 지저분한데 이건 뷰와 이벤트를 web api를 이용해서 다루기 때문이다. + 일일히 render호출하는 부분

 

사실 리액트까지 갈 것도 없이 view와 이벤트를 다루는 Component 추상클래스 정도만 만들어도 해결이 가능하다. 그래서 다음 강의에는 디자인패턴과 뷰패턴을 다루나보다. 전체를 새로그리는 것도 spa아무 라이브러리 혹은 프레임워크를 적용하면된다. 직접 구현하는 것도 재밌다. 그래서 최근에 글을 하나 쓰기도 했다.

https://ms3864.tistory.com/435

 

조금 신경써서 볼 부분은 쉴드패턴 정도인것같다. 내가 자주 사용하는 패턴이기도 한데 이름은 처음 들었다. early return과도 비슷한데 조건이 성립하지 않는 경우 위에서 걸러주는 것이다. 이러면 이미 validation이 성공한 코드만 아래 코드에 오기 때문에 예외처리를 신경쓰지 않아도 된다. 결국 이것도 역할 책임문제다. 공부하면 할수록 역할 책임의 중요성을 느끼고 있다.

 

끝!!

728x90
LIST
댓글
공지사항