1seok2

refactor: detail for functions

# SPA 발전 과정 및 원리
link tag를 사용하는 전통적인 웹 방식은 새로운 페이지 요청 시마다 정적 리소스가 다운로드되고 전체 페이지를 다시 렌더링하는 방식을 사용하므로 새로고침이 발생되어 사용성이 좋지 않다. 그리고 변경이 필요없는 부분를 포함하여 전체 페이지를 갱신하므로 비효율적이다.
SPA는 기본적으로 웹 애플리케이션에 필요한 모든 정적 리소스를 최초에 한번 다운로드한다. 이후 새로운 페이지 요청 시, 페이지 갱신에 필요한 데이터만을 전달받아 페이지를 갱신하므로 전체적인 트래픽을 감소할 수 있고, 전체 페이지를 다시 렌더링하지 않고 변경되는 부분만을 갱신하므로 새로고침이 발생하지 않아 네이티브 앱과 유사한 사용자 경험을 제공할 수 있다.
- 문제점
1. **초기 구동 속도**
SPA는 웹 애플리케이션에 필요한 모든 정적 리소스를 최초에 한번 다운로드하기 때문에 초기 구동 속도가 상대적으로 느리다. 하지만 SPA는 웹페이지보다는 애플리케이션에 적합한 기술이므로 트래픽의 감소와 속도, 사용성, 반응성의 향상 등의 장점을 생각한다면 결정적인 단점이라고 할 수는 없다.
2. **SEO(검색엔진 최적화) 문제**
SPA는 서버 렌더링 방식이 아닌 자바스크립트 기반 비동기 모델(클라이언트 렌더링 방식)이다. 따라서 SEO는 언제나 단점으로 부각되어 왔던 이슈이다. 하지만 SPA는 정보의 제공을 위한 웹페이지보다는 애플리케이션에 적합한 기술이므로 SEO 이슈는 심각한 문제로 볼 수 없다. Angular 또는 React 등의 SPA 프레임워크는 서버 렌더링을 지원하는 SEO 대응 기술이 이미 존재하고 있어 SEO 대응이 필요한 페이지에 대해서는 선별적 SEO 대응이 가능하다.
## 1. 이전의 웹사이트
기존의 웹서비스는 링크(앵커`<a href="#">`)를 클릭하면 해당 페이지로 이동하게 된다. 정확히 이야기하면 앵커에 명시되어 있는 자원(일반적으로 html)을 서버에 요청하고 응답으로 받은 내용을 브라우저에 표현하게 된다. 이런 식으로 매 페이지마다 서버에 문서(html)을 요청하고 응답받아서 표현한다.
- 장점
- 브라우저가 응답을 받자마자 렌더링을 할 수 있어서 빠르다는 점이 있다.
- 단점
- 중복되는 데이터가 계속 네트워크를 타고 넘어온다는 것이다
## 2. Ajax로 특정 부분만 새로 그리는 웹서비스
ajax 방식으로 데이터만 가져온 뒤에 클라이언트에서 html을 렌더링하는 작업을 많이 했다. 즉 필요한 부분만 다시 그리도록 자바스크립트 코드를 작성
- 장점
- 필요한 부분만 새로 그리기 때문에 낭비가 없다.
- 이 방법으로 기존의 링크를 타고 다니던 웹서비스보다 편한 사용자 경험을 줄 수 있다.
- 단점
- 뒤로가거나 앞으로 가거나 히스토리 관리가 안된다.
- 앵커의 액션이 직관적이지 않다.
## 3. Hash Single Page Application
hashchange와 hash는 변경되어도 화면은 갱신되지 않는점을 이용한다. 왜냐하면 hash는 문서에서 부차적인 자원에 대한 참조를 가리키기 위해서 만들었기 때문이다. hashchange 이벤트를 사용한 이유는 popstate는 HTML5 스펙이기 때문에 하위 브라우저에서 동작하지 않기 때문이다. hashchange 이벤트는 ie8 이상 브라우저에서 지원한다.
- 장점
- 이전과 다르게 서버에 재요청을 하지 않는다. 필요한 부분만 요청 가능
- hash가 변경되면 history에 쌓인다
- 단점
- URI에 불필요한 #가 들어간다
- SEO 이슈가 존재한다.
## 4. pushState Single Page Application
pushState 메서드는 주소창의 URL을 변경하고 URL을 history entry로 추가하지만 요청하지는 않는다.
- 장점
- pushState는 서버에 재요청을 하지 않는다.
- SEO에도 문제가 없다.
- 단점
- 새로고침을하면 해당 path로 서버에 요청을 한다.
→ 서버에서 작업이 필요
\ No newline at end of file
{
"name": "",
"name": "database-homework",
"version": "0.0.0",
"description": "",
"main": "build/src/index.js",
......@@ -15,7 +15,8 @@
"clean": "gts clean",
"fix": "gts fix",
"compile": "tsc -w",
"build": "webpack --watch",
"dev": "webpack --watch",
"build": "webpack ",
"start": "webpack serve --open",
"predeploy": "webpack"
},
......
......@@ -4,20 +4,21 @@
* @description : 메인 app
* index.ts에서 pathname 받아
* 컴포넌트 제작 후 반환
**/
**/
import Header from "./views/header";
import Body from "./views/body";
import Footer from "./views/footer";
import {getState} from "@src/store/state";
import './assets/style/App.scss';
import { getState } from "@src/store/state";
import "./assets/style/App.scss";
import { getElement } from "./components/functions";
const App = (pathname : string) : string => {
/* id 없을 시 id 입력으로 redirect */
if(!getState().insta_id) pathname = '';
history.pushState('','', pathname);
export default function App(pathname: string): string {
/* id 없을 시 id 입력으로 redirect */
if (!getState().insta_id) pathname = "";
history.pushState("", "", pathname);
return `
return `
<div class="container">
${pathname && Header()}
${Body(pathname)}
......@@ -26,4 +27,8 @@ const App = (pathname : string) : string => {
`;
}
export default App;
\ No newline at end of file
export function renderApp(pathname: string): void {
const $App = getElement("#App") as HTMLDivElement;
$App.innerHTML = App(pathname);
}
......
......@@ -2,83 +2,29 @@
* @author : wonseog
* @date : 2021/03/10
* @description : 버튼 이벤트, Route 이벤트 등록 모음
**/
**/
import {BASE_URL} from "@src/config/url";
import {setState} from "@src/store/state";
import {togglePageLoader} from "@src/components/page.loader";
import {randomTransition} from "@src/components/page.transition";
import { linkEvent, searchEvent, updateEvent } from "./event.list";
export const addEvent = async (
e : Event,
$App : Element | null,
App : (pathname : string) => string
e: Event,
$App: Element,
App: (pathname: string) => string
) => {
e.stopPropagation();
e.stopPropagation();
/* add navigation click event */
if((e.target as HTMLAnchorElement).matches("[data-link]")){
const href : string= (e.target as HTMLAnchorElement).href.split(BASE_URL)[1];
e.preventDefault();
setTimeout(()=>{
$App && ($App.innerHTML = App(href));
},1200);
randomTransition();
}
/* add navigation click event */
if ((e.target as HTMLAnchorElement).matches("[data-link]")) {
linkEvent(e);
}
if ((e.target as HTMLButtonElement).id === "search-button") {
/* add GET event for button */
else if((e.target as HTMLButtonElement).id === 'search-button') {
let result: any= null;
const insta_id = (document.querySelector('#id-input') as HTMLInputElement).value;
togglePageLoader(true);
searchEvent();
}
if(insta_id){
try{
result = await (await fetch(BASE_URL + 'search?insta_id=' + insta_id)).json();
} catch (e){
console.log(e);
} finally {
console.log('res!',result);
if(result){
$App && (()=>{
randomTransition();
setState(result);
setTimeout(()=>{
$App.innerHTML = App('main');
togglePageLoader(false);
},1200)
})();
} else {
togglePageLoader(false);
alert('fail to search');
}
}
} else {
alert('아이디를 입력하세요');
togglePageLoader(false);
}
}
if ((e.target as HTMLButtonElement).id === "update-button") {
/* add POST event for button */
else if((e.target as HTMLButtonElement).id === 'update-button') {
let result: any= null;
const insta_id = (document.querySelector('#id-input') as HTMLInputElement).value;
if(insta_id){
try{
result = await (await fetch(BASE_URL + 'update?insta_id=' + insta_id)).json();
} catch (e){
console.log(e);
} finally {
console.log(result);
result && $App && (()=>{
randomTransition();
setState(result);
setTimeout(()=>{
$App.innerHTML = App('main');
},1200)
})();
}
} else {
alert('아이디를 입력하세요');
}
}
}
\ No newline at end of file
updateEvent();
}
};
......
import { randomTransition } from "@src/components/page.transition";
import { renderApp } from "@src/App";
import {
getElement,
pendingFetch,
warningEmptyId,
fetchByURL,
} from "./functions";
export function linkEvent(e: Event) {
e.preventDefault();
const pathname: string = (e.target as HTMLAnchorElement).pathname.split(
"/"
)[1];
setTimeout(() => {
renderApp(pathname);
}, 1200);
randomTransition();
}
export async function searchEvent() {
const insta_id = (getElement("#id-input") as HTMLInputElement).value;
pendingFetch();
if (!insta_id) {
warningEmptyId();
return;
}
await fetchByURL("search?insta_id=" + insta_id);
}
export async function updateEvent() {
const insta_id = (getElement("#id-input") as HTMLInputElement).value;
pendingFetch();
if (!insta_id) {
warningEmptyId();
return;
}
await fetchByURL("update?insta_id=" + insta_id);
}
import { BASE_URL } from "@src/config/url";
import { setState } from "@src/store/state";
import { togglePageLoader } from "@src/components/page.loader";
import { randomTransition } from "@src/components/page.transition";
import { renderApp } from "@src/App";
export function getElement(cssSelector: string) {
return document.querySelector(cssSelector);
}
export function getElementsAll(cssSelector: string) {
return document.querySelectorAll(cssSelector);
}
export function getPathname() {
return window.location.pathname.split("/")[1];
}
export function warningEmptyId() {
alert("아이디를 입력하세요");
togglePageLoader(false);
}
export function pendingFetch() {
togglePageLoader(true);
}
export function successFetch(result: any, pathname: string) {
randomTransition();
setState(result);
setTimeout(() => {
renderApp(pathname);
togglePageLoader(false);
}, 1200);
}
export function failFetch() {
togglePageLoader(false);
alert("fail to search");
}
export async function fetchByURL(url: string) {
const result = await (await fetch(BASE_URL + url)).json();
console.log("res!", result);
if (result) {
successFetch(result, "main");
} else {
failFetch();
}
}
export const togglePageLoader = (type : boolean) : void => {
const $loader : HTMLDivElement | null = document.querySelector('.page-loader');
if($loader) {
if(type) {
$loader.style.display = 'block';
$loader.style.zIndex = '1';
} else {
$loader.style.display = 'none';
$loader.style.zIndex = '-1';
}
export const togglePageLoader = (type: boolean): void => {
const $loader: HTMLDivElement | null = document.querySelector(".page-loader");
if ($loader) {
if (type) {
$loader.style.display = "block";
$loader.style.zIndex = "1";
} else {
$loader.style.display = "none";
$loader.style.zIndex = "-1";
}
}
\ No newline at end of file
}
};
export default function createLoaderElement() {
const $div = document.createElement("div");
$div.className = "page-loader";
document.body.appendChild($div);
}
......
......@@ -2,40 +2,50 @@
* @author : wonseog
* @date : 2021/03/09
* @description : 페이지 이동 효과 주는 함수
**/
**/
let $div : HTMLDivElement | null;
let $div: HTMLDivElement;
const pageTransitionClassList = [
"transition-0",
"transition-1",
"transition-2"
]
const getDivs = () : HTMLDivElement | null => document.querySelector('.page-transition');
const turnOnAndOff = (idx : number)=>{
$div && (()=>{
$div.classList.toggle(pageTransitionClassList[idx]);
$div.style.display = 'block';
setTimeout(()=>{
$div && (()=>{
$div.style.display = 'none';
$div.classList.toggle(pageTransitionClassList[idx]);
})()
},idx === 0 ? 900 : 2300);
})()
}
"transition-0",
"transition-1",
"transition-2",
];
const getDivs = (): HTMLDivElement =>
document.querySelector(".page-transition") as HTMLDivElement;
const turnOnAndOff = (idx: number) => {
$div.classList.toggle(pageTransitionClassList[idx]);
$div.style.display = "block";
setTimeout(
function () {
$div.style.display = "none";
$div.classList.toggle(pageTransitionClassList[idx]);
},
idx === 0 ? 900 : 2300
);
};
export const initialTrantition = () => {
$div = $div || getDivs();
$div && turnOnAndOff(0);
}
$div = $div || getDivs();
$div && turnOnAndOff(0);
};
export const randomTransition = () => {
$div = $div || getDivs();
const randomIndex = Math.floor(Math.random()*(pageTransitionClassList.length - 1) + 1);
$div = $div || getDivs();
const randomIndex = Math.floor(
Math.random() * (pageTransitionClassList.length - 1) + 1
);
$div && turnOnAndOff(randomIndex);
};
$div && turnOnAndOff(randomIndex);
}
\ No newline at end of file
export default function animatePage() {
const $div = document.createElement("div");
$div.className = "page-transition";
document.body.appendChild($div);
initialTrantition();
}
......
......@@ -2,42 +2,32 @@
* @author : wonseog
* @date : 2021/03/08
* @description : 현재 pathname을 파악 후 App으로 전달
**/
import App from './App';
import {initialTrantition} from "./components/page.transition";
import {addEvent} from "@src/components/add.event";
import './assets/style/PageTransition.scss';
import './assets/style/PageLoader.scss';
window.addEventListener('DOMContentLoaded', () => {
/* add div for page transitions */
(()=> {
const $div = document.createElement('div');
$div.className = "page-transition";
document.body.appendChild($div);
initialTrantition();
})();
(()=> {
const $div = document.createElement('div');
$div.className = "page-loader";
document.body.appendChild($div);
})();
const $App = document.querySelector('#App');
const pathname = window.location.pathname.split('/')[1];
/* change url */
window.addEventListener('popstate', ()=>{
$App && ($App.innerHTML = App(pathname));
});
/* initial render */
$App && ($App.innerHTML = App(pathname));
document.body.addEventListener('click', async (e) => {
await addEvent(e, $App, App);
});
})
\ No newline at end of file
**/
import App, { renderApp } from "./App";
import animatePage from "./components/page.transition";
import { addEvent } from "@src/components/add.event";
import "./assets/style/PageTransition.scss";
import "./assets/style/PageLoader.scss";
import createLoaderElement from "./components/page.loader";
import { getElement, getPathname } from "./components/functions";
window.addEventListener("DOMContentLoaded", () => {
createLoaderElement();
animatePage();
const $App = getElement("#App") as HTMLDivElement;
const pathname = getPathname();
document.body.addEventListener("click", async (e) => {
await addEvent(e, $App, App);
});
/* change url */
window.addEventListener("popstate", () => {
renderApp(pathname);
});
/* initial render */
renderApp(pathname);
});
......
......@@ -4,30 +4,28 @@
* @description : 로그인 이후 첫 화면
* 업데이트하기
* 다른 메뉴 보기
**/
import {getState} from "@src/store/state";
import {formatNumber} from "@src/components/format.number";
import '@src/assets/style/Main.scss'
**/
import { getState } from "@src/store/state";
import { formatNumber } from "@src/components/format.number";
import "@src/assets/style/Main.scss";
const Main = () => {
const state = getState();
let followers : string = '';
let following : string = '';
// @ts-ignore
state.followers?.sort()
.forEach((id, idx) => {
followers += `<div class="item item-${idx % 2}">${id}</div>`
});
// @ts-ignore
state.following?.sort()
.forEach((id, idx) => {
following += `<div class="item item-${idx % 2}">${id}</div>`
});
const state = getState();
let followers: string = "";
let following: string = "";
const followers_num = formatNumber(state.followers?.length || 0);
const followings_num = formatNumber(state.following?.length || 0);
state.followers?.sort().forEach((id, idx) => {
followers += `<div class="item item-${idx % 2}">${id}</div>`;
});
return `
state.following?.sort().forEach((id, idx) => {
following += `<div class="item item-${idx % 2}">${id}</div>`;
});
const followers_num = formatNumber(state.followers?.length || 0);
const followings_num = formatNumber(state.following?.length || 0);
return `
<div class="main">
<div class="profile">
<img src="${state.src}" alt="${state.insta_id}-image"/>
......@@ -43,7 +41,7 @@ const Main = () => {
</div>
</div>
</div>
`
}
`;
};
export default Main;
\ No newline at end of file
export default Main;
......
export {default} from './Body'
\ No newline at end of file
export { default } from "./Body";
......