목차(클릭하세요)
DOM: (Document Object Model)
[소스코드 참고 사이트]
1. DOM이란?
•
HTML: 가족 구성원들의 이름이 적힌 종이
•
DOM: 가족 관계를 나타내는 실제 가계도 (부모-자식 관계가 명확한 트리 구조)
•
JavaScript: 가계도를 보고 "김철수의 아버지는 누구지?" 같은 질문에 답하거나, 새 가족 구성원을 추가하는 사람
( 이 작업을 하는 사람이 자바스크립트 엔진)
1-1. 왜 필요한가?
•
웹 페이지를 프로그래밍적으로 조작할 수 있게 해주는 인터페이스
1-2.DOM 구조 예시
<html>
<head>
<title>DOM 예제</title>
</head>
<body>
<h1 id="title">안녕하세요</h1>
<p class="msg">DOM 실습 중입니다.</p>
<button id="btn">클릭</button>
</body>
</html>
JavaScript
복사
이것을 도식화 해보면?
html
├── head
│ └── title
└── body
├── h1 (id="title")
├── p (class="msg")
└── button (id="btn")
JavaScript
복사
1-3.DOM 조작 예제코드
import "./styles.css";
// 1) 먼저 #app 안에 HTML을 채움
document.getElementById("app").innerHTML = `
<h1 id="title">안녕하세요</h1>
<p class="msg">DOM 실습 중입니다.</p>
<button id="btn">클릭</button>
`;
// 2) 그 다음 DOM을 선택
const title = document.getElementById("title");
const msg = document.querySelector(".msg");
const btn = document.getElementById("btn");
// 3) 버튼 클릭 시에만 DOM 조작되도록 변경
btn.addEventListener("click", function () {
title.textContent = "Hello DOM!";
msg.textContent = "이 문장은 DOM조작으로 바뀐거임.";
alert("버튼이 클릭됨!");
});
JavaScript
복사
2. SPA페이지와 전통적인 방식(MPA)
[SPA방식으로 구축된 페이지]
•
Gmail: 이메일을 클릭해도 페이지가 새로고침되지 않음
•
YouTube: 동영상 간 이동 시 부드러운 전환
•
Netflix: 콘텐츠 탐색 시 끊김 없는 경험
2-1. SPA
•
마치 디지털 잡지나 태블릿 앱
•
하나의 화면에서 내용만 바뀜
•
페이지 전체를 다시 로드하지 않고 필요한 부분만 업데이트
•
사용자 클릭 → JavaScript 실행 → DOM 업데이트 → 화면 부분 변경
2-2.전통적인 웹사이트(MPA)
•
MPA : Multi Page Application):
•
사용자 클릭 → 서버 요청 → 새 HTML 페이지 → 전체 페이지 새로고침
•
새로운 내용을 보려면 페이지를 넘겨야 함 (새로고침)
•
매번 새 페이지를 완전히 다시 로드
2-3. 구현 목포:SPA
•
이후 탐색: JavaScript가 DOM을 조작하여 화면 변경
•
데이터 통신: AJAX/Fetch API로 서버와 JSON 데이터만 주고받음
3. SPA페이지 구축해보기
3-1. 파일구조
위치 | 역할 | 핵심 DOM 포인트 |
index.html | #app 컨테이너에 현재 라우트의 HTML을 주입함 | document.getElementById('app').innerHTML = ... |
views/*.html | 부분 화면(Partial) 원본 | XHR/fetch 또는 동적 로딩으로 내용 문자열을 받아 #app에 삽입 |
js/route.js | 라우트 정보 객체(이름, 대상 HTML, 기본 여부) | 활성 라우트 비교: location.hash |
js/router.js | hashchange 감지 후 올바른 view 로딩 | window.addEventListener('hashchange', handler) |
js/app.js | 라우터 초기화 및 기본 라우트 지정 | new Router([...routes]).init() |
[코드스페이스에 동일한 파일구조 생성]
•
read.me와 license는 필요 x
•
js폴더안에 파일3개
•
[전체 파일구조]
3-2.파일간 관계
파일 | 주요 역할 | 핵심 공개 API(프로퍼티/메서드) | 의존 관계 | 비고 |
route.js | 라우트 정보(이름, 뷰 파일, 기본 여부) 모델 정의함 | Route(name, htmlName, isDefault) 생성자 | 의존받음: app.js, router.js에서 Route 인스턴스 사용함 | 순수 데이터 구조임 |
router.js | 해시 라우팅 처리 + 뷰(HTML partial) 로딩/주입 담당함 | new Router(routes), init() 내부: _findActiveRoute(), _render(route), _onHashChange() | 의존함: DOM(#app), fetch, Route 인스턴스 배열 | #app에 HTML 주입함 |
app.js | 라우트 테이블 구성 및 라우터 초기화(엔트리) 수행함 | (즉시실행 IIFE 내부에서) router.init() 호출함 | 의존함: Router, Route 모듈 | 시작점(bootstrap) 역할임 |
1.
해시?
•
https://example.com#page1 → 책의 'page1' 섹션으로 바로 이동하는 책갈피
•
책은 그대로인데 (페이지 새로고침 없음), 보는 부분만 바뀜
2.
fetch?
•
비동기적으로 서버에서 데이터를 가져오는 현대적 방법
•
Promise 기반 (async/await 사용 가능)
항목 | 뜻 | 핵심 포인트 | 대표 코드 |
URL hash | URL의 #부터 끝까지(fragment identifier)를 뜻함 | 값이 바뀌면 hashchange 이벤트가 발생함 · SPA 라우팅에 많이 사용함 | |
fetch | 네트워크/리소스를 가져오는 표준 Web API | Promise를 반환함 → 응답은 Response 객체 · 본문은 text() / json() 등으로 꺼냄 · 교차 출처는 기본 mode: "cors" 규칙 적용 |
3-3.실제 SPA뼈대코드
1.
app.js
"use strict";
// 모듈로 명시 가져옴 (전역 의존성 제거됨)
import Route from "./route.js";
import Router from "./router.js";
(function () {
function init() {
// 라우트 테이블 정의함
let router = new Router([
new Route("home", "home.html", true), // 기본 라우트
new Route("about", "about.html"),
]);
router.init(); // 이벤트 바인딩 + 첫 렌더
}
init();
})();
JavaScript
복사
2.
route.js
// Route: 라우트 정의를 담는 간단한 자료구조임
export default function Route(name, htmlName, isDefault) {
this.name = name; // ex) "home"
this.htmlName = htmlName; // ex) "home.html"
this.default = !!isDefault; // 기본 라우트 여부
}
JavaScript
복사
3.
router.js
// Router: 해시 라우팅 + view 주입 담당함
export default function Router(routes) {
this.routes = routes || [];
this.outlet = document.getElementById("app"); // 주입 위치
}
// 현재 활성 라우트를 찾아 반환함 (동일)
Router.prototype._findActiveRoute = function () {
let hash = window.location.hash.replace("#", "");
if (!hash) {
let def = this.routes.find(function (r) {
return r.default;
});
return def || this.routes[0];
}
return this.routes.find(function (r) {
return r.name === hash;
});
};
// ✅ 여기에서 body 클래스 토글을 추가함
Router.prototype._applyBackground = function (routeName) {
let body = document.body;
body.classList.remove("bg-home", "bg-about");
if (routeName === "home") body.classList.add("bg-home");
if (routeName === "about") body.classList.add("bg-about");
};
// 해당 라우트의 html을 fetch하여 #app에 주입함
Router.prototype._render = function (route) {
let self = this;
if (!route) {
this.outlet.innerHTML = "<h2>404</h2><p>Route not found</p>";
document.body.classList.remove("bg-home", "bg-about");
return;
}
// ✅ 라우트에 맞춰 배경 클래스 적용
this._applyBackground(route.name);
fetch("./src/views/" + route.htmlName)
.then(function (res) {
return res.text();
})
.then(function (html) {
self.outlet.innerHTML = html;
})
.catch(function (err) {
self.outlet.innerHTML = "<h2>에러</h2><pre>" + String(err) + "</pre>";
});
};
// 해시 변경 시 처리함 (동일)
Router.prototype._onHashChange = function () {
let route = this._findActiveRoute();
this._render(route);
};
// 초기화 (동일)
Router.prototype.init = function () {
this._onHashChange = this._onHashChange.bind(this);
window.addEventListener("hashchange", this._onHashChange);
this._onHashChange();
};
JavaScript
복사
4.
about.html
<h2>⬆️'상단 글자'를 누르면 페이지 부분 변동</h2>
<h1>About</h1>
<p>라우터는 <code>hashchange</code> 이벤트로 현재 해시를 감지하고,</p>
<p>해당하는 HTML partial을 fetch로 가져와 <code>#app</code>에 주입함.</p>
JavaScript
복사
5.
home.html
<h2>⬆️'상단 글자'를 누르면 페이지 부분 변동</h2>
<h1>Home</h1>
<p>이 페이지는 <b>Vanilla JS</b>로 만든 SPA 예제임.</p>
<p>#home, #about 해시로 화면이 바뀌는 것을 관찰해보자.</p>
JavaScript
복사
6.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Vanilla SPA (Modernized)</title>
<style>
nav a {
margin-right: 8px;
}
body {
font-family: system-ui, sans-serif;
}
#app {
margin-top: 16px;
}
</style>
</head>
<body>
<nav>
<a href="#home">Home</a>
<a href="#about">About</a>
</nav>
<div id="app">Loading…</div>
<!-- ES Module: 의존성은 app.js에서 import로 해결됨 -->
<script type="module" src="./js/app.js"></script>
<link rel="stylesheet" href="styles.css" />
</body>
</html>
JavaScript
복사
7.
style.css
/* 1) 기본 리셋 */
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
}
/* 2) 배경 전환 공통 */
body {
transition: background 600ms ease, background-color 600ms ease;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
/* 3) Home/About 배경 그라데이션 */
.bg-home {
background: radial-gradient(
1200px 800px at 20% 20%,
#ffd1ff 0%,
#c1b6ff 40%,
#a18cd1 70%,
#fbc2eb 100%
);
}
.bg-about {
background: linear-gradient(
135deg,
#c0f7ea 0%,
#79e7ff 40%,
#7ab8ff 70%,
#4d6fff 100%
);
}
/* 4) 레이아웃 */
nav {
display: flex;
gap: 12px;
padding: 16px;
}
nav a {
text-decoration: none;
padding: 8px 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.35);
color: #1b1f23;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
transition: transform 150ms ease, box-shadow 200ms ease, background 200ms ease;
}
nav a:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.5);
}
/* 5) 모피즘/글라스 카드 - #app 컨테이너 */
#app {
margin: 24px;
padding: 24px;
min-height: 220px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.28);
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
border: 1px solid rgba(255, 255, 255, 0.35);
box-shadow: 10px 10px 30px rgba(0, 0, 0, 0.1),
-8px -8px 20px rgba(255, 255, 255, 0.55) inset,
8px 8px 18px rgba(0, 0, 0, 0.08) inset;
transition: box-shadow 250ms ease, background 250ms ease;
}
/* 6) 내부 텍스트 기본 */
#app h1 {
margin: 0 0 8px;
}
#app p {
margin: 0 0 6px;
line-height: 1.6;
}
JavaScript
복사


