코드를 작성함에 있어서 중복된 코드가 발생을 하고 그 중복된 코드를 변수나 함수로 만들지만 그것 또한 한계가 있다. 따라서 코드를 목적에 맞게 범주로 나눠서 분류하면서 중복된 코드를 관리 할 수 있다. 그 방법으로 상속을 사용할 것이다.
공통 요소로 분류하고 공통 요소에서 확장할 수 있도록 개별 요소를 만드는 식으로 상속을 사용하면 된다.
상속을 사용하는 매커니즘은
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만으로 부족한 부분은 믹스인을 사용한다.
인터페이스의 경우엔 타입 알리아스와 기능적으로는 아주 유사하지만 사용 방식(=의 유무)이나 사용하는 목적의 차이가 있다. 인터섹션(&)을 사용하는 타입 알리아스와 달리 인터페이스는 extends 키워드를 사용하여 타입간의 관계를 확장하는 형식을 나타낸다. 확장되는 형식의 타입에는 인터페이스가 주로 사용된다고 한다. 하지만 인터페이스에는 유니온(|)을 사용할 수 없으므로 유니온(|)을 사용해야 하는 경우는 필수적으로 타입 알리아스를 사용해야 한다.
타입스크립트는 중복되는 타입의 경우 공통의 타입을 만들어서 사용할 수 있고 변수말고도 함수에도 타입을 지정할 수 있다.
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
타입스크립트의 타입은 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라는 타입으로 지정했다.
자바스크립트로 만들어진 해커뉴스 페이지를 타입스크립트로 변환하는 작업을 할 것인데 이런 작업을 포팅이라고 한다.
기존에 해커뉴스 페이지는 html파일(index.html)과 js파일(app.js)로 구성되어 있는데 타입스크립트를 사용하기 위해 app.js파일을 app.ts파일로 변환한다. 그리고 tsconfig.json이라는 파일을 생성한다.
- tsconfig.json
{
"compilerOptions": { // 주로 사용하는 부분
"strict": true, // 타입체크
"target": "ES5", // 변환할버전
"module": "commonJS",
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"sourceMap": true, // html과 js, ts 파일 등을 map 형식으로 나타낸다
"downlevelIteration": true
}
}
html은 타입스크립트 파일 자체를 읽어낼 수 없기 때문에 내부적으로 타입스크립트를 자바스크립트로 변환하는 작업이 필요하다. 여기서 config 파일이 그 작업을 도와주고 또 다른 세부 설정도 할 수 있다. parcel 번들러를 실행하고 나면 작업 폴더 내 dist폴더 안에 또 다른 html파일과 js파일이 있는데 형식이 조금 다른 파일이 생성된다. (트랜스파일링이 된 것)
먼저 기존 newsFeed는 getData()를 통해 newsFeed를 호출 할 때마다 ajax로 데이터를 불러 들였기 때문에 공공으로 사용하는 store객체에 feeds라는 배열을 추가하여 store의 안에 있는 feeds 배열로 사용한다. ajax로 불러온 데이터를 store에 저장했기 때문에 전역적으로 데이터를 관리할 수 있으며 데이터의 API 형태나 필요한 데이터 구조를 추가할 수 있다. 데이터 읽음 처리를 위해 makeFeed() 함수를 통해 read라는 boolean값 데이터를 추가하여 글을 조회하지 않았을 시엔 false, 조회 하였을 땐 true 값으로 구분한다.
지금까지 만들어진 해커뉴스의 디자인적인 부분은 tailwindCss를 가지고 작업을 할것이다. 그렇게 하기 전에 기존에 있는 해커뉴스의 코드는 template을 가지고 구조적인 부분을 수정했지만 아직도 문제가 있다.
template의 <ul>, <li> 태그들은 아직도 반복문을 따로 사용해서 작업하는 부분이나 template의 동적인 데이터를 추가해야 하는 부분은 데이터의 변경이 있을때마다 replace함수를 써 줘야하기 때문에 여러가지 기능들이 추가되면 replace 함수를 그만큼 추가로 사용해야 하는 문제점이 있다. 따라서 템플릿엔진(ejs, pug, nunjucks, handlebars 등)을 사용할 것이다.
(확연히 기존 template 변수에 사용된 html 태그보다 스타일링을 통해 내용이 길어짐)
각 목록 화면과 내용 화면에 UI 부분을 tailwindCss를 사용해서 디자인을 수정했다. html 태그의 클래스를 보게되면 css의 속성(display: flex -> flex, justify-content: space-between -> justify-between 등)을 tailwindCss만의 방식으로 클래스명을 사용하여 스타일링한 것을 알 수 있다.
해커뉴스의 내용엔 comment 데이터가 있는데 comment안에 comment가 있는 형식으로 단계가 정해져 있지 않은 구조이다. comment 부분은 makeComment() 함수를 통해 배열 안의 배열을 재귀 호출을 통해서 조회한다. comment배열 안의 comment가 있는지 조건문으로 처리하고 comment안의 배열이 없을 때 해당하는 값을 return 한다.
UI를 변경하기 전에 기존 DOM API를 사용했을 때보다 훨씬 더 보기 직관적이고 개발자도구의 Element와 구조가 비슷한 형태로 어느 부분에 어떤 데이터가 들어갈지 보다 잘 확인할 수 있는 구조가 되었다. 또한 변경이 필요한 부분이라면 어느 부분에서 변경이 일어나는지 한 눈으로 알 수있고 그것이 데이터이던지 화면을 구성하는 코드이던지 변경에 용이하다.
tailwind css는 CSS 프레임워크의 일종으로 부트스트랩과 비슷하지만 다소 차이가 있는것 같다. 사용법은 mt-1, ml-6 등 tailwind css 사이트에 있는 컴포넌트나 레이아웃 등을 참고하며 사용할 수 있다. 부트스트랩과 같은 다른 프레임워크들에 비해 기본 스타일 값을 사용하고 필요하다면 디테일하게 커스텀이 가능하다. 부트스트랩의 경우는 완성된 디자인의 컴포넌트를 갖다 쓰는 느낌이지만 tailwind css는 기본 스타일 값을 수정하고 수정하고 나서도 디자인의 일관성을 망치지 않을 수 있다. css-in-js 와는 컨셉이 확연히 다르다. 하지만 단점으로는 html 태그의 class가 하염없이 길어질 수도 있으며 css-in-js의 장점인 어떤 변수에따라 다른 값을 줄 수 있는 스타일링 등은 하기 힘들다.