View class 로 코드 구조 개선

dev__bokyoung·2022년 9월 6일
0

해커뉴스 클론코딩

목록 보기
10/12
post-thumbnail

프롤로그

앞서 배웠던 class 를 view 클래스로 공통으로 빼야 한다. 공통된 목적을 추출 하는게 우선이다.

1. 공통된 목적을 추출

NewsFeed 나 NewsDetail 의 함수는 UI 를 업데이트 하는 함수이고 그 외에 makeFeed, updateView 등의 함수들은 UI를 업데이트 함에 있어 보조적인 기능을 업데이트 하는 함수이다.

  • constructor : 인스턴스를 처음 만들때에만 필요한 코드를 남겨두고 나머지 코드들은 해당하는 목적의 메소드들로 분류해놓으면 좋다.
  • 상위 클래스로 뽑아 낼 수 있는 것을 많이 뽑아 낼 수 있으면 좋다. (공통된 요소들을 찾아내기)

2. UI 를 담당하는 View

NewsFeed 와 NewsDetail에 있는 공통된 부분을 View Class 로 빼자.
주석으로 소소팁(?)을 적어 놓았다.


abstract class View {
  private template: string;
  private renderTemplate: string;
  private container: HTMLElement;
  private htmlList: string[];

  constructor(containerId: string, template: string) {
    // 루트 엘리먼트 그리는 부분 
    const containerElement = document.getElementById(containerId);
    // 오류 처리 
    if (!containerElement) {
      throw "최상위 컨테이너가 없어 UI를 진행하지 못합니다.";
    }


    this.container = containerElement;
    this.template = template;
    this.renderTemplate = template;
    this.htmlList = [];
  }

  // 두 군데 모두 사용되는 함수 - container 정보를 가지고 있다. 
  protected updateView(): void {
    this.container.innerHTML = this.renderTemplate;
    this.renderTemplate = this.template;
  }

  // 상위클래스에서 (하위클래스에서 직접하지않고) 기능을 제공한다는 컨셉!! 
  // 기존에는 htmllist 배열에 데이터 푸시 (기능만 외부에 노출시키는 방법이 좋음 - 그래서 상위 클래스에 넣어놓음.)
  protected addHtml(htmlString: string): void {
    this.htmlList.push(htmlString);
  }

  // 기존에 template 원본은 유지하고있어야 계속된 update에서도 새로운 데이터로 업데이트 할 수 잇다.
  protected getHtml(): string {
    const snapshot = this.htmlList.join("");
    this.clearHtmlList();
    return snapshot;
  }

  protected setTemplateData(key: string, value: string): void {
    this.renderTemplate = this.renderTemplate.replace(`{{__${key}__}}`, value);
  }

  // 데이터를 지우는 부분도 따로 빼주자. 
  private clearHtmlList(): void {
    this.htmlList = [];
  }

// View 클래스를 가지고 있으면 render 를 실행시켜
  abstract render(): void;
}

NewsFeedView


class NewsFeedView extends View {
  private api: NewsFeedApi;
  private feeds: NewsFeed[];

  // 루트인자를 상위로 부터 받으면 훨씬 더 유연성이 커진다. 
  constructor(containerId: string) {
    let  : string = `
      <div class="bg-gray-600 min-h-screen">
        <div class="bg-white text-xl">
          <div class="mx-auto px-4">
            <div class="flex justify-between items-center py-6">
              <div class="flex justify-start">
                <h1 class="font-extrabold">Hacker News</h1>
              </div>
              <div class="items-center justify-end">
                <a href="#/page/{{__prev_page__}}" class="text-gray-500">
                  Previous
                </a>
                <a href="#/page/{{__next_page__}}" class="text-gray-500 ml-4">
                  Next
                </a>
              </div>
            </div> 
          </div>
        </div>
        <div class="p-4 text-2xl text-gray-700">
          {{__news_feed__}}        
        </div>
      </div>
    `;

    super(containerId, template);

    this.api = new NewsFeedApi(NEWS_URL);
    this.feeds = store.feeds;

    if (this.feeds.length === 0) {
      this.feeds = store.feeds = this.api.getData();
      this.makeFeeds();
    }
  }

  render(): void {
    store.currentPage = Number(location.hash.substr(7) || 1);

    for (
      let i = (store.currentPage - 1) * 10;
      i < store.currentPage * 10;
      i++
    ) {
      // 구조분해할당 
      const { id, title, comments_count, user, points, time_ago, read } =
        this.feeds[i];
        // 기능만 보여줄 수 있게
      this.addHtml(`
        <div class="p-6 ${
          read ? "bg-red-500" : "bg-white"
        } mt-6 rounded-lg shadow-md transition-colors duration-500 hover:bg-green-100">
          <div class="flex">
            <div class="flex-auto">
              <a href="#/show/${id}">${title}</a>  
            </div>
            <div class="text-center text-sm">
              <div class="w-10 text-white bg-green-300 rounded-lg px-0 py-2">${comments_count}</div>
            </div>
          </div>
          <div class="flex mt-3">
            <div class="grid grid-cols-3 text-sm text-gray-500">
              <div><i class="fas fa-user mr-1"></i>${user}</div>
              <div><i class="fas fa-heart mr-1"></i>${points}</div>
              <div><i class="far fa-clock mr-1"></i>${time_ago}</div>
            </div>  
          </div>
        </div>    
      `);
    }
    // 데이터 대체 하는 부분도 클래스에서 상속받아서 사용하면 좋다. 
    this.setTemplateData("news_feed", this.getHtml());
    this.setTemplateData(
      "prev_page",
      String(store.currentPage > 1 ? store.currentPage - 1 : 1)
    );
    this.setTemplateData("next_page", String(store.currentPage + 1));

    this.updateView();
  }

  private makeFeeds(): void {
    for (let i = 0; i < this.feeds.length; i++) {
      this.feeds[i].read = false;
    }
  }
}

NewsDetailView


class NewsDetailView extends View {
  constructor(containerId: string) {
    let template = `
      <div class="bg-gray-600 min-h-screen pb-8">
        <div class="bg-white text-xl">
          <div class="mx-auto px-4">
            <div class="flex justify-between items-center py-6">
              <div class="flex justify-start">
                <h1 class="font-extrabold">Hacker News</h1>
              </div>
              <div class="items-center justify-end">
                <a href="#/page/{{__currentPage__}}" class="text-gray-500">
                  <i class="fa fa-times"></i>
                </a>
              </div>
            </div>
          </div>
        </div>
  
        <div class="h-full border rounded-xl bg-white m-6 p-4 ">
          <h2>{{__title__}}</h2>
          <div class="text-gray-400 h-20">
            {{__content__}}
          </div>
  
          {{__comments__}}
  
        </div>
      </div>
    `;

    super(containerId, template);
  }

  render() {
    const id = location.hash.substr(7);
    const api = new NewsDetailApi(CONTENT_URL.replace("@id", id));
    const newsDetail: NewsDetail = api.getData();

    for (let i = 0; i < store.feeds.length; i++) {
      if (store.feeds[i].id === Number(id)) {
        store.feeds[i].read = true;
        break;
      }
    }

    this.setTemplateData("comments", this.makeComment(newsDetail.comments));
    this.setTemplateData("currentPage", String(store.currentPage));
    this.setTemplateData("title", newsDetail.title);
    this.setTemplateData("content", newsDetail.content);

    this.updateView();
  }

  makeComment(comments: NewsComment[]): string {
    for (let i = 0; i < comments.length; i++) {
      const comment: NewsComment = comments[i];

      this.addHtml(`
        <div style="padding-left: ${comment.level * 40}px;" class="mt-4">
          <div class="text-gray-400">
            <i class="fa fa-sort-up mr-2"></i>
            <strong>${comment.user}</strong> ${comment.time_ago}
          </div>
          <p class="text-gray-700">${comment.content}</p>
        </div>      
      `);

      if (comment.comments.length > 0) {
        this.addHtml(this.makeComment(comment.comments));
      }
    }

    return this.getHtml();
  }
}

3. Router 와 그 외


// 기능을 제공하고 그 기능을 이용햏서 바깥쪽에서 특정한 hash 값이 되면 어떤 페이지로 이동하게끔 하는 것이 설정하는데 가장 좋은 모양 
class Router {
  routeTable: RouteInfo[];
  defaultRoute: RouteInfo | null;

  constructor() {
    window.addEventListener("hashchange", this.route.bind(this));

    this.routeTable = [];
    this.defaultRoute = null;
  }

  setDefaultPage(page: View): void {
    this.defaultRoute = { path: "", page };
  }

  addRoutePath(path: string, page: View): void {
    this.routeTable.push({ path, page });
  }

  route() {
    const routePath = location.hash;

    if (routePath === "" && this.defaultRoute) {
      this.defaultRoute.page.render();
    }

    for (const routeInfo of this.routeTable) {
      if (routePath.indexOf(routeInfo.path) >= 0) {
        routeInfo.page.render();
        break;
      }
    }
  }
}


// 인스턴스 생성 
const router: Router = new Router();
const newsFeedView = new NewsFeedView("root");
const newsDetailView = new NewsDetailView("root");

router.setDefaultPage(newsFeedView); 

router.addRoutePath("/page/", newsFeedView);
router.addRoutePath("/show/", newsDetailView);

router.route();

에필로그

클래스 부분을 바꾸는 일을 조금 헷갈렸다. 그래도 두세번 들으니까 조금 알겠긴 한데 이런 작업들은 실제로 내가 생각하고 하나하나 코드를 구현해 봐야지 점점 실력이 늘 것 같다. 그래도 이해의 범위가 좀 깊어졌다. 좋은 일이다.

profile
개발하며 얻은 인사이트들을 공유합니다.

0개의 댓글