TIL.[Ts] TS 상속(1) 클래스 상속

배상건·2022년 9월 27일
0

TIL

목록 보기
12/15
post-thumbnail

상속 개념

이전 피드에서 News라는 인터페이스를 기반으로 NewsFeed를 만들고, NewsDetail를 만들고, NewsComment를 만들었습니다.
이런 식으로 공통 요소를 만들고, 공통 요소를 확잘 할 수 있는 개별 요소를 만드는 것이 상속의 개념입니다.

코드를 작성할 때, 목적에 부합되도록 분류하고, 그 분류 내에서 훨씬 더 의미를 잘 부여할 수 있게, 중복된 코드를 제거한다면, 코드 가독성에 기여할 수 있습니다.
이를 위한 첫번째 방법이 클래스입니다.

코드 분석

// 네트워크를 통해서 API를 호출하는 함수 getData
function getData<AjaxResponse>(url: string): AjaxResponse {
  ajax.open('GET', url, false);
  ajax.send();

  return JSON.parse(ajax.response);
}

// 뷰와 관련된 업데이트를 처리하는 함수 makeFeeds, 함수 updateView
function makeFeeds(feeds: NewsFeed[]): NewsFeed[] {
  for (let i = 0; i < feeds.length; i++) {
    feeds[i].read = false;
  }

  return feeds;
}

function updateView(html: string): void {
  if (container) {
    container.innerHTML = html;
  } else {
    console.error('최상위 컨테이너가 없어 UI를 진행하지 못합니다.');
  }
}
// 메인 뷰를 처리하는 함수 NewsFeed, 함수 NewsDetail
function newsFeed(): void {
  let newsFeed: NewsFeed[] = store.feeds;
  const newsList = [];
  let template = `
	<div class="bg-gray-600 min-h-screen">
		...
	</div>
	`
  if (newsFeed.length === 0) {
      newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
    }
  ...
  updateView(template);
}

function newsDetail(): void {
  const id = location.hash.substring(7);
  const newsContent = getData<NewsDetail>(CONTENT_URL.replace('@id', id))
  let template = `...`
  ...
}
  1. 네트워크를 통해서 API를 호출하는 함수 getData
  2. 뷰와 관련된 업데이트를 처리하는 함수 makeFeeds, 함수 updateView
  3. 메인 뷰를 처리하는 함수 NewsFeed, 함수 NewsDetail

해당 피드에서는 공통요소로 작용하는 함수 getData에 집중하도록 하겠습니다.

문제 제기

API를 호출하는 함수 getData를 사용하는 입장인 함수 NewsFeed, 함수 NewsDetail에서 getData라는 명칭만으로는 정확히 어떤 타입의 데이터를 가져오는지 의미를 갖고 있지 않습니다.

function newsFeed(): void {
	newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
}

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

즉, 공통함수를 사용하는 입장에서 좀 더 의미화 가 필요합니다.

클래스 상속으로 해결

  1. 함수 NewsFeed, 함수 NewsDetail 에서 공통요소는, 네트워크를 통해서 API를 호출하는 getDtat 입니다.
  2. 따라서, 클래스를 선언하고 getData의 기능을 할당합니다.
  3. 이때, API를 호출할 때 필요한 것들은, 함수 getData를 참고하여, url과 XMLHttpRequest의 인스턴스를 만드는 코드가 필요합니다. 이를 생성자에 추가합니다.
  4. 이때, class Api는 외부로부터 URL를 받고, 어떤 URL를 갖는 API더라도 호출을 할 수 있어야 합니다. 따라서, 생성자 함수의 인자로 url을 전달합니다.
  5. 인스턴스 객체에 접근하는 지시어인 this를 사용하여, 클래스의 내부 요소인 url과 ajax로 접근합니다.
class Api {
    url: string;
    ajax: XMLHttpRequest;
    
	constructor(url: string) {
      this.url = url;
      this.ajax = new XMLHttpRequest();
    }
}
  1. 작성된 공통요소인 Api 클래스를 확장하기 위해, 함수 NewsFeed, 함수 NewsDetail 를 각각 클래스로 만듭니다.
  2. Api 클래스를 확장해서 만든 NewsFeedApi 클래스가 필요한 속성인 함수getData를 선언해줍니다.
  3. 동일한 방법으로 NewsDetailApi 클래스를 선언합니다.
class NewsFeedApi extends Api {
  getData(): NewsFeed[] {
  	ajax.open('GET', url, false);
    ajax.send();
    
    return JSON.parse(ajax.response);
  }
}

class NewsDetailApi extends Api {
  getData(): NewsDetail {
  	ajax.open('GET', url, false);
    ajax.send();
    
    return JSON.parse(ajax.response);
  }
}
  1. 확장 된 NewsFeedApi와 NewsDetailApi가 getData의 기능을 공통요소로 가지고 있습니다. 따라서, 이 공통요소를 Api 클래스로 끌어올립니다. 이를 통해 확장 된 NewsFeedApi와 NewsDetailApi에 해당 코드들을 제거할 수 있습니다.
  2. 공통요소는 코드의 묶음이므로 함수(getRequest)로 작성해줍니다.
  3. 공통요소로 작성된 함수 getRequest의 ajax와 url은 모두 Api 클래스의 내부 요소로 작성되어 있기 때문에 this 로 바꿔줍니다.
class Api {
  ajax: XMLHttpRequest;
  url: string;

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

  getRequest<AjaxResponse>(): AjaxResponse {
    this.ajax.open("GET", this.url, false);
    this.ajax.send();

    return JSON.parse(this.ajax.response);
  }
}
  1. 이제 수정된 Api 클래스를 NewsFeedApi와 NewsDetailApi로 확장해줍니다. 이때, 확장된 클래스의 getData에는 상위 클래스인 Api의 getRequest를 호출한 값을 가져오면 됩니다.
class NewsFeedApi extends Api {
  getData(): NewsFeed[] {
    return this.getRequest<NewsFeed[]>();
  }
}

class NewsDetailApi extends Api {
  getData(): NewsDetail {
    return this.getRequest<NewsDetail>();
  }
}
  1. 확장된 클래스를 적용합니다. 이때, api라는 클래스 인스턴스를 만들어주었습니다.
function newsFeed(): void {
   const api = new NewsFeedApi(NEWS_URL);
  ...
	newsFeed = store.feeds = makeFeeds(api.getData<NewsFeed[]>(NEWS_URL));
}

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

클래스 메소드 노출 문제 발생

  • Api 가 제공하는 getData 를 기존의 getData로 바꿀 때, getData와 getRequest 라는 두가지 메소드가 등장합니다.
  • getDtat 메소드는 기존의 getData 함수와 동일하게 바깥쪽에서 API 호출을 위해서 사용하는 용도로 만들어진 메소드입니다. 따라서,
  • getRequest 메소드는 Api 클래스를 확장한 하위 클래스에서 사용할 용도로 만들어진 메소드입니다.
  • 따라서, getRequest 메소드는 Api 클래스 외부에서 사용되지 않아야 합니다.
  • 그러나, 에디터에서는 이를 그대로 노출하고 있기 때문에 사용될 수 있습니다. 따라서, 에디터에서 이를 노출시키지 않게 해야합니다.
  • 이러한 문제를 해결하기 위해 다음과 같은 방법을 사용하면 됩니다.

클래스 메소드 노출 문제 해결

  1. 타입스크립트에서는 class의 속성과 메소드를 외부로 노출시키지 않는 지시어를 제공합니다.

바로, Protected입니다.

  1. 이렇게 속성명 앞에 Protecte를 지정해주면, 해당 속성은 class 외부에서 인스턴스 객체로 등장하지 않게 됩니다.
class Api {
  ajax: XMLHttpRequest;
  url: string;

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

  protected getRequest<AjaxResponse>(): AjaxResponse {
    this.ajax.open("GET", this.url, false);
    this.ajax.send();

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

profile
목표 지향을 위해 협업하는 개발자

0개의 댓글