Single Page Application : SPA 라이브러리 기본 구조
Single Page Application
SPA 라이브러리 (Vue.js, Reacj.js) 처럼 Front Project 구성하기
1) index.html
<article> <div> <table> </table> </div> </article>
<script>
1) 장바구니 메뉴가 클릭되면 cart.omj 파일의 내용을 article 에 넣기
: cart.omj 파일의 <script>는 ctrl+X 복사하고 삭제한 후 index.html 아래에 붙여넣기
2) 음료 메뉴가 클릭되면 products.omj 파일의 내용을 article 에 넣기
products.omj 파일의 <script>는 ctrl+X 복사하고 삭제한 후 index.html 아래에 붙여넣기
</script>
- cart.omj
<div> <table> </table> </div>
<style> {table.backgroudColor:red}</style>
<script> backend URL 요청 후 응답받은 JSON 문자열을 table 에 채워 atricle 영역에 붙여넣기 </script>
- products.omj
<div> <table> </table> </div>
<style>{table.backgroudColor:greed} </style>
<script> backend URL 요청 후 응답받은 JSON 문자열을 table 에 채워 atricle 영역에 붙여넣기 </script>
**※ 직접 구현 시, 기존 DOM 의 내용과 일치하더라도 아예 전부 다시 랜더링 **
=> Vue, Reactjs : 가상 돔 사용 -> 이전 내용을 가상 돔에 기억해서 새롭게 랜더링 되는 내용과 비교해서 동일하면 재랜더링하지 않음
사례
- index.html –(import) –> menu.js
- menu.js –(use)–> dynamicInsertion.js
- dynamicInsertion.js <–(pull)– cart.omj, product.omj
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>SCSA25BUCKS</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="./css/index_float.css" />
<style>
</style>
</head>
<body>
<header>
<img src="./images/logo.png" class="logo">
<nav>
<ul class="menu">
<li class="products">
<!-- 규칙 : 메뉴용 class속성값은 "menu 메뉴종류"를 갖는다.-->
<a href="#" class="menu products">음료</a>
</li>
<li class="cart"><a href="#" class="menu cart">장바구니</a></li>
<li class="orders">
<a href="#">주문목록</a>
</li>
<li><a href="#">로그인</a></li>
<li><a href="#">로그아웃</a></li>
<li><a href=#">가입</a></li>
<li>
<a href="#">채팅</a>
</li>
</ul>
</nav>
</header>
<section>
<article>
내용
<!-- <router-view :key="currentRoute.path"></router-view> -->
<!-- <RouterView/> -->
</article>
<aside>광고</aside>
</section>
<footer>FOOT</footer>
<!--
(X)
<script src="./js/dynamicInsertion.js"></script>
<script src="./js/menu.js"></script>
-->
<script type="module" src="./js/menu.js"></script>
<!--
menu.js파일에서 dynamicInsertion.js의 함수나 변수를 사용하려면
1) dynamicInsertion.js의 함수나 변수 선언부에 export 예약어를 붙여준다
ex) export const appendScript = async (url, target) => {}
2) menu.js에서 import한다
eX) import { appendScript } from './dynamicInsertion.js';
3) html에서 menu.js파일을 선언할 때 type=module이 필요하다
</body>
</html>
menu.js
import { appendScript } from './dynamicInsertion.js';
const articleObj = document.querySelector('article')
/* 메뉴 중 "음료"가 클릭되었을 때 할 일 START */
//DOM트리에서 class속성값이 products인 객체 찾기
const productsAObj = document.querySelector(".products")
if (productsAObj != null) {
productsAObj.addEventListener('click', async (e) => {
e.preventDefault()
const url = './products.omj'
appendScript(url, articleObj)
})
}
/* 메뉴 중 "음료"가 클릭되었을 때 할 일 END */
/* 메뉴 중 "장바구니" 클릭되었을 때 할 일 START */
//DOM트리에서 class속성값이 cart인 객체 찾기
const cartAObj = document.querySelector(".cart")
if (cartAObj != null) {
cartAObj.addEventListener('click', async (e) => {
e.preventDefault()
const url = './cart.omj'
appendScript(url, articleObj)
})
}
/* 메뉴 중 "장바구니" 클릭되었을 때 할 일 END */
/**
* 요청queryString에 menu정보가 포함(ex: ?menu=cart)되었다면('<a> : 장바구니 메뉴 클릭 시') 해당메뉴(ex: 장바구니메뉴)에 클릭이벤트를 발생시킨다
* @param (NodeListOf<Element>) menuArr - 메뉴객체들
*/
export const emitClick = (menuArr)=>{
//DOM트리에서 class속성값이 menu인 모든 객체 찾기
//ex)
// <a href=="#" class="menu products">음료</a>
// <a href="#" class="menu cart">장바구니</a>
// const menuArr = document.querySelectorAll('.menu')
if (location.search !== '') {
const queryString = location.search.substring(1)
const qArr = queryString.split("&")
// 아래 menu변수에
//ex) ?q=v인 경우 undefined반환
// ?menu=cart인 경우 cart반환
// ?menu=cart&menu=product인 경우 cart반환
const menu = qArr.find(q => q.startsWith('menu='))?.split('=')[1]
const target = Array.from(menuArr).find(el => el.classList.contains(menu))
if (target) {
console.log('찾은 요소:', target)
target.click() // 강제로 클릭 이벤트 발생
} else {
console.log(`class="${menu}" 인 요소 없음`)
}
}
}
emitClick(document.querySelectorAll('.menu'));
dynamicInsertion.js
/**
* 외부js파일에서 JSDoc를 지원한다.
* HTML 안의 <script> 에서는 Doc용 사용설명서를 완벽히 지원하지 않는다.
*
* 요청한 URL의 응답내용을 target객체의 innerHTML로 동적으로 삽입하고, 응답내용에 <script>가 있다면 <script>가 실행될 수 있도록 현재페이지의 <body>하위요소로 추가한다
* @async
* @param {string} url - 요청URL
* @param {object} target - 응답내용이 삽입될 DOM객체
* @returns {Promise<void>}
*/
export const appendScript = async (url, target) => {
const response = await fetch(url)
const responseText = await response.text()
// 기존 스크립트 제거
const oldScripts = document.querySelectorAll('script[data-dynamic-script]')
oldScripts.forEach(s => s.remove())
// HTML 삽입
target.innerHTML = responseText
// 새로 삽입된 script 태그 실행
const scripts = target.querySelectorAll("script")
scripts.forEach((oldScript, index) => {
// console.log(index)
// console.log(oldScript)
const newScript = document.createElement("script")
if (oldScript.src) {
newScript.src = oldScript.src
} else {
//innerText와 textContent의 차이점
//innerText : 화면에 실제로 렌더링 된 텍스트 - <script>용 DOM객체.innerText는 렌더링되지않기 때문에 ""반환
//textContent : DOM트리의 텍스트노드 - <script>용 DOM객체의 JS코드들을 반환
newScript.textContent = oldScript.textContent
}
// 구분용 속성 추가
newScript.setAttribute('data-dynamic-script', 'true')
document.body.appendChild(newScript)
oldScript.remove()
})
}
cart.omj
<div class="cart">
<h1>장바구니</h1>
<div class="result"></div>
</div>
<script>
// 브라우저는 innerHTML 등으로 HTML을 동적으로 삽입할 때 <script> 태그 안의 코드를 자동으로 실행하지 않는다.
(async () => {
const url = 'http://localhost:8888/cart'
const response = await fetch(url, { method: "GET", credentials: "include" })
const jsonObj = await response.json()
console.log(jsonObj)//step1. 장바구니 내용 출력하기
console.log(Array.isArray(jsonObj))
//step2. 장바구니 내용이 article영역에 <table>형태 또는 <div>형태로 출력하기
if (jsonObj.length == 0) {
alert("장바구니가 비었습니다")
return
}
const articleObj = document.querySelector("article")
const divResultObj = document.querySelector('div.cart>div.result')
divResultObj.innerHTML = ''
let divText = ''
console.log(jsonObj)
// jsonObj.forEach(element => {
// const { prodNo, prodName, prodPrice, quantity } = element
// divText += `<div class="item">`
// divText += `<div>`
// divText += `<img src="./images/${prodNo}.jpg" alt="${prodName}">`
// divText += `</div>`
// divText += `<div>`
// divText += `<ul>`
// divText += `<li>상품번호:${prodNo}</li>`
// divText += `<li>상품명:${prodName}</li>`
// divText += `<li>상품가격:${prodPrice}</li>`
// divText += `<li>수량:${quantity}</li>`
// divText += `</ul></div>`
// divText += `</div>`
// });
// divResultObj.innerHTML = divText
jsonObj.forEach(element => {
const { prodNo, prodName, prodPrice, quantity } = element
const divItem = document.createElement("div")
divItem.classList.add('item')
const div1 = document.createElement("div")
const img = document.createElement("img")
img.setAttribute('src', `./images/${prodNo}.jpg`)
img.setAttribute('alt', `${prodName}`)
div1.appendChild(img)
divItem.appendChild(div1)
const div2 = document.createElement("div")
const ul = document.createElement('ul')
const liProdNo = document.createElement('li')
liProdNo.textContent = `상품번호:${prodNo}`
ul.appendChild(liProdNo)
const liProdName = document.createElement('li')
liProdName.textContent = `상품명:${prodName}`
ul.appendChild(liProdName)
const liProdPrice = document.createElement('li')
liProdPrice.textContent = `상품가격:${prodPrice}`
ul.appendChild(liProdPrice)
const liQuantity = document.createElement('li')
liQuantity.textContent = `수량:${quantity}`
ul.appendChild(liQuantity)
div2.appendChild(ul)
divItem.appendChild(div2)
divResultObj.appendChild(divItem)
})
})()
</script>
<style>
div.cart>div.result {
/* display: flex; */
/* 가로 방향 배치 */
/* flex-wrap: wrap; */
/* 줄바꿈 허용 */
/* gap: 10px; */
/*아이템 사이 가로/세로 간격 지정*/
}
/* 각 아이템 전체를 flex로 배치 */
div.item {
/* flex-grow: 0 → 여유 공간을 늘리지 않음
flex-shrink: 0 → 줄어들지 않음
flex-basis: 20% → 기본 너비 20% (1/5)
*/
/* flex: 0 0 20%; */
display: flex;
/* 가로 방향 배치 */
flex-direction: row;
/* 기본값 */
align-items: center;
/* 세로 위쪽 정렬 */
gap: 16px;
/* 이미지와 텍스트 간격 */
margin-bottom: 20px;
/* 아이템 간 간격 */
border: 1px solid #ccc;
/* 보기 좋게 테두리 */
padding: 10px;
}
div.item ul {
list-style-type: none;
}
/* 이미지 포함 div */
div.result .item>div:first-child {
flex: 0 0 auto;
/* 너비 고정 : 이미지 div는 내용 크기만큼만 차지 */
}
/* ul 포함 div */
div.result .item>div:last-child {
flex: 1;
/* 남은 공간 채움 */
}
/* 이미지 크기 조절 */
div.result .item img {
width: 100px;
/* 원하는 크기로 조절 */
height: auto;
display: block;
}
</style>
products.omj
<div class="products">
<h1>상품목록</h1>
<table border="1"></table>
</div>
<script>
(() => {
let productArr = [];
const fillTable = () => {
let tableText = "";
for (let product of productArr) {
tableText +=
'<tr><td rowspan="3" class="prodImage"><a href="./product.html?prodNo='+product.prodNo +'"><img src="./images/' +
product.prodNo +
'.jpg">';
tableText += "</a></td>";
tableText += '<td class="prodNo">' + product.prodNo + "</td>";
tableText += "</tr>";
tableText +=
'<tr><td class="prodName">' + product.prodName + "</td></tr>";
tableText +=
'<tr><td class="prodPrice">' + product.prodPrice + "원</td></tr>";
}
//);
tableObj.innerHTML = tableText;
};
const url = "http://localhost:8888/products";
const f = async() =>{
const response = await fetch(url)
productArr = await response.json()
fillTable()
}
f()
let tableObj = document.querySelector("div.products>table");
})();
</script>
<style>
.products {
box-sizing: border-box;
}
.products table {
width: 60%;
border-collapse: collapse;
}
.products td.prodImage {
width: 30%;
padding: 5px;
}
.products td.prodName {
font-weight: bold;
font-size: 1.5em;
/* 기본폰트 16px기준 16*1.5 = 24px */
}
.products table,
td {
border: 1px solid;
}
.products table img {
width: 100%;
}
</style>
댓글남기기