6 분 소요

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>


  1. cart.omj

<div> <table> </table> </div>

<style> {table.backgroudColor:red}</style>

<script> backend URL 요청 후 응답받은 JSON 문자열을 table 에 채워 atricle 영역에 붙여넣기 </script>


  1. products.omj

<div> <table> </table> </div>

<style>{table.backgroudColor:greed} </style>

<script> backend URL 요청 후 응답받은 JSON 문자열을 table 에 채워 atricle 영역에 붙여넣기 </script>

**※ 직접 구현 시, 기존 DOM 의 내용과 일치하더라도 아예 전부 다시 랜더링 **

=> Vue, Reactjs : 가상 돔 사용 -> 이전 내용을 가상 돔에 기억해서 새롭게 랜더링 되는 내용과 비교해서 동일하면 재랜더링하지 않음


사례

  1. index.html –(import) –> menu.js
  2. menu.js –(use)–> dynamicInsertion.js
  3. 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>


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>

태그: ,

카테고리:

업데이트:

댓글남기기