SPA(Single Page Application)

화낼거양's avatar
Nov 22, 2024
SPA(Single Page Application)
 
 
 
💡
SPA(Single Page Application)는 단일 HTML 페이지로 구성된 웹 애플리케이션입니다. SPA에서는 새로운 페이지를 로드하는 대신, JavaScript를 사용하여 현재 페이지의 콘텐츠를 동적으로 업데이트합니다. 이렇게 하면 사용자 경험이 더 빠르고 부드럽게 느껴집니다.
 

주요 특징

 
  1. 단일 HTML 페이지:
      • 애플리케이션은 하나의 HTML 페이지로 구성되어 있으며, 이 페이지가 처음 로드된 이후로는 페이지 자체를 다시 로드하지 않습니다.
  1. 동적 콘텐츠 업데이트:
      • JavaScript와 AJAX를 사용하여 서버로부터 필요한 데이터를 비동기적으로 가져와 페이지 내용을 동적으로 업데이트합니다.
  1. 클라이언트 사이드 라우팅:
      • URL 변경 없이 페이지 내에서의 이동을 지원하여, 애플리케이션의 다른 부분으로 네비게이션할 때 새로운 페이지를 로드하는 것처럼 보이게 합니다.
  1. 빠른 반응 속도:
      • 페이지 전환 시 새로운 페이지를 로드하는 대신 필요한 데이터만을 로드하여 페이지의 일부분만 업데이트하므로, 반응 속도가 빠릅니다.
 
 
 

장점

 
  • 빠른 로딩 시간: 한 번 로드된 이후에는 서버 요청이 최소화되므로 빠른 로딩을 제공합니다.
  • 부드러운 사용자 경험: 페이지 이동 시 새로고침 없이 콘텐츠가 동적으로 업데이트됩니다.
  • 더 나은 성능: 필요한 부분만 업데이트하므로, 리소스 사용이 효율적입니다.
 

단점

 
  • 초기 로드 시간: 초기 로딩 시 모든 필요한 리소스를 한 번에 로드하므로 시간이 더 걸릴 수 있습니다.
  • SEO 문제: 서버에서 렌더링되지 않기 때문에 검색 엔진 최적화가 어렵습니다.
 
 
 
 
 

SPA 구현해보기

 
 
기존의 메인 화면 HTML을 복사하여 index.html 이라고 저장한 뒤, index의 모든 페이지 렌더링 내용들을 자바 스크립트를 이용하여 페이지를 구성하도록 하겠습니다.
 
아래는 참고를 위한 전체 코드 작성 내용입니다.
 
클릭하면 전체 코드가 출력됩니다.
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>blog</title> </head> <body> <nav> <ul> <li> <a href="javascript:void(0);" onclick="renderList()">홈</a> </li> <li> <a href="javascript:void(0);" onclick="renderSaveForm()">글쓰기</a> </li> </ul> </nav> <hr> <section id="root"> </section> <script> // state let state = {}; // init let root = document.querySelector("#root"); renderList(); // list 디자인 function renderList() { clear(); let dom = ` <table border="1"> <thead> <tr> <th>번호</th> <th>제목</th> <th></th> </tr> </thead> <tbody id="list-box"> </tbody> </table> `; root.innerHTML = dom; sendList(); } function renderListItem(board) { let dom = ` <td>${board.id}</td> <td>${board.title}</td> <td><a href="javascript:void(0);" onclick="renderDetail(${board.id})">상세보기</a></td> `; let item = document.createElement("tr"); item.innerHTML = dom; return item; } async function sendList() { // 1. API 요청 let response = await fetch("http://localhost:8080/api"); let responseBody = await response.json(); // 2. 응답 처리 let boards = responseBody.body; let listBox = document.querySelector("#list-box"); boards.forEach(board => { let item = renderListItem(board); listBox.append(item); }); } // detail 디자인 async function renderDetail(id) { clear(); let board = await sendDetail(id); state = board; let dom = ` <form> <button type="button" onclick="sendDelete();">삭제</button> </form> <form> <button type="button" onclick="renderUpdateForm();">수정</button> </form> <div> 번호: ${board.id}<br> 제목: ${board.title}<br> 내용: ${board.content}<br> 작성일: ${board.createdAt}<br> </div> ` root.innerHTML = dom; } async function sendDetail(id) { // 1. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`); let responseBody = await response.json(); // 2. 응답 처리 return responseBody.body; } // saveForm 디자인 function renderSaveForm() { clear(); let dom = ` <form> <input type="text" id="title" placeholder="제목"><br> <input type="text" id="content" placeholder="내용"><br> <button type="button" onclick="sendSave();">글쓰기</button> </form> `; root.innerHTML = dom; } async function sendSave() { // 1. 사용자 입력값 받기 let board={ title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch("http://localhost:8080/api/board", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); let responseBody = await response.json(); // 4. 응답 처리 renderList(); } async function sendDelete() { await fetch(url=`http://localhost:8080/api/board/${state.id}`, { method: "delete" }); renderList(); } async function sendUpdate() { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch(`http://localhost:8080/api/board/${state.id}`, { method: "put", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); // 4. 응답 처리 renderDetail(state.id); } // updateForm 디자인 async function renderUpdateForm() { clear(); let dom = ` <form> <input type="number" value="${state.id}" readonly><br> <input type="text" id="title" value="${state.title}"><br> <input type="text" id="content" value="${state.content}"><br> <input type="text" value="${state.createdAt}" readonly><br> <button type="button" onclick="sendUpdate();">수정</button> </form> ` root.innerHTML = dom; } // 화면 초기화 function clear() { root.innerHTML = ""; } </script> </body> </html>
 
 
 
 

예제 설명

 
  • 기존의 하이퍼링크들의 모든 기본 동작을 막고, 클릭했을 때 스크립트의 함수를 실행할 수 있도록 변경하였습니다.
(ex : <a href="javascript:void(0);" onclick="renderList()"> )
 
  • form 데이터 대신 JSON 데이터를 이용하여 값을 주고 받을 예정이기 때문에 form 태그의 action이나 method 등의 내용을 모두 삭제하였습니다.
    • form 데이터를 전송하지 않을 것이기 때문에 버튼의 타입도 submit에서 button으로 변경하였고, 클릭했을 때 동작할 메서드 이름 또한 추가로 작성합니다.
       

      (Form 데이터를 사용하지 않는 이유)

       
      1. 페이지 새로고침:
          • Form 제출을 하면 브라우저가 기본적으로 전체 페이지를 다시 로드하게 됩니다. 이 과정에서 기존의 상태나 데이터를 잃어버릴 수 있습니다.
      1. SPA와의 충돌:
          • SPA(Single Page Application)는 한 번 로드된 페이지 내에서 동적으로 콘텐츠를 갱신하는 방식입니다. SPA는 페이지를 새로고침하지 않고, JavaScript를 통해 필요한 데이터를 서버에서 받아와 페이지의 일부분만 업데이트합니다. 따라서 Form 제출로 인한 전체 페이지 새로고침은 SPA의 장점을 무력화시킵니다.
      1. 로드 시간:
          • SPA는 처음 로드할 때 필요한 모든 리소스를 불러오기 때문에 초기 로드 시간이 다소 길 수 있습니다. 하지만 이후에는 필요한 부분만 동적으로 업데이트하기 때문에 더 빠르고 부드러운 사용자 경험을 제공합니다. 전체 페이지를 새로고침하는 방식은 이러한 SPA의 장점을 살릴 수 없게 합니다.
           
       
  • 버튼또는 링크을 눌렀을 때 변경시킬 페이지 요소는 section 태그 안에 있는 요소들입니다.
    • 요소를 추가하기전. 기존 요소를 모두 삭제하기 위해 section에 id를 추가하고 해당 id를 이용하여 메서드 실행시 공통적으로 clear 메서드를 동작하도록 추가하였습니다.
      (clear 메서드 : section태그안에 있는 모든 요소를 삭제합니다.)
       
 
 
 

요소 그리기

 
 

renderList 함수

 
function renderList() { clear(); let dom = ` <table border="1"> <thead> <tr> <th>번호</th> <th>제목</th> <th></th> </tr> </thead> <tbody id="list-box"> </tbody> </table> `; root.innerHTML = dom; sendList(); }
 
  • clear(): root 요소의 내용을 초기화합니다.
  • root.innerHTML = dom: 게시글 목록을 표시할 HTML 테이블을 생성하여 삽입합니다.
  • sendList(): 게시글 목록을 서버에서 가져오는 함수입니다.
 
 

renderListItem 함수

 
function renderListItem(board) { let dom = ` <td>${board.id}</td> <td>${board.title}</td> <td><a href="javascript:void(0);" onclick="renderDetail(${board.id})">상세보기</a></td> `; let item = document.createElement("tr"); item.innerHTML = dom; return item; }
 
  • renderListItem(board): 각 게시글 항목을 포함하는 HTML 테이블 행을 생성하여 반환합니다.
  • 객체로 변환하지 않을 경우, 단순 문자열이 출력 되기 때문에 객체로 변환하여 반환하였습니다.
 
 

sendList 함수

 
async function sendList() { // 1. API 요청 let response = await fetch("http://localhost:8080/api"); let responseBody = await response.json(); // 2. 응답 처리 let boards = responseBody.body; let listBox = document.querySelector("#list-box"); boards.forEach(board => { let item = renderListItem(board); listBox.append(item); }); }
 
  • sendList(): 서버로부터 게시글 목록을 가져와 각 게시글 항목을 #list-box 요소에 추가합니다.
 
 

renderDetail 함수

 
async function renderDetail(id) { clear(); let board = await sendDetail(id); state = board; let dom = ` <form> <button type="button" onclick="sendDelete();">삭제</button> </form> <form> <button type="button" onclick="renderUpdateForm();">수정</button> </form> <div> 번호: ${board.id}<br> 제목: ${board.title}<br> 내용: ${board.content}<br> 작성일: ${board.createdAt}<br> </div> `; root.innerHTML = dom; }
 
  • renderDetail(id): 특정 게시글의 상세 정보를 서버에서 가져와 state에 저장하고, 이를 화면에 표시합니다.
 
 

sendDetail 함수

 
async function sendDetail(id) { // 1. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`); let responseBody = await response.json(); // 2. 응답 처리 return responseBody.body; }
 
  • sendDetail(id): 특정 ID의 게시글 상세 정보를 서버에서 가져와 반환합니다.
 
 

renderSaveForm 함수

 
function renderSaveForm() { clear(); let dom = ` <form> <input type="text" id="title" placeholder="제목"><br> <input type="text" id="content" placeholder="내용"><br> <button type="button" onclick="sendSave();">글쓰기</button> </form> `; root.innerHTML = dom; }
 
  • renderSaveForm(): 글쓰기 폼을 화면에 생성하여 표시합니다.
 
 

sendSave 함수

 
async function sendSave() { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch("http://localhost:8080/api/board", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); let responseBody = await response.json(); // 4. 응답 처리 renderList(); }
 
  • 사용자 입력값 받기 및 JSON 변환: 입력된 제목과 내용을 JSON 문자열로 변환합니다.
  • API 요청: POST 요청을 서버로 보냅니다.
  • 응답 처리: 게시글 목록을 다시 렌더링합니다.
 

sendDelete 함수

 
async function sendDelete() { await fetch(`http://localhost:8080/api/board/${state.id}`, { method: "delete" }); renderList(); }
 
  • sendDelete(): 현재 상태의 게시글 ID를 사용하여 DELETE 요청을 보냅니다.
  • 응답 처리: 게시글 목록을 다시 렌더링합니다.
 
 

sendUpdate 함수

 
async function sendUpdate() { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch(`http://localhost:8080/api/board/${state.id}`, { method: "put", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); // 4. 응답 처리 renderDetail(state.id); }
 
  • 사용자 입력값 받기 및 JSON 변환: 입력된 제목과 내용을 JSON 문자열로 변환합니다.
  • API 요청: PUT 요청을 서버로 보냅니다.
  • 응답 처리: 게시글 상세 정보를 다시 렌더링합니다.
 
 

renderUpdateForm 함수

 
async function renderUpdateForm() { clear(); let dom = ` <form> <input type="number" value="${state.id}" readonly><br> <input type="text" id="title" value="${state.title}"><br> <input type="text" id="content" value="${state.content}"><br> <input type="text" value="${state.createdAt}" readonly><br> <button type="button" onclick="sendUpdate();">수정</button> </form> `; root.innerHTML = dom; }
 
  • 기존 게시글 정보 로드: 현재 상태의 게시글 정보를 폼에 채웁니다.
  • 화면에 표시: 수정 폼을 생성하여 화면에 표시합니다.
 
 
 

 

비동기 프로그래밍의 핵심 요소

 

async 함수

async 키워드는 함수를 비동기 함수로 선언하는 데 사용됩니다. 비동기 함수는 Promise를 반환하며, 내부에서 비동기 작업을 처리할 수 있습니다.
 

await 표현식

await 키워드는 async 함수 내에서만 사용할 수 있으며, Promise가 처리될 때까지 함수의 실행을 일시 중지시킵니다. Promise가 해결되면 await는 그 값을 반환합니다.
 

fetch 함수

fetch 함수는 네트워크 요청을 보내고, Promise를 반환합니다. fetch 함수는 다양한 HTTP 요청을 처리할 수 있으며, 데이터를 받아오는 데 주로 사용됩니다.
 

 

비동기 방식을 사용한 이유 :

 
💡
비동기 방식을 사용하는 주요 이유는 효율적인 자원 활용더 나은 사용자 경험을 제공하기 위해서입니다.
 

1. 자원 활용의 효율성

 
  • 블로킹을 피함: 비동기 방식에서는 하나의 작업이 완료될 때까지 다른 작업을 기다리지 않습니다. 이는 특히 I/O 작업(네트워크 요청, 파일 읽기/쓰기 등)에서 유용합니다. 예를 들어, 서버로부터 데이터를 가져오는 동안 다른 작업을 계속 수행할 수 있습니다.
  • 동시성 처리: 비동기 방식은 여러 작업을 동시에 처리할 수 있도록 하여, CPU와 메모리를 더 효율적으로 사용합니다. 이는 서버와 같은 환경에서 특히 중요한데, 다수의 클라이언트 요청을 동시에 처리할 수 있기 때문입니다.
 

2. 더 나은 사용자 경험

 
  • 응답성 향상: 웹 애플리케이션에서 비동기 방식은 사용자 인터페이스가 응답성을 유지하도록 합니다. 긴 작업이 진행되는 동안에도 사용자 인터페이스가 잠기거나 멈추지 않게 됩니다.
  • 빠른 피드백: 사용자가 어떤 행동을 취했을 때, 비동기적으로 즉시 피드백을 제공할 수 있습니다. 예를 들어, 버튼 클릭 시 서버에 요청을 보내는 동안 로딩 스피너를 보여주고, 요청이 완료되면 결과를 업데이트합니다.
 

3. 복잡한 작업 관리

 
  • 코드 가독성: asyncawait를 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있습니다. 이는 콜백 지옥(callback hell)이나 복잡한 Promise 체인보다 훨씬 가독성이 높습니다.
  • 에러 처리: 비동기 작업의 에러를 try...catch 블록을 통해 쉽게 처리할 수 있습니다. 이는 복잡한 비동기 로직에서 에러 핸들링을 더 직관적이고 명확하게 만듭니다.
 
 
 

 
 

스크립트 작성할 때 알아두면 좋은 내용

 
아래의 내용은 DOM(Document Object Model) 조작과 관련된 메서드 4가지입니다.
특정 위치에 새로운 요소를 삽입하는 데 사용며, 각각의 메서드는 요소가 삽입되는 위치에 따라 다릅니다.
 

after()

  • 지정된 요소 바로 뒤에 새로운 요소를 삽입합니다.
 
let element = document.getElementById("element"); element.after("New Content");
 
예를 들어, <div id="element"></div> 뒤에 "New Content"가 삽입됩니다.
 

append()

  • 지정된 요소의 마지막 자식으로 새로운 요소를 삽입합니다.
 
let parent = document.getElementById("parent"); parent.append("New Content");
 
예를 들어, <div id="parent"></div> 내부의 끝에 "New Content"가 삽입됩니다.
 

prepend()

  • 지정된 요소의 첫 번째 자식으로 새로운 요소를 삽입합니다.
 
let parent = document.getElementById("parent"); parent.prepend("New Content");
 
예를 들어, <div id="parent"></div> 내부의 첫 부분에 "New Content"가 삽입됩니다.
 

before()

  • 지정된 요소 바로 앞에 새로운 요소를 삽입합니다.
 
let element = document.getElementById("element"); element.before("New Content");
 
예를 들어, <div id="element"></div> 앞에 "New Content"가 삽입됩니다.
 

요약

  • after(): 요소 바로 뒤에 삽입
  • append(): 요소 내부의 마지막 자식으로 삽입
  • prepend(): 요소 내부의 첫 번째 자식으로 삽입
  • before(): 요소 바로 앞에 삽입
Share article

moohyun