
SPA : Single Page Application
한 개의 페이지로 이루어진 애플리케이션
페이지 전체를 새로고침🔄 하지 않고,
동적으로 필요한 데이터나 화면 일부만 바꿔서 보여주는 웹앱 구조
- SPA는 HTML 파일이 1개
→ 자바스크립트가 모든 뷰를 전환함
- MPA는 HTML 파일이 여러 개
→<a href>나 window.open()으로 페이지 이동함
| 구분 | .js 기반 SPA 구조 | .html 여러 개로 만든 구조 |
|---|---|---|
| ✅ 진짜 SPA | ✔️ (Yes) | ❌ (아님, MPA에 가까움) |
| 페이지 수 | 1개 (예: index.html) | 여러 개 (main.html, list.html, regist.html 등) |
| 전환 방식 | JS로 DOM만 교체 (ex. showView()) | 링크 클릭 시 페이지 전체 이동 |
| 새로고침 | 없음 (AJAX, 이벤트로만 전환) | 있음 (다른 HTML로 이동하면 리로드) |
| 사용자 경험 | 앱처럼 매끄러움 | 로딩 지연 있음 |
| 속도 | 빠름 (변경된 부분만 렌더링) | 느림 (전체 페이지 다시 요청) |
| 유지비용 | 높지만 관리 잘하면 효율적 | 초기엔 쉬움, 많아지면 비효율적 |
구글API 사용하기
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY&callback=myMap"></script>
API Key삭제 습관 들이기
- 많은 API Key는 네이버, 카카오, 구글 등 외부 서비스와 연결돼 있고, 누군가 내 키를 복사해서 무단으로 API 호출을 하면 요금 청구될 수도 있음
- 악의적인 사용자가 내 키를 이용해 서버를 공격하거나, 내 한도(limit)를 다 써버릴 수도 있음
위도경도
var mapProp = {
center:new google.maps.LatLng(51.508742,-0.120850),
zoom:5,
};
var map = new
var marker = new google.maps.Marker({
position:myCenter, //경도 위도 지정
animation:google.maps.Animation.BOUNCE, //마커 효과
icon:'pinkball.png'
});
marker.setMap(map);
// Zoom to 9 when clicking on marker
google.maps.event.addListener(marker,'click',function() {
map.setZoom(9);
map.setCenter(marker.getPosition());
});
var infowindow = new google.maps.InfoWindow({
content:"Hello World!"
});
infowindow.open(map,marker);
body태그 닫는 곳 바로 위에
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCutucJ6ILzhy-Lz-nSsePe30kyW_1gOvQ
&callback=initMap"></script>
</body>


var storeList=[]; // 등록한 가게들을 저장할 배열
let map;
let profileData; // <img src>에 넣을 base64 문자열 데이터
function initMap(){
// 1️⃣ 맵 기본 속성
let mapProp = {
center: new google.maps.LatLng(37.50973, 127.0555), //지도의 중심좌표 (위도,경도)
zoom: 16, //확대 레벨
};
map = new google.maps.Map(document.getElementById("content"), mapProp);
}
regist()storeList에 저장function regist(){
let store = {
store_name: document.getElementById("store_name").value,
tel: document.getElementById("tel").value,
pos: document.getElementById("pos").value,
profileImg: profileData,
iconImg: document.getElementById("icon").value
};
storeList.push(store);
// 위도, 경도를 파싱하여 마커 생성
let latiLogi = store.pos.split(",");
let pos = new google.maps.LatLng(parseFloat(latiLogi[0]), parseFloat(latiLogi[1]));
// 2️⃣ 마커 추가 + 애니메이션 효과
let marker = new google.maps.Marker({
position: pos,
animation: google.maps.Animation.BOUNCE,
icon: {
url: document.getElementById("icon").value,
scaledSize: new google.maps.Size(50, 50)
}
});
marker.setMap(map);
// 마커 클릭하면 줌 땡겨짐 3️⃣~
google.maps.event.addListener(marker, 'click', function() {
// 4️⃣ 마커 클릭 시 정보창 표시
let infowindow = new google.maps.InfoWindow({
content: "여기 존맛!"
});
infowindow.open(map, marker);
// ~3️⃣
map.setZoom(18);
map.setCenter(marker.getPosition());
});
}
registAll(obj)regist()와 유사한 로직을 반복문 안에 직접 구현 (일괄등록이기 때문)registAll(obj) 호출 시,
obj는 다른 창 (regist.html)에서 이러한 형태opener.registAll({ storeList: opener.storeList });
function registAll(obj){
// 지도에 마커 출력
for(let i = 0; i < obj.storeList.length; i++){
let store = obj.storeList[i];
// 위도, 경도를 파싱하여 마커 생성
let latiLogi = store.pos.split(",");
let pos = new google.maps.LatLng(parseFloat(latiLogi[0]), parseFloat(latiLogi[1]));
// 2️⃣ 마커 추가 + 애니메이션 효과
let marker = new google.maps.Marker({
position: pos,
animation: google.maps.Animation.BOUNCE,
icon: {
url: store.iconImg,
scaledSize: new google.maps.Size(50, 50)
}
});
marker.setMap(map);
// 마커 클릭하면 줌 땡겨짐 3️⃣~
google.maps.event.addListener(marker, 'click', function() {
// 4️⃣ 마커 클릭 시 정보창 표시
let infowindow = new google.maps.InfoWindow({
content: store.store_name
});
infowindow.open(map, marker);
// ~3️⃣
map.setZoom(18);
map.setCenter(marker.getPosition());
});
}
}
✅ 리팩터링 中 공통 함수로 분리하기
regist()와registAll()모두 마커를 생성해서 지도에 표시하는 공통 로직이 있으니,
그 부분을 공통 함수로 분리할 수 있음 ➡ 리팩터링
getList()function getList(){
window.open("list.html", "_blank", "width=550px,height=500px");
}
previewImg(e)FileReader로 읽고,base64 dataURL 형태로 변환하여 미리보기 영역에 표시function previewImg(e){
// FileReader를 통해 이미지 파일을 문자열 형태(dataURL)로 읽음
let reader = new FileReader();
reader.onload = function(data){
document.getElementById("preview").src = data.target.result;
profileData = data.target.result;
}
reader.readAsDataURL(e.target.files[0]);
/* 이벤트 객체(e) 디버깅해보면
target의 유사배열 files[0]에 이미지 파일 들어있었음 */
}
batchRegist()regist.html)을 띄워 다수의 맛집을 일괄 등록할 수 있도록 함function batchRegist(){
let url = "regist.html";
let name = "pop";
let options = "width=500px, height=500px";
window.open(url, name, options);
}
addEventListener("load", function(){
// 이미지 파일 선택 시 미리보기 실행
document.getElementById("profile").addEventListener("change", function(e){
previewImg(e);
});
// 등록 버튼
document.querySelector("#aside_regist :nth-child(7)").addEventListener("click", function(){
regist();
});
// 일괄 등록 버튼
document.querySelector("#aside_regist :nth-child(8)").addEventListener("click", function(){
batchRegist();
});
// 목록 보기 버튼
document.querySelector("#aside_regist :nth-child(9)").addEventListener("click", function(){
getList();
});
});
let obj; // JSON으로 파싱된 데이터 객체를 담을 변수
// 파일 선택 시 실행되는 함수
function loadData(e){
console.log(e); // e.target.files[0]에 선택한 파일이 들어 있음
let file = e.target.files[0]; // 사용자가 선택한 파일
// FileReader를 이용해 텍스트 파일 읽기
let reader = new FileReader();
reader.onload = function(data){
// 읽어들인 텍스트는 JSON 문자열이므로, 객체로 변환해야 사용 가능
obj = JSON.parse(data.target.result);
console.log("파싱결과는 ", obj);
printTable(obj); // 테이블로 화면에 출력
};
reader.readAsText(file); // 텍스트로 읽기 시작 (비동기)
}
// 전달받은 객체를 테이블 형태로 출력
function printTable(obj){
let tag = "<table width='100%' border='1'>";
tag += "<tr>";
tag += "<td>No</td>";
tag += "<td>상호명</td>";
tag += "<td>연락처</td>";
tag += "<td>위도경도</td>";
tag += "<td>대표사진</td>";
tag += "</tr>";
// obj.storeList 배열의 각 요소(가게)를 출력
let n = obj.storeList.length; // 출력용 번호
for(let i = 0; i < obj.storeList.length; i++){
let store = obj.storeList[i]; // i번째 가게
tag += "<tr>";
tag += "<td>" + n-- + "</td>";
tag += "<td>" + store.store_name + "</td>";
tag += "<td>" + store.tel + "</td>";
tag += "<td>" + store.pos + "</td>";
tag += "<td>대표사진</td>"; // (추후 이미지 처리 가능)
tag += "</tr>";
}
tag += "</table>";
document.getElementById("content").innerHTML = tag;
}
// 부모 창(opener)의 registAll() 함수에 현재 객체(obj)를 넘겨 마커 등록
function showIcons(){
window.opener.registAll(obj); // 부모 창의 마커 등록 함수 호출
window.close(); // 현재 창 닫기
}
// 이벤트 바인딩
addEventListener("load", function(){
// 파일 선택 시 loadData 실행
document.querySelector("#header input[type='file']").addEventListener("change", function(e){
loadData(e);
});
// 지도에 적용하기 버튼 클릭 시 showIcons 실행
document.querySelector("#footer button").addEventListener("click", function(){
showIcons();
});
});
• YOUR_API_KEY는 반드시 Google Cloud에서 발급받은 실제 키로 교체하기
• API 키는 깃허브에 절대 올리지 말고 .env 파일이나 비공개 저장소에서 관리하기
• 페이지는 하나지만 JavaScript를 통해 상태와 화면을 계속 갱신하는 게 핵심