패스트캠퍼스 강의를 정리한 내용입니다.
"김민태의 프론트엔드 아카데미 : 제 1강 JavaScript & TypeScript Essential"
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>HN client</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script src="app.js" type="module"></script>
</body>
</html>
//app.js
<script>
let ajax = new XMLHttpRequest();
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
ajax.open('GET', NEWS_URL, false);
ajax.send();
const newsFeed = JSON.parse(ajax.response);
console.log(newsFeed);
const ul = document.createElement('ul');
for (let i = 0; i < newsFeed.length; i++) {
const li = document.createElement('li');
li.innerHTML = newsFeed[i].title;
ul.appendChild(li);
}
document.getElementById('root').appendChild(ul);
</script>
HNPWA API :
https://github.com/tastejs/hacker-news-pwas/blob/master/docs/api.md
웹 어플리케이션으로 화면 전환시키는 코드 짜기
타이틀을 클릭하면 그 타이틀에 해당하는 데이터 가져오기
//app.js
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
ajax.open('GET', NEWS_URL, false);
ajax.send();
const newsFeed = JSON.parse(ajax.response);
console.log(newsFeed);
const ul = document.createElement('ul');
window.addEventListener('hashchange', function () {
const id = location.hash.substr(1);
ajax.open('GET', CONTENT_URL.replace('@id', id), false);
ajax.send();
const newsContent = JSON.parse(ajax.response);
const title = document.createElement('h1');
title.innerHTML = newsContent.title;
content.appendChild(title);
console.log(newsContent);
});
for (let i = 0; i < newsFeed.length; i++) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${newsFeed[i].id}`;
a.innerHTML = `${newsFeed[i].title} (${newsFeed[i].comments_count})`;
li.appendChild(a);
ul.appendChild(li);
}
container.appendChild(ul);
container.appendChild(content);
</script>
DOM API 사용으로 인해 UI의 구조가 잘 드러나지 않는 문제점을 해결하는 방법은?
DOM API 자체를 최대한 사용하지 않고, 문자열만을 이용해서 UI 만들기
+중복 코드 제거하기
코드가 반복되는 중복은 어떻게 제거할 수 있을까?
데이터를 담을 수 있는 그릇은 변수
여러개의 변수를 담을 수 있는 것은 객체
코드를 묶을 수 있는 건 함수!
//app.js
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
const newsFeed = getData(NEWS_URL);
const ul = document.createElement('ul');
window.addEventListener('hashchange', function () {
const id = location.hash.substr(1);
const newsContent = getData(CONTENT_URL.replace('@id', id));
const title = document.createElement('h1');
title.innerHTML = newsContent.title;
content.appendChild(title);
});
for (let i = 0; i < newsFeed.length; i++) {
const div = document.createElement('div');
const li = document.createElement('li');
const a = document.createElement('a');
div.innerHTML = `
<li>
<a href="#${newsFeed[i].id}">${newsFeed[i].title} (${newsFeed[i].comments_count})</a>
</li>
`;
ul.appendChild(div.firstElementChild);
// === ul.appendChild(div.children[0]);
}
container.appendChild(ul);
container.appendChild(content);
</script>
한 번에 하나의 화면만 보여주는 구조 만들기
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
function newsFeed() {
const newsFeed = getData(NEWS_URL);
const newsList = [];
newsList.push('<ul>');
for (let i = 0; i < newsFeed.length; i++) {
newsList.push(`
<li>
<a href="#${newsFeed[i].id}">${newsFeed[i].title} (${newsFeed[i].comments_count})</a>
</li>
`);
}
newsList.push('</ul>');
container.innerHTML = newsList.join('');
}
const ul = document.createElement('ul');
function newsDetail() {
const id = location.hash.substr(1);
const newsContent = getData(CONTENT_URL.replace('@id', id));
const title = document.createElement('h1');
container.innerHTML = `
<h1>${newsContent.title}</h1>
<div>
<a href="#">목록으로</a>
</div>
`;
title.innerHTML = newsContent.title;
content.appendChild(title);
}
function router() {
const routePath = location.hash;
//location hash에 #만 들어있는 경우에는 빈값을 반환
if (routePath === '') {
newsFeed();
} else {
newsDetail();
}
}
window.addEventListener('hashchange', router);
router();
</script>
현재 페이지가 몇 번째 페이지인지를 기억하는 변수가 필요
페이지의 위치값은 바뀌므로 상수가 아닌 변수!
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
const store = {
currentPage: 1,
};
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
function newsFeed() {
const newsFeed = getData(NEWS_URL);
const newsList = [];
newsList.push('<ul>');
for (let i = (store.currentPage - 1) * 10; i < store.currentPage * 10; i++) {
newsList.push(`
<li>
<a href="#/show/${newsFeed[i].id}">${newsFeed[i].title} (${newsFeed[i].comments_count})</a>
</li>
`);
}
newsList.push('</ul>');
newsList.push(`
<div>
<a href="#/page/${store.currentPage > 1 ? store.currentPage - 1 : 1}">이전 페이지</a>
<a href="#/page/${
store.currentPage * 10 < newsFeed.length ? store.currentPage + 1 : store.currentPage
}">다음 페이지</a>
</div>
`);
container.innerHTML = newsList.join('');
}
const ul = document.createElement('ul');
function newsDetail() {
const id = location.hash.substr(7);
const newsContent = getData(CONTENT_URL.replace('@id', id));
const title = document.createElement('h1');
container.innerHTML = `
<h1>${newsContent.title}</h1>
<div>
<a href="#/page/${store.currentPage}">목록으로</a>
</div>
`;
title.innerHTML = newsContent.title;
content.appendChild(title);
}
function router() {
const routePath = location.hash;
//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();
</script>
템플릿 코드 공통으로 분리 사용
+tailwind를 활용한 스타일 개선
tailwind CSS란?
Utility-First 컨셉을 가진 CSS 프레임워크로, 미리 세팅된 유틸리티 클래스를 HTML 코드 내에서 작성하여 스타일 적용 가능
//index.html
//tailwind CDN link 추가
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>HN client</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script src="app.js" type="module"></script>
</body>
</html>
//app.js
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
const store = {
currentPage: 1,
};
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
function newsFeed() {
const newsFeed = getData(NEWS_URL);
const newsList = [];
let template = `
<div class="container mx-auto p-4">
<h1>Hacker News</h1>
<ul>
{{__news_feed__}}
</ul>
<div>
<a href="#/page/{{__prev_page__}}">이전 페이지</a>
<a href="#/page/{{__next_page__}}">다음 페이지</a>
</div>
</div>
`;
for (let i = (store.currentPage - 1) * 10; i < store.currentPage * 10; i++) {
newsList.push(`
<li>
<a href="#/show/${newsFeed[i].id}">${newsFeed[i].title} (${newsFeed[i].comments_count})</a>
</li>
`);
}
template = template.replace('{{__news_feed__}}', newsList.join(''));
template = template.replace('{{__prev_page__}}', store.currentPage > 1 ? store.currentPage - 1 : 1);
template = template.replace(
'{{__next_page__}}',
store.currentPage * 10 < newsFeed.length ? store.currentPage + 1 : store.currentPage
);
container.innerHTML = template;
}
const ul = document.createElement('ul');
function newsDetail() {
const id = location.hash.substr(7);
const newsContent = getData(CONTENT_URL.replace('@id', id));
const title = document.createElement('h1');
container.innerHTML = `
<h1>${newsContent.title}</h1>
<div>
<a href="#/page/${store.currentPage}">목록으로</a>
</div>
`;
title.innerHTML = newsContent.title;
content.appendChild(title);
}
function router() {
const routePath = location.hash;
//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();
</script>
tailwind CSS :
https://tailwindcss.com/
+fontawesome을 활용한 UI 개선
fontawesome이란?
CSS 및 Less를 기반으로하는 글꼴 및 아이콘 툴킷
*툴킷: 필요할 때 가져다 쓸 수 있는 라이브러리
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>HN client</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script src="app.js" type="module"></script>
</body>
</html>
//app.js
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
const store = {
currentPage: 1,
};
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
function newsFeed() {
const newsFeed = getData(NEWS_URL);
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="itmes-center justify-end">
<a href="#/page/{{__prev_page__}}">Prev</a>
<a href="#/page/{{__next_page__}}">Next</a>
</div>
</div>
</div>
</div>
<div class="p-4 text-2xl text-gray-700">
{{__news_feed__}}
</div>
</div>
`;
for (let i = (store.currentPage - 1) * 10; i < store.currentPage * 10; i++) {
newsList.push(`
<div class="p-6 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">${newsFeed[i].user}</i></div>
<div><i class="fas fa-heart mr-1">${newsFeed[i].points}</i></div>
<div><i class="fas fa-clock mr-1">${newsFeed[i].time_ago}</i></div>
</div>
</div>
</div>
`);
}
template = template.replace('{{__news_feed__}}', newsList.join(''));
template = template.replace('{{__prev_page__}}', store.currentPage > 1 ? store.currentPage - 1 : 1);
template = template.replace(
'{{__next_page__}}',
store.currentPage * 10 < newsFeed.length ? store.currentPage + 1 : store.currentPage
);
container.innerHTML = template;
}
const ul = document.createElement('ul');
function newsDetail() {
const id = location.hash.substr(7);
const newsContent = getData(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">Hacher 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>
`;
function makeComment(comments, called = 0) {
const commentString = [];
for (let i = 0; i < comments.length; i++) {
commentString.push(`
<div style="padding-left: ${called * 40}px;" class="mt-4">
<div class="text-gray-400">
<i class="fa fa-sort-up mr-2"></i>
<strong>${comments[i].user}</strong>${comments[i].time_ago}
</div>
<p class="text-gray-700">${comments[i].content}</p>
</div>
`);
if (comments[i].comments.length > 0) {
//재귀호출
commentString.push(makeComment(comments[i].comments, called + 1));
}
}
return commentString.join('');
}
container.innerHTML = template.replace('{{__comments__}}', makeComment(newsContent.comments));
}
function router() {
const routePath = location.hash;
//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();
</script>
다양한 cdn 링크 제공 사이트 : https://cdnjs.com/
font, icon 제공 사이트 : https://fontawesome.com/
<script>
const container = document.getElementById('root');
const ajax = new XMLHttpRequest();
const content = document.createElement('div');
const NEWS_URL = 'https://api.hnpwa.com/v0/news/1.json';
const CONTENT_URL = 'https://api.hnpwa.com/v0/item/@id.json';
const store = {
currentPage: 1,
feeds: [],
};
function getData(url) {
ajax.open('GET', url, false);
ajax.send();
return JSON.parse(ajax.response);
}
function makeFeeds(feeds) {
for (let i = 0; i < feeds.length; i++) {
feeds[i].read = false;
}
return feeds;
}
function newsFeed() {
let 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="itmes-center justify-end">
<a href="#/page/{{__prev_page__}}">Prev</a>
<a href="#/page/{{__next_page__}}">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(NEWS_URL));
}
for (let i = (store.currentPage - 1) * 10; i < store.currentPage * 10; i++) {
newsList.push(`
<div class="p-6 ${
newsFeed[i].read ? 'bg-gray-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">${newsFeed[i].user}</i></div>
<div><i class="fas fa-heart mr-1">${newsFeed[i].points}</i></div>
<div><i class="fas fa-clock mr-1">${newsFeed[i].time_ago}</i></div>
</div>
</div>
</div>
`);
}
template = template.replace('{{__news_feed__}}', newsList.join(''));
template = template.replace('{{__prev_page__}}', store.currentPage > 1 ? store.currentPage - 1 : 1);
template = template.replace(
'{{__next_page__}}',
store.currentPage * 10 < newsFeed.length ? store.currentPage + 1 : store.currentPage
);
container.innerHTML = template;
}
const ul = document.createElement('ul');
function newsDetail() {
const id = location.hash.substr(7);
const newsContent = getData(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">Hacher 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;
}
}
function makeComment(comments, called = 0) {
const commentString = [];
for (let i = 0; i < comments.length; i++) {
commentString.push(`
<div style="padding-left: ${called * 40}px;" class="mt-4">
<div class="text-gray-400">
<i class="fa fa-sort-up mr-2"></i>
<strong>${comments[i].user}</strong>${comments[i].time_ago}
</div>
<p class="text-gray-700">${comments[i].content}</p>
</div>
`);
if (comments[i].comments.length > 0) {
//재귀호출
commentString.push(makeComment(comments[i].comments, called + 1));
}
}
return commentString.join('');
}
container.innerHTML = template.replace('{{__comments__}}', makeComment(newsContent.comments));
}
function router() {
const routePath = location.hash;
//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();
</script>