[JavaScript] 프로그램 수정하기
in JavaScript on Study
1️⃣ 목표
- Javascript를 공부하다보니 이전에 만든기능들의 수정할 점들이 보이기 시작했습니다.
- 이번 포스트에서는 다음의 두가지 프로젝트를 보완해볼 예정입니다.
[ 1 ] 마우스 이벤트
🍄 addEventListener 써보기(마우스 이벤트) 포스트👈🏻👈🏻
[ 2 ] Todo 리스트
🍄 ToDo리스트 만들기 포스트👈🏻👈🏻
2️⃣ 마우스이벤트 프로젝트 수정하기
(1) 수정하기 전 상태
- 먼저 마우스이벤트 프로젝트를 수정하겠습니다.
- 기존 js코드는 다음과 같았습니다.
const aa = document.querySelectorAll(".hello");
for (let i = 0; i < aa.length; i++) {
function mouseEnterFunc() {
aa[i].style.color = "#" + parseInt(Math.random() * 0xffffff).toString(16);
aa[i].style.fontSize = "300%";
aa[i].innerText = "Hello Mouse!";
}
function mouseLeaveFunc() {
aa[i].style.color = "#" + parseInt(Math.random() * 0xffffff).toString(16);
aa[i].style.fontSize = "200%";
aa[i].innerText = "@@@@@@@@@@@";
}
aa[i].addEventListener("mouseenter", mouseEnterFunc);
aa[i].addEventListener("mouseleave", mouseLeaveFunc);
}
- 이때당시 콜백함수를 잘 알지 못했고 위와 같이
for문
을 돌려서 만들었습니다. - 추후에 위와 같이 사용할때만 함수들은 반복문에서 빼주었고 이벤트를 지정해줄 떄만
forEach
를 이용하였습니다.
const aa = document.querySelectorAll(".hello");
function mouseEnterFunc(event) {
const value = event.target;
value.style.color = "#" + parseInt(Math.random() * 0xffffff).toString(16);
value.style.fontSize = "300%";
value.innerText = "Hello Mouse!";
}
function mouseLeaveFunc(event) {
const value = event.target;
value.style.color = "#" + parseInt(Math.random() * 0xffffff).toString(16);
value.style.fontSize = "200%";
value.innerText = "@@@@@@@@@@@";
}
Array.from(aa).forEach((a) => {
a.addEventListener("mouseenter", mouseEnterFunc);
a.addEventListener("mouseleave", mouseLeaveFunc);
});
- 하지만 이벤트 위임을 이용하면
forEach
로 반복문을 돌릴필요가 없습니다. - 번외로 위 코드에서
style.color
를 직접 바꾸는 것도 매우 비효율적인 작업입니다. 하지만 아직 css적으로 랜덤한 색을 지정하는 방법을 모르기 때문에 이번에 다루지 않겠습니다.(css의 style요소를 js에서 바꿔주는 것을레이아웃(layout) 까지 변경되는 작업이므로트리거 속도가 매우느려지고 비효율적으로 됩니다, 왠만하면 Composite선에서 변경되는 기능을 사용하는 것이 효율적입니다.)
(2) 이벤트 위임 적용하기(mouseenter, mouseleave)
- 먼저 위의 코드에서 수정할 부분은 다음과 같습니다.
Array.from(aa).forEach((a) => {
a.addEventListener("mouseenter", mouseEnterFunc);
a.addEventListener("mouseleave", mouseLeaveFunc);
});
- 이벤트 위임의 대략적인 구현을 말하자면 자식노드에 이벤트가 중복된다면 부모노드에 이벤트를 할당하는 것 입니다.
- 그렇다면 어떤 자식노드 이벤트인지 어떻게 찾을 수 있을까? 각각의 자식노드들을 서로다른 id를 할당하여 구분하면 되지않을까 생각이듭니다.
- 그러나 JavaScript에서는 그럴 필요없이 자식노드와 부모노드를 구분할 수 있는 요소로 구분만 해준다면 각각의 자식노드에게 독립적으로 이벤트가 작동되도록 해줍니다.
const body = document.querySelector("body");
body.addEventListener("mouseenter", (event) => {
if (event.target.className === "hello") {
mouseEnterFunc(event);
}
});
body.addEventListener("mouseleave", (event) => {
if (event.target.className === "hello") {
mouseLeaveFunc(event);
}
});
- 자식노드와 부모노드를 구분하는 요소로 class를 이용하였습니다.
- 하지만 생각한 것처럼 이벤트가 동작하지 않았습니다.
- 그 이유로 MDN사이트에서는 “mouseenter”와 “mouseleave”이벤트에 대해 다음과 같이 말하고 있습니다.
it doesn’t bubble and it isn’t sent to any descendants when the pointer is moved from one of its descendants physical space to its own physical space.
- mozilla(MDN)
- 즉, “mouseenter”와 “mouseleave”는 bubble이 아니기 때문에 자식노드로 물리적 이동이 일어나도 이벤트 포인터가 자식노드로 이동하지 않는다고 합니다.
- 위의 이미지와 같이 “mouseenter”와 “mouseleave”의 bubble속성값이 false로 되어 있습니다.
👉🏻👉🏻👉🏻 mouseenter 이벤트 - MDN
👉🏻👉🏻👉🏻 mouseleave 이벤트 - MDN
(3) 이벤트 위임 적용하기(mouseover, mouseout)
- 그렇다면 부모위임을 이용하기위해 bubble속성값이 true인 이벤트를 이용해야될 것 같습니다.
- 다행히 그중에서 “mouseenter”와 “mouseleave”와 같은 기능을 하는 이벤트가 있는데 “mouseover”와 “mouseout”이벤트입니다.
const body = document.querySelector("body");
body.addEventListener("mouseover", (event) => {
if (event.target.className === "hello") {
mouseEnterFunc(event);
}
});
body.addEventListener("mouseout", (event) => {
if (event.target.className === "hello") {
mouseLeaveFunc(event);
}
});
- 하지만 깊은 계층 구조를 사용하면 전송 되는 이벤트 수가 상당히 많아져 심각한 성능 저하를 일으킬 수 있다고 합니다.
(4) mouseover성능문제 생각해보기
- “mouseover”와 “mouseout”이벤트를 이용하여 성공적으로 이벤트위임을 적용시켰습니다.
- 하지만 굳이 이벤트위임을 사용할 필요가 있을까 의문이듭니다. MDN사이트에서도 잘못사용하면 심각한 성능 저하를 일으킬 수 있다고 합니다.
- 다음은 “mouseover”이벤트가 정확히 어떻게 동작하는지 확인 하기위한 예시입니다.
[ index.html ]
[ app.js ]
const body = document.querySelector("body");
function aaa(event) {
event.target.style.backgroundColor =
"#" + parseInt(Math.random() * 0xffffff).toString(16);
}
body.addEventListener("mouseover", aaa);
- 자식노드마다 이벤트가 감지되어 동작됨을 알 수 있습니다.
- 다시말해서 만약 특정 자식노드에서만 이벤트가 동작하도록하기 위해 다음과 같이 작성하는 것은 성능적으로 비효율적입니다.
body.addEventListener("mouseover", (event) => {
if (event.target.className === "hello") {
mouseEnterFunc(event);
}
});
- “click”과 같은 이벤트는 의도적으로 동작하는 것이기 때문에 이벤트 위임을 쉽게 고려할 수 있지만 “mouseover”와 같은 이벤트는 의도하지 않아도 계속해서 일어나는 동작이기 때문에 상황에 따라서 “mouseenter”와 비교하여 알맞게 선택하여 사용하는 것이 좋을 것 같습니다.
3️⃣ todo리스트 프로젝트 수정하기(1)
(1) 수정하기 전 상태
function paintToDo(newObj) {
const newToDoSet = document.createElement("li");
newToDoSet.id = newObj.id;
const newToDo = document.createElement("span");
if (newObj.is_done === true) {
newToDo.classList.add("A17-del");
}
newToDo.innerText = newObj.text;
newToDo.addEventListener("click", doneFunc);
const deleteButton = document.createElement("button");
deleteButton.innerText = "X";
deleteButton.id = newObj.id;
deleteButton.addEventListener("click", deleteToDo);
newToDoSet.append(newToDo, " ", deleteButton);
toDoList.appendChild(newToDoSet);
}
- 위의 코드는 todo리스트의 리스트를 추가해주는 코드입니다.
- 먼저 click 이벤트가 리스트를 생성할 때마다 지정되도록 되어있습니다. 이것은 별로 좋은 방법이 아니기때문에 이벤트 위임을 이용하여 빼줄 계획입니다.
- 그리고 innerText를 이용하여 텍스트를 추가해주고 있습니다. 이것도 별로 좋은 방법이 아니기 때문에 다른 방법을 찾아보도록 하겠습니다.
(2) 이벤트 위임 적용
toDoList.addEventListener("click", (event) => {
if (event.target.tagName === "SPAN") {
doneFunc(event);
} else if (event.target.tagName === "BUTTON") {
deleteToDo(event);
}
});
- 이벤트를 부모노드에 위임하여 span태그일때와 button태그일때 이벤트가 동작하도록했습니다.
- 위와같이
if
문을 이용하여 다른함수를 호출하도록 지정해줄 수도 있습니다.
(3) delete버튼 대상 찾기
x
버튼으로 표시된 delete버튼을 클릭하면 해당 리스트가 지워지도록 코드를 작성했습니다.- 기존의 코드는 다음과 같습니다.
function deleteToDo(event) {
const target = event.target.parentElement;
target.remove();
toDos = toDos.filter((toDos) => toDos.id !== parseInt(target.id));
saveToDos();
}
x
버튼의 부모노드는 해당리스트를 가리키기 때문에 위와 같이event.target.parentElement
을 이용하여 대상을 지정해도 됩니다.- 하지만 만약 추후의 버튼 위의 새로운 부모노드가 생긴다면
event.target.parentElement.parentElement
로 바꿔줘야 합니다. - 부모노드가 추가로 더 생긴다면
event.target.parentElement.parentElement.parentElement
가 될 것입니다. - 이런식으로 일리리 수정을 하기가 번거롭고 나중에 적절한 타겟 노드를 잘못 지정할 수도 있습니다.
- 차라리
id
를 이용하여 타겟 노드를 지정하는 것이 나을 것 같습니다.
function deleteToDo(event) {
const target = toDoList.querySelector(`li[id="${event.target.id}"]`);
target.remove();
toDos = toDos.filter((toDos) => toDos.id !== parseInt(target.id));
saveToDos();
}
- 위와 같이 버튼과 같은
id
를 타겟 노드(리스트)에 저장해놓으면 됩니다.
4️⃣ todo리스트 프로젝트 수정하기(2)
(1) innerText 사용하지 않기
- 보통 innerText와 textContent와 innerHTML을 비교합니다.
- 그중에서 innerHTML는 태그를 포함한 내용을 그대로 파싱하며 기능도 유지됩니다. 나머지 둘과의 차이가 명확하기 때문에 구분하는게 어렵지 않습니다.
- 하지만 innerText와 textContent의 차이점에 대해서는 햇갈릴 수 있는데, 아래의 MDN에서 구체적인 예시와 설명을 통해 쉽게 이해할 수 있습니다.
👉🏻👉🏻👉🏻 Node.innerText - MDN
- 위의 MDN사이트의 설명에 의하면 innerText는 기능들을 적용하여 사용자에게 보여지는 텍스트로 파싱을 하게 됩니다. 심지어 공백도 자동으로 보정되어 파싱을 합니다. 그에반해 textContent는 태그를 무시하고 오직 텍스트부분만을 파싱합니다.
- 당연히 innerText는 거쳐야할 단계가 있기 때문에 textContent보다 파싱이 느릴 것 입니다. 그렇기 때문에 단순히 텍스트만을 파싱하는 것이 목적이라면 textContent를 사용하는 것이 좋습니다.
- 추가적으로 innerText의 탄생유례에 대해 생각해본다면, IE가 textContent를 지원하지 않기 때문에 이러한 IE위해 만들어진 프로퍼티 입니다. (현재 IE는 2022년 6월 15일부로 지원을 종료한다고함..)
- 기능적으로나 탄생배경으로나 innerText는 사용하지 않는편이 좋을 것같습니다.
(2) textContent vs innerHTML
- 이제 textContent와 innerHTML의 사용을 고민해보면될 것 같습니다.
- 먼저 todo리스트 프로젝트를 수정한 모습입니다.
< textContent사용 >
< innerHTML >
- 만약 추가해줄 내용의 깊이가 더 깊어질수록 innerHTML을 사용하는편이 더 직관적일 것 같습니다.
- 물론 성능면에서 textContent을 사용하는 편이 나을 것이고 위와같은 코드정도는 textContent를 사용해도 가독성이 크게 떨어지지 않을 것같습니다.
- 하지만 또 다르게 생각해본다면, 성능의 차이는 미미하다고 하고 위와같이 자주 변경되는 요소가 아니고 단순히 리스트가 추가될 때만 이용하는 것이라면 걱정없이 innerHTML을 사용하여 가독성을 챙기는 것도 나쁘지 않을 것 같습니다.
- 결론적으로 이 부분도 상황에 맞게 사용하는 것이 좋을 것 같습니다.
(3) innerHTML 보안성 문제?
- 하지만 innerHTML사용함에 있어서 주의해야할 점이 있습니다.
- 현재 구현한 todolist같은 경우 사용자의 입력을 받은 내용을 innerHTML으로 작성했습니다.
- 만약, <script>태그를 이용해 JavaScript코드를 작성하면 실행시킬 수도 있습니다. 다행히 HTML5에서는 <script>는 실행되지 않도록 처리하였습니다.
- 하지만
<img src='x' onerror='alert(1)'>
와 같이 다른 태그를 이용하여 JavaScript기능을 이용하는 방법도 있을뿐더러 여전히 위험할 가능성이 있습니다.
👉🏻👉🏻👉🏻 참고: Node.innerHTML - MDN
- 결론적으로 innerHTML은 사용자 입력을 직접적으로 받도록 설계해서 사용하지 않는 것이 좋을 것 같습니다.
- 사용자 입력을 처리하여 사용하던지, innerHTML을 내부적으로만 사용하는 것이 안전합니다.
(4) innerHTML 안전하게 사용하기
- 위에서도 언급했듯이 왠만하면 사용자 입력을 직접적으로 받아서 사용하지 않는 것이 좋습니다.
- 그래도 굳이 사용자 입력에 대해 innerHTML을 사용하고 싶다면 다음과 같이 사용자 입력을 변경해서 사용하면될 것 같습니다.
const temp = document.createElement("b");
temp.textContent = newObj.text;
newToDoSet.innerHTML = `${temp.innerHTML}`;
- textContent로 포메팅하여 처리한 뒤 다시 innerHTML로된 값을 대입합니다.
- 그럼 왜 다시 innerHTML로 대입할까요. 위에서 textContent로 포메팅한 값을 다음과 같이 두가지로 출력해보겠습니다.
console.dir(`textContent: ${temp.textContent}`);
console.dir(`innerHTML: ${temp.innerHTML}`);
innerHTML: <b>sdfds</b>
- 위와같이 값이 저장되어 있습니다 만약 textContent을 대입했다면 포메팅을 안한거나 마찬가지일 것입니다.