[기술정리] 싱글 페이지 애플리케이션(SPA)

송송승현's avatar
Nov 25, 2024
[기술정리] 싱글 페이지 애플리케이션(SPA)

정의

💡
사용자와의 상호작용을 위해 단일 HTML 페이지를 로드한 후, 추가적인 페이지 로딩 없이 콘텐츠를 동적으로 업데이트 하는 웹 애플리케이션 또는 웹 사이트를 의미
전체적인 페이지를 다시 로드하지 않고 필요한 부분만을 갱신함으로써 사용자 경험을 향상

특징

  • 단일 HTML 페이지 : 초기 로딩 시 한 개의 HTML 페이지를 가져오고, 그 이후에는 새로운 페이지 요청 없이 이 페이지의 콘텐츠를 동적으로 변경
  • AJAX. API 호출 : 서버와의 상호작용은 AJAX 요청이나 API 호출을 통해 수행, 필요한 데이터만을 서버에서 가져오고, 전체 페이지 리로딩을 피함
  • 빠른 응답 : 페이지 전환 없이 콘텐츠를 동적으로 업데이트 하기 때문에 응답속도가 빠름

동작 원리

  • 초기 로딩 : 브라우저가 애플리케이션을 처음 로드할 때 , 하나의 HTML 페이지와 함께 필요한 JavaScript 및 CSS파일을 로드
  • 라우팅 : 클라이언트 측 라우터를 사용하여 URL 변경에 따라 특정 콘텐츠를 표시, 히스토리 API 또는 해시 기반 라우팅을 통해 구현
  • 데이터 가져오기 : 사용자에게 입력값 받아 AJAX 또는 Fetch를 이용하여 비동기적으로 데이터 가져옴
  • DOM 업데이트 : 서버에서 가져온 데이터를 기반으로 DOM을 동적으로 업데이트

장점

  • 트래픽 감소 : 전체 페이지를 다시 로드하지 않기 때문에 네트워크 트래픽 저함
  • 분리된 클라이언트와 서버 : 클라이언트와 서버간의 통신이 API를 통해 이루어져, 클라이언트와 서버의 개발 및 유지보수가 독립적으로 이루어짐

단점

  • 초기 로딩 시간 : 초기 로딩 시 필요한 모든 리소스를 가져와야함
  • SEO : 전통적인 서버사이드 렌더링과 달리, SPA는 클라이언트 측에서 콘텐츠가 렌더링 되기 때문에 검색엔진 최적화가 어려움
  • 복잡한 클라이언트 로직 : 클라이언트 측에서 많은 로직이 처리되기 때문에 애플리케이션 구조가 복잡

예제

<!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; } function renderListItem2(board) { let dom = ` <tr> <td>${board.id}</td> <td>${board.title}</td> <td><a href="javascript:void(0);" onclick="renderDetail(${board.id})">상세보기</a></td> </tr> `; return dom; } async function sendList() { // 1. 사용자 입력값 받기 - 받을 데이터 필요 x // 2. 입력값 JSON 변환 // 3. API 요청 let response = await fetch("http://localhost:8080/api"); // 4. 응답 처리 let responseBody = await response.json(); let listbox = document.querySelector("#list-box"); let boards = responseBody.body; boards.forEach((board) => { let item = renderListItem(board); listbox.append(item); }); } // 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; } // detail async function renderDetail(id) { clear(); let board = await sendDetail(id); state = board; let dom = ` <form> <button type="buttom" onclick ="sendDelete(${board.id})">삭제</button> </form> <form> <button type="buttom" onclick ="renderUpdateForm(${board.id})">수정</button> </form> <div> 번호 : ${board.id} <br> 제목 : ${board.title} <br> 내용 : ${board.content} <br> 작성일 : ${board.createdAt} </div> `; root.innerHTML = dom; } // updateForm 디자인 function renderUpdateForm() { clear(); let dom = ` <form> <input type="text" readonly="true" id = "id" value="${state.id}"/><br> <input type="text" id = "title" placeholder = "제목" value="${state.title}"/><br> <input type="text" id = "content" placeholder = "내용" value="${state.content}"/><br> <input type="text" readonly="true" placeholder = "작성일" value="${state.createdAt}"/><br> <button type="button" onclick = "sendUpdate(${state.id})">수정하기</button> </form> `; root.innerHTML = dom; } // 화면 초기화 function clear() { root.innerHTML = ""; } async function sendDetail(id) { // 1. 사용자 입력값 받기 // 2. 입력값 JSON 변환 // 3. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`); let responseBody = await response.json(); return responseBody.body; // 4. 응답 처리 } async function sendSave() { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. 입력값 JSON 변환 let requestBody = JSON.stringify(board); try { // 3. API 요청 let response = await fetch("http://localhost:8080/api/board", { method: "POST", body: requestBody, headers: { "Content-type": "application/json; charset=utf-8", }, }); // 4. 응답 처리 let responseBody = await response.json(); renderList(); } catch (error) {} } async function sendDelete(id) { // 1. 사용자 입력값 받기 // 2. 입력값 JSON 변환 // 3. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`, { method : "delete" }); let responseBody = await response.json(); console.log(responseBody); // 4. 응답 처리 renderList(); } async function sendUpdate(id) { // 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/${id}`,{ method : "put", body : requestBody, headers : { "content-type" : "application/json; charset=utf-8" } }) let responseBody = response.json(); // 4. 응답 처리 renderDetail(id); } </script> </body> </html>
Share article

송승현의 블로그