코드 migration 하기(JS를 TS 로)

dev__bokyoung·2022년 9월 2일
0

해커뉴스 클론코딩

목록 보기
8/12
post-thumbnail

프롤로그

이때까지 js 만들었던 hacker news 를 typescript 로 변환하고자 한다.
어떤 부분들이 바뀌는 지 어떻게 바뀌는지 흐름을 따라 가 보면 좋을 듯 하다.

1. 환경 설정 설명

간단한 컴파일 옵션들 - tsconfig.json

{
  "compilerOptions": {
    "strict": true, // 본격적으로 타입스크립트로 변환하겠다라고 했을 때 엄격모드로 설정 해 놓으면 조금 더 세부적으로 변환 가능하다. 
    "target": "ES5", // 어떤 문법을 사용할 것인지 (js 에 사용될 문법 체계를 어떤 버전을 쓸 것이냐) 
    "module": "CommonJS",
    "alwaysStrict": true,
    "noImplicitAny": true, //any 타입을 쓰지 못하도록 한다. (타입을 명확하게 할 수 있도록 한다.) 
    "noImplicitThis": true,
    "sourceMap": true, //개발환경과 소스코드를 일치 시킨다. (관리자도구에서 ts 파일을 볼 수 있다.) 
    "downlevelIteration": true
  } 
}

2. 변수에 타입 변경하기


//타입 알리아스 
type Store = {
  currentPage: number;
  feeds: NewsFeed[]; //NewsFeed 유형의 데이터가 들어가는 배열
}

type NewsFeed = {
  id: number;
  comments_count: number;
  url: string;
  user: string;
  time_ago: string;
  points: number;
  title: string;
  read?: boolean; //optional 한 속성 
}

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

타입 추론 : 누가 봐도 당연한 값들은 typescript 에서 알아서 타입 추론을 해 준다. 따로 타입 지정을 할 필요가 없다. 예를 들면 for 문

3. 타입 가드

const container: HTMLElement | null = document.getElementById('root');

container 같은 경우 타입이 2가지이다. 그래서 이러한 부분들을 따로 타입에 대한 조건을 달아 줘야 한다. 타입 가드
만약에 innerHTML 에 null 이라는 타입이 들어오면 오류가 나게 되니 미리 방지해줘야 한다는 말이다.

중복되는 부분이 있어서 함수로 만들었다. 이와 같이 null 을 체크하는 부분을 타입 가드한다 라고 한다.

function updateView(html) {
  if (container) {
    container.innerHTML = html;
  } else {
    console.error('최상위 컨테이너가 없어 UI를 진행하지 못합니다.');
  }
}

타입가드 는 타입스크립트 내에서 어떤 변수가 2개이상의 타입을 갖게 되는 경우가 있을 때, 코드상에서 a 라는 타입이 들어왔을 때 작동 될 수 없는 코드에 대해서 경고를 해 주거나, 혹은 그것을 원천적으로 막을 수 있는 코드 테크닉 혹은 코딩 방식을 타입가드라고 한다.

4. 함수의 규격 작성하기

타입 알리아스의 공통 속성 사용


// 공통 속성 
type News = {
  id: number;
  time_ago: string;
  title: string;
  url: string;
  user: string;
  content: string;
}

// news 에 대한 공통 속성을 사용하는 방법을 제공한다 &을 이용
type NewsFeed = News & {
  comments_count: number;
  points: number;
  read?: boolean;
}


type NewsDetail =  News & {
  comments: NewsComment[];
}

type NewsComment = News & {
  comments: NewsComment[];
  level: number;
}

제네릭으로 표현하기

// | 타입으로 타입을 여러개 정의 할 수 있으나 타입이 많아지면 곤란할 뿐더러 getData 를 사용하는 입장에서도 어떤 데이터를 사용할 지 모호한 상황이 오게 된다.
//  그래서 제네릭 기능 :: 입력이 n 개의 유형일 때 출력도 n 개의 유형인 것을 정의하는 것
//  <T> 이러한 타입이라는 입력이 들어오면 T 로 나온다. 
function getData<AjaxResponse>(url: string): AjaxResponse {
  ajax.open('GET', url, false);
  ajax.send();

  return JSON.parse(ajax.response);
}

// !! 이렇게 사용한다. 같은 getData 함수이지만 입력이 NEWsFeed 가 들어오면 NewsFeed 로 출력

if (newsFeed.length === 0) {
    newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
  }
  
  //NewsDetail 이 들어오면 NewsDetail 로 출력 
  const newsContent = getData<NewsDetail>(CONTENT_URL.replace('@id', id))

제네릭
확정되지 않은 T 라는 값인데, 인자값에 타입이 들어오면 타입으로 T 를 쓸꺼야. 반환값으로도 T 로 쓸 거야.
타입을 호출 순간에 확정하고, 확정 함으로써 그 뒤로 확정되는 범위를 확대해서 타입스크립트의 장점을 누릴 수 있는 기능! (객체를 쓸 때 진가가 발휘 된다)

5. 전체 코드


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

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

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

type NewsDetail =  News & {
  comments: NewsComment[];
}

type NewsComment = News & {
  comments: NewsComment[];
  level: number;
}

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

function getData<AjaxResponse>(url: string): AjaxResponse {
  ajax.open('GET', url, false);
  ajax.send();

  return JSON.parse(ajax.response);
}

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를 진행하지 못합니다.');
  }
}

function newsFeed(): void {
  let newsFeed: NewsFeed[] = store.feeds;
  const newsList = [];
  let 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>
  `;

  if (newsFeed.length === 0) {
    newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
  }

  for(let i = (store.currentPage - 1) * 10; i < store.currentPage * 10; i++) {
    newsList.push(`
      <div class="p-6 ${newsFeed[i].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/${newsFeed[i].id}">${newsFeed[i].title}</a>  
          </div>
          <div class="text-center text-sm">
            <div class="w-10 text-white bg-green-300 rounded-lg px-0 py-2">${newsFeed[i].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>${newsFeed[i].user}</div>
            <div><i class="fas fa-heart mr-1"></i>${newsFeed[i].points}</div>
            <div><i class="far fa-clock mr-1"></i>${newsFeed[i].time_ago}</div>
          </div>  
        </div>
      </div>    
    `);
  }

  template = template.replace('{{__news_feed__}}', newsList.join(''));
  template = template.replace('{{__prev_page__}}', String(store.currentPage > 1 ? store.currentPage - 1 : 1));
  template = template.replace('{{__next_page__}}', String(store.currentPage + 1));

  updateView(template);
}

function newsDetail(): void {
  const id = location.hash.substr(7);
  const newsContent = getData<NewsDetail>(CONTENT_URL.replace('@id', id))
  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/${store.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>${newsContent.title}</h2>
        <div class="text-gray-400 h-20">
          ${newsContent.content}
        </div>

        {{__comments__}}

      </div>
    </div>
  `;

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

  updateView(template.replace('{{__comments__}}', makeComment(newsContent.comments)));
}

function makeComment(comments: NewsComment[]): string {
  const commentString = [];

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

    commentString.push(`
      <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) {
      commentString.push(makeComment(comment.comments));
    }
  }

  return commentString.join('');
}

function router(): void {
  const routePath = location.hash;

  if (routePath === '') {
    newsFeed();
  } else if (routePath.indexOf('#/page/') >= 0) {
    store.currentPage = Number(routePath.substr(7));
    newsFeed();
  } else {
    newsDetail()
  }
}

window.addEventListener('hashchange', router);

router();

에필로그

뭔가 타입스크립트라고 해서 어렵게 생각했는데 js 를 먼저 작성하고 차례대로 바꾸는 작업을 거치니까 문턱이 낮아진 느낌이다. 물론 고급 기능들을 습득 한 것은 아니지만 점점 알게되고 있는 기분이라 좋게 생각한다.

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

0개의 댓글