스터디 과제 - 요구하는 기능을 구현하는 목적으로 만든 프로젝트입니다
localStorage 저장
- 현재의 스티커 상태를 localStorage에 저장합니다
- 스티커의 상태가 변경되는 모든 상황마다 저장해야 합니다. (ex. 이동, 추가, 삭제 등)
- 스티커의 상태를 관리하기 위해서 객체 지향 프로그래밍으로 스티커와 항목을 클래스로 만들면 좋습니다. 다른 방법을 사용해도 상관없습니다
- 현재 스티커의 상태를 객체형태의 데이터로 만들어야 합니다. 그래야 문자열로 만들어 localStorage에 저장할 수 있습니다.- 최초 화면 로드시 localStorage에 저장되어 있는 스티커 정보를 가져와 그대로 다시 복원합니다
첫 번째 요구사항에서 로컬 스토리지를 이용하여 데이터를 추가/삭제하는 기능 요구
프로젝트를 시작할 때 Class를 사용하여 만들어 두었기 데이터를 쉽게 추가/삭제 할 수 있을거라 생각했지만 예상하지 못했 던 곳에서 다양한 문제점이 발생하였습니다
※ HTML과 CSS는 V1과 비교해 봤을 때 크게 변화된 게 없어 생략
app > StickerLayout > Sticker > StickerList
import StickerLayout from "./stickerLayout.js";
window.addEventListener("DOMContentLoaded", (e) => {
const stickerAdd = document.querySelector("#sticker-add");
const stickerRemoveAll = document.querySelector("#sticker-remove-all");
const stickerLayout = new StickerLayout();
stickerLayout.render(document.body);
// 스티커 추가
stickerAdd.addEventListener("click", e => {
stickerLayout.createSticker();
});
// 스티커 전부 삭제
stickerRemoveAll.addEventListener("click", e => {
stickerLayout.removeStickerAll();
});
});
import Sticker from "./sticker.js";
export default class StickerLayout {
constructor() {
this.el = null;
this.count = 0;
this.zIdx = 0;
this.stickerList = [];
this.initialPositionX = 5;
this.initialPositionY = 5;
this.fetchDatas();
this.initElement();
}
initElement() {
const element = document.createElement("div");
element.classList.add("sticker-wrapper");
// 다 없어져야 할 로직
element.addEventListener("removeSticker", this.handleClickRemoveSticker.bind(this));
element.addEventListener("changeZindex", this.handleMousedownChangeZindex.bind(this));
element.addEventListener("changeSticker", this.handleClickUpdate.bind(this));
element.addEventListener("updateSticker", this.setStorage.bind(this));
this.el = element;
}
fetchDatas() {
const storage = localStorage;
const datas = JSON.parse(storage.getItem("stickers"));
if (datas) {
this.stickerList = datas;
}
}
setStorage() {
const storage = localStorage;
storage.setItem("stickers", JSON.stringify(this.stickerList));
}
render(parent) {
this.renderSticker();
parent.appendChild(this.el);
}
renderSticker() {
this.stickerList = this.stickerList?.map(data => {
const sticker = new Sticker(data);
sticker.render(this.el);
return sticker;
});
}
createSticker() {
const data = {
id: `sticker_${crypto.randomUUID()}`,
stickerCount: ++this.count,
zIdx: ++this.zIdx,
position: {
currentX: this.initialPositionX,
currentY: this.initialPositionY
},
parentClientRect: this.el.getBoundingClientRect(),
};
const sticker = new Sticker(data);
// 필요한 데이터만 저장한다
this.stickerList.push(sticker);
sticker.render(this.el);
this.setStorage();
// 아이템 추가 후 초기값에 10을 더해준다
this.initialPositionX = this.initialPositionX + 10;
this.initialPositionY = this.initialPositionY + 10;
}
removeSticker(id) {
this.stickerList = this.stickerList.filter(sticker => sticker.id !== id);
this.count--;
this.initialPositionX = this.initialPositionX - 10;
this.initialPositionY = this.initialPositionY - 10;
}
removeStickerAll() {
this.stickerList.forEach(sticker => {
this.removeSticker(sticker.id);
sticker.el.remove();
});
this.setStorage();
}
updateZindex(id) {
const updateSticker = this.getSticker(id);
if (updateSticker[0].zIdx !== this.zIdx) {
updateSticker[0].zIdx = ++this.zIdx;
}
}
getSticker(id) {
return this.stickerList.filter(sticker => sticker.id === id);
}
handleClickRemoveSticker(e) {
const { id } = e.detail;
this.removeSticker(id);
this.setStorage();
}
handleMousedownChangeZindex(e) {
const { id } = e.detail;
this.updateZindex(id);
this.setStorage();
}
handleClickUpdate(e) {
const { id, startParentID, endParentID } = e.detail;
const startSticker = this.getSticker(startParentID)[0];
const endSticker = this.getSticker(endParentID)[0];
const removeList = startSticker.removeList(id);
endSticker.updateList(removeList[0]);
this.setStorage();
}
}
import StickerList from "./stickerList.js";
export default class Sticker {
constructor(options) {
this.el = null;
this.listEl = null;
this.itemList = [];
this.listCount = 0;
this.isDraggable = false;
Object.assign(this, options);
this.titleColor = options.titleColor || "";
this.bgColor = options.bgColor || this.makeBgColor();
this.position.shiftX = 0;
this.position.shiftY = 0;
this.initElement();
this.initBindEvent();
}
initElement() {
const sticker = document.createElement("div");
sticker.classList.add("sticker");
sticker.id = this.id;
this.setStyle(sticker);
const header = this.createHeader();
const contents = this.createContents();
sticker.appendChild(header);
sticker.appendChild(contents);
this.el = sticker;
}
initBindEvent() {
const header = this.el.querySelector(".sticker-header");
// DRAG 이벤트 바인딩
header.addEventListener("mousedown", this.dragStart.bind(this));
header.addEventListener("mouseup", this.dragEnd.bind(this));
// customEvent
this.el.addEventListener("removeList", this.handleClickRemoveList.bind(this));
this.el.addEventListener("chageList", this.handleClickUpdate.bind(this));
}
createHeader() {
const header = document.createElement("div");
header.classList.add("sticker-header", "cursor-move");
const headingTitle = document.createElement("h3");
headingTitle.textContent = `STICKER ${this.stickerCount}`;
headingTitle.style.color = this.titleColor;
const btnSettings = document.createElement("div");
btnSettings.classList.add("sticker-setting");
const addListBtn = document.createElement("button");
addListBtn.classList.add("btn");
addListBtn.id = "sticker-list-add";
addListBtn.type = "button";
addListBtn.textContent = "항목추가";
addListBtn.addEventListener("click", this.handleClickAdddList.bind(this));
const removeSticker = document.createElement("button");
removeSticker.classList.add("btn");
removeSticker.id = "sticker-remove";
removeSticker.type = "button";
removeSticker.textContent = "스티커삭제";
removeSticker.addEventListener("click", this.handleClickRemoveSticker.bind(this, this.id));
btnSettings.appendChild(addListBtn);
btnSettings.appendChild(removeSticker);
header.appendChild(headingTitle);
header.appendChild(btnSettings);
return header;
}
createContents() {
const contents = document.createElement("div");
contents.classList.add("sticker-contents");
const list = document.createElement("ul");
list.classList.add("sticker-list");
contents.appendChild(list);
this.listEl = list;
return contents;
}
render(parent) {
this.renderList();
parent.append(this.el);
}
renderList() {
this.itemList = this.itemList?.map(data => {
const list = new StickerList(data);
list.render(this.listEl);
return list;
});
}
// CRUD
createList() {
const data = {
id: `list_${crypto.randomUUID()}`,
stickerCount: this.stickerCount,
listCount: ++this.listCount,
parentEl: this.listEl,
}
const list = new StickerList(data);
list.render(this.listEl);
this.itemList.push(list);
}
removeList(id) {
const removeListIdx = this.itemList.findIndex(item => item.id === id);
const removeList = this.itemList.splice(removeListIdx, 1);
this.updateSticker();
return removeList;
}
updateList(list) {
this.itemList.push(list);
}
// DRAG
dragStart(e) {
if (e.target.tagName === "BUTTON" || e.target.tagName === "LI") return false;
e.preventDefault();
const pageX = e.pageX;
const pageY = e.pageY;
const clientX = e.clientX;
const clientY = e.clientY;
const currentElX = this.el.getBoundingClientRect().x;
const currentElY = this.el.getBoundingClientRect().y;
const parentX = this.parentClientRect.x;
const parentY = this.parentClientRect.y;
// 현재 선택 된 스티커가 가장 상단에 올 수 있도록 zindex값을 변경한다
const event = new CustomEvent("changeZindex", { bubbles: true, detail: { id: this.id } });
this.el.dispatchEvent(event);
this.el.style.zIndex = this.zIdx;
this.isDraggable = true;
this.position.shiftX = clientX - (currentElX - parentX);
this.position.shiftY = clientY - (currentElY - parentY);
this.setPosition(pageX, pageY);
document.addEventListener("mousemove", this.dragMove.bind(this));
}
dragMove(e) {
if (this.isDraggable) {
this.setPosition(e.pageX, e.pageY);
}
}
dragEnd(e) {
this.updateSticker();
this.isDraggable = false;
document.removeEventListener("mousemove", this.dragMove);
}
// SET, GET
setPosition(pageX, pageY) {
const currentX = pageX - this.position.shiftX;
const currentY = pageY - this.position.shiftY;
this.el.style.left = `${currentX}px`;
this.el.style.top = `${currentY}px`;
this.position.currentX = currentX;
this.position.currentY = currentY;
}
setStyle(target) {
const { currentX, currentY } = this.position;
target.style.top = `${currentY}px`;
target.style.left = `${currentX}px`;
target.style.zIndex = this.zIdx;
target.style.backgroundColor = `rgba(${this.bgColor}, 0.8)`;
}
makeBgColor() {
const result = [];
let sum = 0;
for (let i = 0; i < 3; i++) {
const num = Math.floor(Math.random() * 255);
sum += num;
result.push(num);
}
this.titleColor = (sum > 500) ? "#333" : "#fff";
return result.join();
}
updateSticker() {
const event = new CustomEvent("updateSticker", { bubbles: true, detail: { id: this.id } });
this.el.dispatchEvent(event);
}
// HANDLER
handleClickAdddList(e) {
this.createList();
}
handleClickRemoveList(e) {
const { id } = e.detail;
this.removeList(id);
}
handleClickRemoveSticker(id) {
const event = new CustomEvent("removeSticker", { bubbles: true, detail: { id } });
this.el.dispatchEvent(event);
this.el.remove();
this.el = null;
}
handleClickUpdate(e) {
const event = new CustomEvent("changeSticker", { bubbles: true, detail: e.detail });
this.el.dispatchEvent(event);
}
}
export default class StickerList {
constructor(options) {
this.el = null;
this.helper = null;
this.isDraggable = false;
this.startParentID = null;
this.endParentID = null;
this.position = {};
Object.assign(this, options);
this.content = options.content || `목록${this.stickerCount}-${this.listCount}`;
this.initElement();
this.initBindEvent();
}
initElement() {
const li = document.createElement("li");
li.classList.add("cursor-move");
li.id = this.id;
const div = document.createElement("div");
div.textContent = this.content;
const remove = document.createElement("button");
remove.classList.add("btn");
remove.type = "button";
remove.textContent = "삭제";
remove.addEventListener("click", this.handleClickRemove.bind(this, this.id));
li.appendChild(div);
li.appendChild(remove);
this.el = li;
}
initBindEvent() {
this.el.addEventListener("mousedown", this.dragStart.bind(this));
}
render(parent) {
parent.appendChild(this.el);
}
// DRAG
dragStart(e) {
if (e.target.tagName === "BUTTON") return false;
e.preventDefault();
this.setHelper();
const pageX = e.pageX;
const pageY = e.pageY;
const clientX = e.clientX;
const clientY = e.clientY;
const currentElX = this.el.getBoundingClientRect().x;
const currentElY = this.el.getBoundingClientRect().y;
this.isDraggable = true;
this.startParentID = this.el.closest(".sticker").id;
this.position.shiftX = clientX - currentElX;
this.position.shiftY = clientY - currentElY;
this.setPosition(pageX, pageY);
this.el.classList.add("transper");
document.addEventListener("mousemove", this.dragMove.bind(this));
}
dragMove(e) {
if (this.isDraggable) {
this.helper.style.display = "none"; // 마우스에 있는 녀석 잠깐 숨겼다가
// const targetElement = document.elementFromPoint(e.clientX, e.clientY).closest("li"); // 마우스 아래 있는 요녀석만 찾아내고
const cursorElement = document.elementFromPoint(e.clientX, e.clientY);
const listElement = cursorElement.closest("ul");
const listItemElement = cursorElement.closest("li");
this.helper.style.display = "flex"; // 마우스에 있는 녀석 다시 보여준다
if (listItemElement) { // li
const y = listItemElement.getBoundingClientRect().y + (listItemElement.getBoundingClientRect().height / 2);
if (e.pageY < y) {
listItemElement.before(this.el);
}
else {
listItemElement.after(this.el);
}
}
else if (listElement && listElement.children.length < 1) { // ul
listElement.appendChild(this.el);
}
this.setPosition(e.pageX, e.pageY);
}
}
dragEnd(e) {
this.isDraggable = false;
this.helper.remove();
this.helper = null;
this.el.classList.remove("transper");
this.endParentID = this.el.closest(".sticker").id;
this.checkOfChangeParent();
document.removeEventListener("mousemove", this.dragMove);
}
// SET, GET
setPosition(pageX, pageY) {
const currentX = pageX - this.position.shiftX;
const currentY = pageY - this.position.shiftY;
this.helper.style.left = `${currentX}px`;
this.helper.style.top = `${currentY}px`;
}
setHelper() {
this.helper = this.el.cloneNode(true);
this.helper.classList.add("helper");
this.helper.style.position = "absolute";
this.helper.style.zIndex = "1000";
this.helper.addEventListener("mouseup", this.dragEnd.bind(this));
document.body.append(this.helper);
}
checkOfChangeParent() {
const eventOptions = {
bubbles: true,
detail: {
id: this.id,
startParentID: this.startParentID,
endParentID: this.endParentID,
}
};
const event = new CustomEvent("chageList", eventOptions);
this.el.dispatchEvent(event);
}
// HANDLER
handleClickRemove(id) {
const event = new CustomEvent("removeList", { bubbles: true, detail: { id: id } });
this.el.dispatchEvent(event);
this.el.remove();
this.el = null;
}
}
많은 기능을 하는 어플리케이션이 아님에도 불구하고 요구하는 기능을 추가하다보니 읽기도 어려워지고 로직끼리 연결되어 있는 부분이 많아 사이드이펙트도 많이 발생하였습니다 이 이상 진행하기 어려워 리팩토링을 진행하려고 합니다
수정해야할 범위를 정확하게 예측할 수 없고 어떤방식으로 리팩토링을 진행해야할 것인가에 대해서 정해진 바가 없지만, 리팩토링을 진행 하면서 개선해야할 내용 및 추가해야될 내용은 업데이트예정임
2022.10.15
DOM을 직접 변경하는 방식이 아닌 상태로 인해 DOM이 변경될 수 있도록 관련 내용 학습중
DOM을 직접 변경하는 것이 어떠한 문제점이 있는지, 상태로 DOM을 관리하게 되면 어떤 장점이 있는지도 같이 파악