정의
사용자와의 상호작용을 위해 단일 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