콜백함수 없는 비동기코드 작성법

 

지금까지의 코드에서 비동기 처리를 해야하는 부분(서버에서 데이터를 가져오는)은 XHR을 통해 콜백함수로 받아오거나, fetch를 사용해서 Promise를 리턴하는 방법이 있다.

 

Promise 패턴을 사용하게되면 then(), catch(), finally 등으로 코드를 단계적으로 확인할 수 있는데, 이 방법 말고도 보기에는 동기적인 코드로 작성하지만 작동은 비동기적으로 사용할 수 있는 패턴이있다. 마찬가지로 이것은 Promise를 반환할 때 사용하며, 내부적으로 비동기처리에 대한 사이클이 달라지는 것은 아니지만 다른 프로그래밍 언어의 코드 진행처럼 비동기처리를 작성할 수 있다.

 

 

async & await

 

async

export default class Api {
  ...

  async request<AjaxResponse>(): Promise<AjaxResponse> {
    const response = await fetch(this.url);
    return await response.json() as AjaxResponse;
  }
}

export class NewsFeedApi extends Api {
  ...

  async getData(): Promise<NewsFeed[]> {
    return this.request<NewsFeed[]>();
  }
}

export class NewsDetailApi extends Api {
  ...

  async getData(): Promise<NewsDetail> {
    return this.request<NewsDetail>();
  }
}

사용하는 함수나 메소드에 async 키워드를 사용하고 해당 함수나 메소드는 await으로 받아서 사용할 수 있다.

 

await

export default class NewsDetailView extends View {
  ...

  render = async (id: string): Promise<void> => {
    const api = new NewsDetailApi(id);
    
    const { title, content, comments } = await api.getData();

    this.store.makeRead(Number(id));
    this.setTemplateData('currentPage', this.store.currentPage.toString());
    this.setTemplateData('title', title);
    this.setTemplateData('content', content);
    this.setTemplateData('comments', this.makeComment(comments));

    this.updateView();
  }
  
  ...
}

export default class NewsFeedView extends View {
  ...

  render = async (page: string = '1'): Promise<void> => {
    this.store.currentPage = Number(page);

    if (!this.store.hasFeeds) {
      this.store.setFeeds(await this.api.getData());
    }

    for(let i = (this.store.currentPage - 1) * 10; i < this.store.currentPage * 10; i++) {
      const { id, title, comments_count, user, points, time_ago, read } = this.store.getFeed(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(this.store.prevPage));
    this.setTemplateData('next_page', String(this.store.nextPage));
  
    this.updateView();
  }
}

마찬가지로 비동기 처리를 위한 await을 사용하는 함수는 async로 선언되어 있고 그 함수는 Promise를 반환한다. await은 순서대로 코드를 작성해도 차례대로 비동기로 실행된다.

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

XHR to Fetch & Promise

 

지금까지 작성한 모든 소스코드는 동기적 방식으로 작성했다. 현재 해커뉴스 클라이언트의 API를 통해서 json형태의 데이터를 받아온다. 

 

export default class Api {
  ...
  
  getRequest<AjaxResponse>(): AjaxResponse {
    this.ajax.open('GET', this.url, false);
    this.ajax.send();

    return JSON.parse(this.ajax.response) as AjaxResponse;
  }
  
  ...
}

 

Api 클래스의 getRequest 메서드는 XHR 형태로 데이터를 받아오지만 동기적으로 받아온다. 그렇다는 것은 데이터의 양이 많거나 서버의 통신이 조금 느릴 때, 해당 작업을 처리하는 시간동안 만들어놓은 시스템이 잠시 멈추는 듯한 느낌을 받을 수 있고 실제로 그 처리 작업을 하는 동안 그 뒤의 작업은 수행하지 못함으로 화면의 처리가 유동적이지 못하게 느낄 수 있다.

 

하여 기존의 방식을 비동기 형식으로 수정할 것이다.

export default class Api{
  
  ...
  
  getRequestWithXHR<AjaxResponse>(cb: (data: AjaxResponse) => void): void {
    this.xhr.open('GET', this.url);
    this.xhr.addEventListener('load', () => {
      cb(JSON.parse(this.xhr.response) as AjaxResponse);
    });

    this.xhr.send();
  }

  getRequestWithPromise<AjaxResponse>(cb: (data: AjaxResponse) => void): void {
    fetch(this.url)
      .then(response => response.json())
      .then(cb)
      .catch(() => {
        console.error('데이타를 불러오지 못했습니다.');
      });
  }
  
  ...
}

 

위 방식은 기존의 동기적인 XHR 형식에서 비동기적인 XHR 형식으로 전환한 것과 Promise를 반환하는 패턴인 fetch Api를 사용했다. XHR 형식은 비동기적 처리를 할 때, return을 반환할 수 없으므로 ( return이 반환 되었을 시에 이미 send를 통해 작업이 끝남, 처리 순서가 뒤바뀜) callback 함수를 통하여 해당 작업이 끝났을 시에 작동하도록 처리 결과를 넘겨주었다.

 

fetch 형식은 Promise를 반환하며, Promise는 하나의 디자인패턴으로, 비동기처리를 위한 콜백 함수를 사용할때, 콜백 함수의 가시적인 면에서 이점이 있으며, json의 데이터를 object 형태로 변환하는 과정 등의 처리에 있어 이점이 있다.

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

안전한 전역 상태 관리

 

기존의 app.ts로 만든 하나의 파일에 페이지의 모든 기능을 작성했다. 이번에는 하나로 작성된 ts 파일을 기능 단위로 여러개의 ts파일로 구분할 것이다. 페이지의 역할을 하는 파일, 타입을 지정해주는 파일, 공통적으로 사용되는 파일, 라우팅 기능을 하는 파일 등으로 구분하는 폴더 구조를 갖는다.

 

구조를 정하여 관리하게 되면 어느 부분에서 에러, 오류 등이 발생 했을 때, 해당하는 파일에서 사용된 부분만을 수정하면 되고, 기능을 사용하는 부분은 사용 되어지는 파일의 세부적인 부분까지 몰라도 기능을 사용할 수 있지만 너무 세세하게 나눠진 구조는 오히려 구조를 더 복잡하게 할 수도 있다.

 

// (src/app.ts)

import Router from "./core/router";
import { NewsFeedView, NewsDetailView } from "./page";
import Store from "./store";

const store = new Store();
const router: Router = new Router();
const newsFeedView = new NewsFeedView('root', store);
const newsDetailView = new NewsDetailView('root', store);

router.setDefaultPage(newsFeedView);

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

router.route();

 

// (src/config.ts)

export const NEWS_URL :string = 'https://api.hnpwa.com/v0/news/1.json'
export const CONTENT_URL :string = 'https://api.hnpwa.com/v0/item/@id.json';

 

// (src/store.ts)

import { NewsFeed, NewsStore } from "./types";

export default class Store implements NewsStore{
    private feeds: NewsFeed[];
    private _currentPage: number;

    constructor(){
        this.feeds = []
        this._currentPage = 1;
    }

    get currentPage(){
        return this._currentPage;
    }

    set currentPage(page: number){
        this._currentPage = page;
    }

    get nextPage(): number{
        return this._currentPage + 1;
    }

    get prevPage(): number{
        return this._currentPage > 1 ? this._currentPage - 1 : 1;
    }

    get numberOfFeed(): number{
        return this.feeds.length;
    }

    get hasFeeds(): boolean{
        return this.feeds.length > 0;
    }

    getAllFeeds(): NewsFeed[]{
        return this.feeds;
    }

    getFeed(position: number): NewsFeed{        
        return this.feeds[position];
    }

    setFeeds(feeds: NewsFeed[]): void{
        this.feeds = feeds.map((feed) => ({
            ...feed,
            read: false
        }));
    }

    makeRead(id: number): void{
        const feed = this.feeds.find((feed: NewsFeed) => feed.id === id);
        
        if(feed){
            feed.read = true;
        }
    }
}

 

// (src/core/api.ts)

import { NewsFeed, NewsDetail } from '../types';

export class Api{
    url: string;
    ajax: XMLHttpRequest;
    
    constructor(url :string) {
        this.url = url;
        this.ajax = new XMLHttpRequest();
    }

    protected getRequest<AjaxResponse>() :AjaxResponse {
        this.ajax.open('GET', this.url, false); // method, Url, async
        this.ajax.send();
    
        return JSON.parse(this.ajax.response);    
    }

}

export class NewsFeedApi extends Api{
    constructor(url: string) {
        super(url);
    }

    getData(): NewsFeed[]{
        return this.getRequest<NewsFeed[]>();
    }
}

export class NewsDetailApi extends Api{
    constructor(url: string) {
        super(url);
    }
    
    getData(): NewsDetail{
        return this.getRequest<NewsDetail>();
    }
}

 

// (src/core/router.ts)

import {RouteInfo} from '../types';
import View from './view';

export default class Router{
    routeTable: RouteInfo[];
    defaultRoute: RouteInfo | null;

    constructor(){        
        window.addEventListener('hashchange', this.route.bind(this));
        
        this.routeTable = [];
        this.defaultRoute = null;
    }
    
    addRoutePath(path: string, page: View): void{
        this.routeTable.push({path, page});
    }
    
    setDefaultPage(page: View): void{
        this.defaultRoute = {path: '', page}
    }
    
    route() {
        const routePath = location.hash;

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

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

 

// (src/core/view.ts)

export default abstract class View {
    private template :string;
    private rederTemplate: 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.rederTemplate = template;
        this.htmlList = [];
    }

    protected updateView() :void {        
        this.container.innerHTML = this.rederTemplate;
        this.rederTemplate = this.template;
    }

    protected addHtml(htmlString: string): void {
        this.htmlList.push(htmlString);
    }

    protected getHtml(): string{
        const snapshot = this.htmlList.join('');
        this.clearHtmlList();
        return snapshot;
    }

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

    private clearHtmlList(): void{
        this.htmlList = [];
    }

    abstract render(id: string): void;
}

 

// (src/page/index.ts)

export {default as NewsDetailView} from './news-detail-view';
export {default as NewsFeedView} from './news-feed-view';

 

// (src/core/news-detail-view.ts)

import View from '../core/view';
import { NewsDetailApi } from '../core/api';
import { NewsDetail, NewsComments, NewsStore } from '../types';
import { CONTENT_URL } from '../config';

const 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="border rounded-xl bg-white m-6 p-4 ">
            <h2>{{__title__}}</h2>
            <div class="text-gray-400 h-20">
                {{__content__}}
            </div>
            {{__comments__}}
        </div>
    </div>
`;

export default class NewsDetailView extends View{
    private store: NewsStore;
    
    constructor(containerId: string, store: NewsStore){        
        super(containerId, template);
        this.store = store;
    }
    
    render = (id: string): void => {
        const api = new NewsDetailApi(CONTENT_URL.replace('@id', id));
        const {title, content, comments} = api.getData();

        this.store.makeRead(parseInt(id));
        this.setTempateData('comments', this.makeComment(comments))
        this.setTempateData('currentPage', this.store.currentPage.toString());
        this.setTempateData('title', title);
        this.setTempateData('content', content);
                
        this.updateView();
    }

    private makeComment(comments :NewsComments[]) :string{        
        for(let i=0; i<comments.length; i++){
            const comment :NewsComments = 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();
    }
}

 

// (src/core/news-feed-view.ts)

import View from "../core/view";
import { NewsFeedApi } from "../core/api";
import { NewsFeed, NewsStore } from "../types";
import { NEWS_URL } from "../config";

const template = `
    <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>
`;

export default class NewsFeedView extends View{
    private api: NewsFeedApi;
    private store: NewsStore;
    private newsTotalPage :number;
    
    constructor(containerId: string, store: NewsStore){

        super(containerId, template);

        this.store = store;
        this.api = new NewsFeedApi(NEWS_URL);
        this.newsTotalPage = this.store.numberOfFeed / 10;
    
        if(!this.store.hasFeeds){
            this.store.setFeeds(this.api.getData())            
        }        
    }
    
    render = (page: string = '1'): void => {
        this.store.currentPage = Number(location.hash.substring(7) || 1);
        for(let i=(this.store.currentPage - 1) * 10; i<this.store.currentPage * 10; i++){            
            const {id, title, comments_count, user, points, time_ago, read} = this.store.getFeed(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.setTempateData('news_feed', this.getHtml());
        this.setTempateData('prev_page', `${this.store.currentPage > 1 ? this.store.currentPage - 1 : 1}" ${this.store.currentPage === 1 ? 'style="pointer-events: none;"' : " "}`);
        this.setTempateData('next_page', `${this.store.currentPage + 1}" ${this.store.currentPage === this.newsTotalPage ? 'style="pointer-events: none;"' : " "}`);    
    
        this.updateView();
    }
}

 

// (src/type/news-feed-view.ts)

import View from "../core/view";

export interface NewsStore{
    getAllFeeds: () => NewsFeed[];
    getFeed: (position: number) => NewsFeed;
    setFeeds: (feeds: NewsFeed[]) => void;
    makeRead: (id: number) => void;
    hasFeeds: boolean;
    currentPage: number;
    numberOfFeed: number;
    nextPage: number;
    prevPage: number;
}

export interface Store {
    currentPage :number, 
    feeds :NewsFeed[]
}

export interface News {
    readonly id :number,
    readonly url :string,
    readonly user :string,
    readonly time_ago :string,
    readonly title :string,
    readonly content :string
}

export interface NewsFeed extends News{    
    readonly comments_count :number,    
    readonly points :number,    
    read? :boolean
}

export interface NewsDetail extends News{    
    readonly comments :NewsComments[]
}

export interface NewsComments extends News{
    readonly comments: NewsComments[],
    readonly level :number
}

export interface RouteInfo {
    path: string;
    page: View;
}

 

이번 파트에서는 지난번에 모든 작업들을 클래스로 만들어 구조화 했는데 그 작업한 결과를 각 영역별로 폴더를 분할하고 import & export를 통해 클래스들을 관리한다.

 

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

뷰 클래스로 코드 구조 개선

 

기존에 함수(function)형태로 작성한 코드들을 ES6의 클래스(class)형태로 바꾼다. function을 class로 바꾸면서 기존의 각 기능 등을 정의하는 함수만으로 정의되어 있는데, 각각의 함수들을 공통적인 부분으로 인스턴스화 할 수 있고, 공통적인 부분 들은 공통적인 클래스로 정의하고 상속을 받아서 사용 할 수 있는 구조를 만들 수 있다.

 

또한 클래스의 특징으로 구조와 기능은 같지만 값만 다른 여러개의 인스턴스를 복제하듯이 만들어 낼 수 있다. (화면의 동일한 컴포넌트들을 값만 다르게 생성할 수 있음)

interface Store {
    currentPage :number, 
    feeds :NewsFeed[]
}

interface News {
    readonly id :number,
    readonly url :string,
    readonly user :string,
    readonly time_ago :string,
    readonly title :string,
    readonly content :string
}

interface NewsFeed extends News{    
    readonly comments_count :number,    
    readonly points :number,    
    read? :boolean
}

interface NewsDetail extends News{    
    readonly comments :NewsComments[]
}

interface NewsComments extends News{
    readonly comments: NewsComments[],
    readonly level :number
}

interface RouteInfo {
    path: string;
    page: View;
}

const ajax :XMLHttpRequest = new XMLHttpRequest();
const NEWS_URL :string = 'https://api.hnpwa.com/v0/news/1.json'
const CONTENT_URL :string = 'https://api.hnpwa.com/v0/item/@id.json';
const store :Store = {
    currentPage: 1,
    feeds: []
}

class Api{
    url: string;
    ajax: XMLHttpRequest;
    
    constructor(url :string) {
        this.url = url;
        this.ajax = new XMLHttpRequest();
    }

    protected getRequest<AjaxResponse>() :AjaxResponse {
        ...    
    }

}

class NewsFeedApi extends Api{
    getData(): NewsFeed[]{
        ...
    }
}

class NewsDetailApi extends Api{
    getData(): NewsDetail{
        ...
    }
}

abstract class View {
    private template :string;
    private rederTemplate: 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.rederTemplate = template;
        this.htmlList = [];
    }

    protected updateView() :void {        
        ...
    }

    protected addHtml(htmlString: string): void {
        ...
    }

    protected getHtml(): string{
        ...
    }

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

    private clearHtmlList(): void{
        this.htmlList = [];
    }

    abstract render(): void;
}

class Router{
    routeTable: RouteInfo[];
    defaultRoute: RouteInfo | null;

    constructor(){        
        window.addEventListener('hashchange', this.route.bind(this));
        
        this.routeTable = [];
        this.defaultRoute = null;
    }
    
    addRoutePath(path: string, page: View): void{
        this.routeTable.push({path, page});
    }
    
    setDefaultPage(page: View): void{
        this.defaultRoute = {path: '', page}
    }
    
    route() {
        ...
    }
}

class NewsFeedView extends View{
    private api: NewsFeedApi;
    private feeds: NewsFeed[];
    private newsTotalPage :number;
    
    constructor(containerId: string){
        ...

        super(containerId, template);

        this.api = new NewsFeedApi(NEWS_URL);
        this.feeds = store.feeds;
        this.newsTotalPage = this.feeds.length / 10;
    
        if(this.feeds.length === 0){
            this.feeds = store.feeds = this.api.getData();
            this.makeFeeds();
        }
        
    }
    
    render(): void {
        store.currentPage = Number(location.hash.substring(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(...);
        }
    
        this.setTempateData('news_feed', this.getHtml());
        this.setTempateData('prev_page', `${store.currentPage > 1 ? store.currentPage - 1 : 1}" ${store.currentPage === 1 ? 'style="pointer-events: none;"' : " "}`);
        this.setTempateData('next_page', `${store.currentPage + 1}" ${store.currentPage === this.newsTotalPage ? 'style="pointer-events: none;"' : " "}`);    
    
        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){        
        ...
        super(containerId, template);
    }
    
    render(): void{
        const id = location.hash.substring(7);
        const api = new NewsDetailApi(CONTENT_URL.replace('@id', id));
        const newsDetail = api.getData();

        for(let i=0; i<store.feeds.length; i++){
            if(store.feeds[i].id === parseInt(id)){
                store.feeds[i].read = true;
                break;
            }
        }
        this.setTempateData('comments', this.makeComment(newsDetail.comments))
        this.setTempateData('currentPage', String(store.currentPage));
        this.setTempateData('title', newsDetail.title);
        this.setTempateData('content', newsDetail.content);
                
        this.updateView();
    }

    private makeComment(comments :NewsComments[]) :string{        
        ...
    }
}

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();

 

이번 장에선 지금까지 구현한 코드들의 구조를 개선했다. 함수로 구현했던 부분을 전부 es6의 클래스 형태로 바꿨고 목록이나 내용을 보여주는 클래스는 공통적으로 처리하는 부분을 View 클래스를 통해 상속을 받아 구현 했고 ajax를 통해 데이터를 불러오는 부분은 Api 클래스를 상속 받아 만듦으로써 구조화 시켰으며, 라우팅 처리해주는 부분 역시 클래스화 했다.

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

상속과 믹스인

 

코드를 작성함에 있어서 중복된 코드가 발생을 하고 그 중복된 코드를 변수나 함수로 만들지만 그것 또한 한계가 있다. 따라서 코드를 목적에 맞게 범주로 나눠서 분류하면서 중복된 코드를 관리 할 수 있다. 그 방법으로 상속을 사용할 것이다.

 

공통 요소로 분류하고 공통 요소에서 확장할 수 있도록 개별 요소를 만드는 식으로 상속을 사용하면 된다.

상속을 사용하는 매커니즘은

1) 클래스 사용

 

중복을 제거하기 위해 클래스를 사용했지만 중복된 코드를 제거하려다 코드가 더 무거워질 수도 있다. 코드 자체가 하는 일이 작은 경우에 그런건데 클래스의 경우는 단순 함수에서 어떠한 구조를 갖는 것이다. 구조를 갖는다는 건 목적을 위한 형식을 갖는 것이고 그럼에서 나중에 클래스가 더 많은 기능을 가지게 될 때 초기의 복잡도는 유지되면서 사용할 때의 단순함은 유지될 수 있다.

 

2) 믹스인 사용

 

기존의 extends의 상속 방법은 상속의 관계를 바꾸고싶다면 코드를 바꿔야한다. 또 class 문법은 다중상속을 지원하지 않는다. 그럴 때 이 믹스인 기법을 사용한다.

 

...

function applyApiMixins(targetClass: any, baseClasses :any[]) :void{
    baseClasses.forEach(baseClass => {
        Object.getOwnPropertyNames(baseClass.prototype).forEach(name => {
            const descriptor = Object.getOwnPropertyDescriptor(baseClass.prototype, name);

            if(descriptor){
                Object.defineProperty(targetClass.prototype, name, descriptor);
            }
        })
    })
}

class Api{
    getRequest<AjaxResponse>(url :string) :AjaxResponse {
        const ajax = new XMLHttpRequest();
        ajax.open('GET', url, false); // method, Url, async
        ajax.send();
    
        return JSON.parse(ajax.response);    
    }
}

class NewsFeedApi{
    getData(): NewsFeed[]{
        return this.getRequest<NewsFeed[]>(NEWS_URL);
    }
}

class NewsDetailApi{
    getData(id :string): NewsDetail{
        return this.getRequest<NewsDetail>(CONTENT_URL.replace('@id', id));
    }
}

interface NewsFeedApi extends Api {};
interface NewsDetailApi extends Api {};

applyApiMixins(NewsFeedApi, [Api]);
applyApiMixins(NewsDetailApi, [Api]);

function newsFeed() :void{
    const api = new NewsFeedApi();
    let newsFeed: NewsFeed[] = store.feeds;
    const newsTotalPage = newsFeed.length / 10;
    const newsList = [];
    ... 
}

function newsDetail() :void{
	...
}

 

코드의 특성상 class의 extends를 사용하는 것만으로도 해결되는 부분이 많기때문에 대부분 믹스인까지 사용하기보다 클래스의 extends를 사용하지만 어떤 유형은 코드베이스의 유연성이 굉장히 많이 필요하기 때문에 extends만으로 부족한 부분은 믹스인을 사용한다.

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

타입과 인터페이스

 

타입스크립트는 type(타입 알리아스)말고도 interface로도 타입을 지정해 줄 수 있다. 타입과 인터페이스는 대체로 비슷하나 근소한 차이가 있다. 

 

1) 타입을 결합하거나 조합하는 방식의 차이

 - 확장되는 형식의 타입에서는 인터페이스를 선호한다.

 - type이 제공하는 유니온(|)타입은 인터페이스가 지원하지 않음 (인터섹션(&)은 가능)

 - 그 외에는 인터페이스를 사용하는 경향이 있다

 

interface Store {
    currentPage :number, 
    feeds :NewsFeed[]
}

interface News {
    readonly id :number,
    readonly url :string,
    readonly user :string,
    readonly time_ago :string,
    readonly title :string,
    readonly content :string
}

interface NewsFeed extends News{    
    readonly comments_count :number,    
    readonly points :number,    
    read? :boolean
}

interface NewsDetail extends News{    
    readonly comments :NewsComments[]
}

interface NewsComments extends News{
    readonly comments: NewsComments[],
    readonly level :number
}

const container: HTMLElement | null = document.querySelector('#root');
const ajax :XMLHttpRequest = new XMLHttpRequest();
const NEWS_URL :string = 'https://api.hnpwa.com/v0/news/1.json'
const CONTENT_URL :string = 'https://api.hnpwa.com/v0/item/@id.json';
const store :Store = {
    ...
}

function getData<AjaxResponse>(url :string) :AjaxResponse {
    ...
}

function makeFeeds(feeds :NewsFeed[]) :NewsFeed[]{
    ...
}

function updateView(html :string) :void{
    ...
}

function newsFeed() :void{
    ...
}

function newsDetail() :void{
    ...
}

function makeComment(comments :NewsComments[]) :string{
    ...
}

function router() :void{
    ...
}

window.addEventListener('hashchange', router);

router();

 

인터페이스의 경우엔 타입 알리아스와 기능적으로는 아주 유사하지만 사용 방식(=의 유무)이나 사용하는 목적의 차이가 있다. 인터섹션(&)을 사용하는 타입 알리아스와 달리 인터페이스는 extends 키워드를 사용하여 타입간의 관계를 확장하는 형식을 나타낸다. 확장되는 형식의 타입에는 인터페이스가 주로 사용된다고 한다. 하지만 인터페이스에는 유니온(|)을 사용할 수 없으므로 유니온(|)을 사용해야 하는 경우는 필수적으로 타입 알리아스를 사용해야 한다.

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

함수의 규격 작성하기

 

타입스크립트는 중복되는 타입의 경우 공통의 타입을 만들어서 사용할 수 있고 변수말고도 함수에도 타입을 지정할 수 있다.

 

type Store = {
    currentPage :number, 
    feeds :NewsFeed[]
}

type News = {
    id :number,    
    url :string,
    user :string,
    time_ago :string,
    title :string,
    content :string
}

type NewsFeed = News & {    
    comments_count :number,    
    points :number,    
    read? :boolean
}

type NewsDetail = News & {    
    comments :NewsComments[]
}

type NewsComments = News & {
    comments: NewsComments[],
    level :number
}

const container: HTMLElement | null = document.querySelector('#root');
const ajax :XMLHttpRequest = new XMLHttpRequest();
const NEWS_URL :string = 'https://api.hnpwa.com/v0/news/1.json'
const CONTENT_URL :string = 'https://api.hnpwa.com/v0/item/@id.json';
const store :Store = {
    currentPage: 1,
    feeds: []
}

function getData<AjaxResponse>(url :string) :AjaxResponse {
    ...
}

function makeFeeds(feeds :NewsFeed[]) :NewsFeed[]{
    ...
}

function updateView(html :string) :void{
    ...
}

function newsFeed() :void{
    let newsFeed: NewsFeed[] = store.feeds;
    const newsTotalPage = newsFeed.length / 10;
    const newsList = [];  

    if(newsFeed.length === 0){
        newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
    }
    
    for(let i=(store.currentPage - 1) * 10; i<store.currentPage * 10; i++){
        ...
    }
    ...
}

function newsDetail() :void{
    const id = location.hash.substring(7);
    const newsContent = getData<NewsDetail>(CONTENT_URL.replace('@id', id));
    ...
}

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

function router() :void{
	...
}

window.addEventListener('hashchange', router);

router();

 

GET으로 받아오는 ajax의 타입을 newsFeed type으로 지정했었는데 NewsFeed, NewsDetail, NewsContents 등 API형식을 다르게 써야하는 경우가 있다. 세가지 타입 각각 따로 type을 지정해도 되지만 공통적으로 쓰이는 부분이 있기 때문에 그 부분을 따로 만들어도 된다.

 

News라는 type에 공통적인 값들의 타입을 지정하고 &(인터섹션)를 통해 추가적으로 필요한 부분은 새로운 type (NewsFeed, NewsDetail, NewsContents 등)으로 정의하여 사용하면 된다.

 

 

vsCode의 익스텐션에 'REST Client'라는 것을 추가하면 api 결과를 좀 더 편하게 볼 수 있다.

###
GET https://api.hnpwa.com/v0/news/1.json HTTP/1.1

###
GET https://api.hnpwa.com/v0/item/29816504.json HTTP/1.1

 

postman처럼 사용할 수 있는 것 같다...

 

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

변수에 타입 지정하기

 

타입스크립트의 타입은 primitive 타입과 object타입으로 나눌 수 있다. primitive 타입은 string, number, boolean 등 원시 타입이고, object타입은 객체 그 자체는 모든 것이 타입이 될 수 있기 때문에 수 없이 많다.

 

type Store = {
    currentPage :number, 
    feeds :NewsFeed[]
}

type NewsFeed = {
    id :number,
    comments_count :number,
    url :string,
    user :string;
    time_ago :string,
    points :number,
    title :string,
    read? :boolean
}

const container: HTMLElement | null = document.querySelector('#root');
const ajax :XMLHttpRequest = new XMLHttpRequest();
const NEWS_URL :string = 'https://api.hnpwa.com/v0/news/1.json'
const CONTENT_URL :string = 'https://api.hnpwa.com/v0/item/@id.json';
const store :Store = {
	...
}

function getData(url){
    ...
}

function makeFeeds(feeds){
    ...
}

function updateView(html){
   ...
}

function newsFeed(){
    let newsFeed: NewsFeed[] = store.feeds;
    ...    
}

function newsDetail(){
    ...

    function makeComment(comments, called = 0){
        ...
    }
}

function router(){
    ...
}

window.addEventListener('hashchange', router);

router();

 

먼저 제일 상단에 있는 const에서 선언한 변수들의 타입을 지정했다. dom요소에도 타입을 지정할 수 있는데 dom요소는 대부분 Element 타입과 null 타입이 공존하기 때문에 null 체크를 통한 타입가드를 해주어야한다. updateView() 함수가 id가 root인 dom요소의 타입가드를 해주는 로직의 함수이다. 또 ajax는 XMLHttpRequest 타입을 지정했다.

 

store객체의 currentPage(number), feeds(Array)의 타입을 지정하기 위하여 Store라는 타입을 새로 만들었는데 Store타입 안의 feeds 배열의 타입 또한 지정 해줄 수 있다. feeds 배열의 타입은 getData() 함수를 통해 API의 규격에 맞게 가져온 객체의 형식을 NewsFeed라는 타입으로 지정했다.

 

 

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #김민태의프론트엔드아카데미:제1강JavaScript&TypeScriptEssential

+ Recent posts