이때까지 js 만들었던 hacker news 를 typescript 로 변환하고자 한다.
어떤 부분들이 바뀌는 지 어떻게 바뀌는지 흐름을 따라 가 보면 좋을 듯 하다.
{
"compilerOptions": {
"strict": true, // 본격적으로 타입스크립트로 변환하겠다라고 했을 때 엄격모드로 설정 해 놓으면 조금 더 세부적으로 변환 가능하다.
"target": "ES5", // 어떤 문법을 사용할 것인지 (js 에 사용될 문법 체계를 어떤 버전을 쓸 것이냐)
"module": "CommonJS",
"alwaysStrict": true,
"noImplicitAny": true, //any 타입을 쓰지 못하도록 한다. (타입을 명확하게 할 수 있도록 한다.)
"noImplicitThis": true,
"sourceMap": true, //개발환경과 소스코드를 일치 시킨다. (관리자도구에서 ts 파일을 볼 수 있다.)
"downlevelIteration": true
}
}
//타입 알리아스
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 문
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 라는 타입이 들어왔을 때 작동 될 수 없는 코드에 대해서 경고를 해 주거나, 혹은 그것을 원천적으로 막을 수 있는 코드 테크닉 혹은 코딩 방식을 타입가드라고 한다.
// 공통 속성
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 로 쓸 거야.
타입을 호출 순간에 확정하고, 확정 함으로써 그 뒤로 확정되는 범위를 확대해서 타입스크립트의 장점을 누릴 수 있는 기능! (객체를 쓸 때 진가가 발휘 된다)
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 를 먼저 작성하고 차례대로 바꾸는 작업을 거치니까 문턱이 낮아진 느낌이다. 물론 고급 기능들을 습득 한 것은 아니지만 점점 알게되고 있는 기분이라 좋게 생각한다.