앞서 배웠던 class 를 view 클래스로 공통으로 빼야 한다. 공통된 목적을 추출
하는게 우선이다.
NewsFeed 나 NewsDetail 의 함수는 UI 를 업데이트 하는 함수이고 그 외에 makeFeed, updateView 등의 함수들은 UI를 업데이트 함에 있어 보조적인 기능을 업데이트 하는 함수이다.
constructor
: 인스턴스를 처음 만들때에만 필요한 코드를 남겨두고 나머지 코드들은 해당하는 목적의 메소드들로 분류해놓으면 좋다. 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;
}
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;
}
}
}
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();
}
}
// 기능을 제공하고 그 기능을 이용햏서 바깥쪽에서 특정한 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();
클래스 부분을 바꾸는 일을 조금 헷갈렸다. 그래도 두세번 들으니까 조금 알겠긴 한데 이런 작업들은 실제로 내가 생각하고 하나하나 코드를 구현해 봐야지 점점 실력이 늘 것 같다. 그래도 이해의 범위가 좀 깊어졌다. 좋은 일이다.