일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 웹사이트광고
- 매물
- 오마카세
- 서남물재생12번코트
- 바닐라 js
- 마곡나루
- MVVM
- 물재생테니스장
- 강서
- 강서테니스
- 다중 목적지
- 지도 API
- 네이버 map api
- 네이버 maps
- 하드코트
- 테니스코트
- 스시교메이
- 뷰모델
- 서울특별시 공공서비스
- 서남물재생테니스장
- 애드핏 광고
- 도톰카츠 청라점
- JS
- 자바스크립트
- 상태관리
- 카카오 애드핏
- vanila js
- 네이버 지도
- 렌더링 시스템
- 도톰카츠
- Today
- Total
jojokiki
바닐라 js로 상태관리 할 수 있을까? [ #1 ] 본문
상태관리
프론트에서 규모가 커지면 커질수록 복잡해지는 것이 바로 상태 관리입니다.
바닐라 js로 개발해야 한다면 상태 관리를 어떻게 해야 할까? 또 상태 관리를 잘할 수 있는 방법은 무엇일까? 여러 방법이 있겠지만
view와 viewModel을 정의 후 VM의 변화에 따라 렌더링 하는 방식으로 만들어 보려고 합니다. crud가 되는 간단한 투두 앱을 목표로 만들어 보겠습니다.
먼저
어떤식으로 작동하면 좋을지 작성해두고 구현하는 방식으로 진행해보려고 합니다.
app.js
new ViewBuilder().load('todo.html', '#body', (rView) => {
const todoModel = new Todo();
rView.init((rView) => {
}).on(todoModel, (v, m) => {
})
})
todo.html을 로드 후 #body에 삽입합니다. 그리고 #body 안에 삽입한 코드를 관리할 수 있는 view를 람다의 인자로 받았습니다.
람다의 본문에서는 todoModel이라는 뷰 모델을 정의해 줍니다.
rView에는 init과 on이 있습니다.
1. init은 캐싱을 지원합니다. 만약 해당 파일이 여러 번 실행된다면 변하지 않는 부분을 따로 관리하기 위함입니다. 하지만 지금은 ViewBuilder 인스턴스가 매번 생성되니까 유효한 기능은 아닐 것 같습니다. 지금은 초기 시점과 렌더링 시점을 분리하는 목적으로만 사용하고 나중에 고쳐보는 걸로 넘어가겠습니다.
2. on은 모델과 렌더링 시 실행될 람다를 등록합니다. 해당 모델에 변화가 발생하면 람다를 실행하게 됩니다. 람다에서는 이름을 재정의할 기회를 주기 위해 인자를 제공해 주었습니다.
위 코드가 동작하도록 구현해 보겠습니다.
ViewBuilder 구현
export class ViewBuilder {
load(htmlPath, key, block) { //file을 불러와서 렌더링
this.#load(htmlPath).then((htmlEl) => {
block(this.#append(htmlEl, key))
})
}
}
load에서는 html 경로와 key 그리고 람다를 인자로 받습니다. 실제 html를 로드하는 코드는 #load에게 맡기도록 하고 로딩이 완료된 html은 지정한 key에 삽입해 주도록 #append에 맡기겠습니다. #append는 삽입한 view를 block에 제공해 줍니다.
async #load(htmlPath) {
if (this.#htmlMap.has(htmlPath)) {
return (await this.#htmlMap.get(htmlPath))
} else {
const promise = htmlLoader(htmlPath)
this.#htmlMap.set(htmlPath, promise)
return (await promise)
}
}
htmlLoader는 html 파일의 경로를 받아 실제로 로딩하는 역할을 합니다. 같은 템플릿을 반복해서 요청할 때 네트워크 요청이 반복되지 않도록 htmlMap에 캐시를 잡았습니다. htmlPath가 캐시에 있으면 저장된 프로미스를 가져와 실행합니다.
export function htmlLoader(url){
return fetch(`./templates/${url}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
const wrapper = document.createElement("div");
const ElCollection = new DOMParser().parseFromString(html, 'text/html').body
if(ElCollection !== null) {
const children = ElCollection.children
Array.from(children).forEach(element => {
wrapper.appendChild(element);
});
const element = wrapper.children[0];
if (element instanceof HTMLElement) {
return element; // HTMLElement 반환
} else {
throw new Error("HTML 파일의 내용이 올바르지 않습니다. HTML 요소가 존재하지 않습니다.");
}
}
})
.catch(error => {
console.error(`Error loading ./templates/${url}:`, error);
// 에러 발생 시 null 반환
throw new Error("HTML 파일을 불러오는 중 오류가 발생했습니다.");
});
}
html 파일의 위치는 반드시 templates 하위에 있도록 정해놨습니다. 그리고 파일을 HTMLElment로 변환해 줍니다.
#append(htmlEl, key) {
const view = new ViewBuilder()
view.#element = htmlEl.cloneNode(true)
if (key === "") {
if (this.#element === undefined) {
throw new Error("element가 정의되지 않았습니다.");
} else {
this.#element.appendChild(view.#element);
}
} else {
if (this.#element === undefined) {
Dom.qs(key).appendChild(view.#element);
} else {
this.#element.querySelector(key).appendChild(view.#element);
}
}
return view
}
#append은 항상 새로운 view를 생성합니다. 그리고 기존의 element의 특정 위치(Key에 해당하는)에 해당 view를 삽입합니다.
최초의 viewBuilder가 load 하는 시점엔 key는 있지만 element는 존재하지 않기 때문에
Dom.qs(key).appendChild(view.#element);
이 실행됩니다. 저는 #body로 지정해놓았으니 id가 body인 태그에 삽입됩니다.
파일 만들기
index.html
이제 #body가 있는 Index.html을 만들어보겠습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<script type="module" src="./app.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.todo-form {
margin-bottom: 20px;
}
.todo-input {
padding: 8px;
width: 70%;
margin-right: 10px;
}
.todo-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.todo-list {
list-style-type: none;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #ddd;
}
.todo-text {
cursor: pointer;
}
.todo-text.completed {
text-decoration: line-through;
color: #888;
}
.delete-button {
background-color: #f44336;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="body"></div>
</body>
</html>
간단하게 스타일링해 주고 id가 body인 div 태그를 만들어 주었습니다. 이 div 태그에 todo.html을 삽입해야 하기 때문에 todo.html 파일을 만들겠습니다.
todo.html
<div>
<h1>Todo List</h1>
<div class="form">
<input type="text" class="todo-input" placeholder="Add a new task...">
<button class="todo-button">
<i class="fas fa-plus"></i>
Add
</button>
</div>
<div class="todo-list-container">
<p><i class="fas fa-tasks"></i>uncompleted tasks</p>
<ul class="todo-list"></ul>
</div>
<div class="todo-list-container">
<p><i class="fas fa-check-circle"></i>completed tasks</p>
<ul class="completed-todo-list"></ul>
</div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
div {
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 40px;
font-weight: 300;
letter-spacing: 1px;
}
.form {
display: flex;
gap: 10px;
margin-bottom: 40px;
position: relative;
}
.todo-input {
flex: 1;
padding: 15px 20px;
border: none;
border-radius: 8px;
font-size: 16px;
background-color: #f5f6fa;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.todo-input:focus {
outline: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.todo-button {
padding: 15px 25px;
background-color: #4a69bd;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.todo-button:hover {
background-color: #3c56a8;
transform: translateY(-2px);
}
.todo-list-container {
background-color: #fff;
padding: 25px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.todo-list-container p {
color: #7f8c8d;
margin-bottom: 20px;
font-weight: 500;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 8px;
}
.todo-list-container p i {
color: #4a69bd;
}
.todo-list-container:last-child p i {
color: #2ecc71;
}
.todo-list,
.completed-todo-list {
list-style: none;
padding: 0;
margin: 0;
}
</style>
</div>
todo.html은 크게
1. 투두의 내용을 입력할 수 있는 form
2. 완료되지 않은 리스트
3. 완료된 리스트
로 나누어 보았습니다.
이제 실행해보면~
개발자도구에서 dom을 확인해보면
body안에 todo.html 파일이 잘 삽입된것을 확인할 수 있습니다.
현재까지 만든 파일은 위와 같습니다.
다음엔 form에 내용을 입력하면 리스트에 등록될 수 있도록 렌더링을 구현해보겠습니다~
'IT' 카테고리의 다른 글
바닐라 js로 상태관리 할 수 있을까? [ #2 ] (0) | 2025.04.28 |
---|---|
다중 목적지를 지원하는 지도 (+네이버 maps) (4) | 2025.04.21 |
개인 웹사이트에 카카오 애드핏 적용방법 (심사 10분컷!) (0) | 2025.04.20 |