JavaScript DOM 요소 편집 함수 중복 오류
다음과 같은 목록이 있다고 가정해 봅시다:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
목록 자체와 그 항목들을 별도의 변수에 저장해 보겠습니다:
let ul = document.querySelector('ul');
let lis = document.querySelectorAll('li');
목록 항목을 나타나는 입력 필드(input)로 편집할 수 있도록 만들어 보겠습니다:
for (let li of lis) {
li.addEventListener('click', function func() {
let input = document.createElement('input');
input.value = list.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
});
}
이제 목록에 새로운 항목을 추가할 수 있기를 원한다고 가정해 봅시다. 이를 위해 목록 아래에 해당 입력 필드가 있다고 가정합니다:
<input id="adder">
이 입력 필드에 대한 참조를 변수에 저장하겠습니다:
let adder = document.querySelector('#adder');
입력 필드가 포커스를 잃을 때(blur) 입력 필드에서 가져온 텍스트를 가진 새 항목이 목록에 추가되도록 만들어 보겠습니다:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});
이제 새로 추가된 항목들도 편집할 수 있기를 원한다고 가정해 봅시다. 항목 목록에 클릭 핸들러를 추가했을 때는 이 항목들이 아직 존재하지 않았기 때문에 자체적으로는 편집 기능이 작동하지 않을 것입니다.
이 문제를 해결할 수 있는 여러 가지 방법을 살펴보겠습니다.
첫 번째 해결책
가장 간단한 해결책은 func 함수의 코드를 복제하여
새로 생성된 항목에도 동일하게 적용하는 것입니다:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function func() {
// 여기서 코드를 중복합니다
});
ul.append(li);
});
물론, 이 해결책의 단점은 명확합니다. 코드를 중복하는 것은 올바르지 않습니다.
두 번째 해결책
중복 문제를 해결하기 위해
func 함수를 외부로 빼내어
함수 선언(Function Declaration)으로 만드는 것이 논리적입니다:
function func() {
let input = document.createElement('input');
input.value = list.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
여기서 문제가 발생합니다.
문제는 우리의 함수가 외부 스코프에서 가져온
변수 li를 사용했다는 점입니다.
하지만 함수를 외부로 빼낸 후에는
이 변수가 보이지 않게 됩니다!
이 문제를 해결하기 위해
우리의 li를 매개변수로 전달하겠습니다:
function func(li) {
let input = document.createElement('input');
input.value = list.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
그리고 여기서 우리의 해결책이 또 다른 문제를 야기합니다. 문제는 이벤트 핸들러에 매개변수를 간단히 전달할 수 없다는 점입니다:
for (let li in lis) {
li.addEventListener('click', func(li)); // 작동하지 않습니다!
}
이 문제를 해결하기 위해 익명 핸들러 내부에서 우리의 함수를 호출하면 됩니다:
for (let li of lis) {
li.addEventListener('click', function() {
func(li);
});
}
그리고 새로운 목록 항목을 생성할 때도 비슷한 방식으로 처리하겠습니다:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function() {
func(li);
});
ul.append(li);
});
세 번째 해결책
더 우아한 해결책이 존재합니다. 이벤트 위임을 활용하면 됩니다. 이 경우 새로운 목록 항목에 대한 문제가 발생하지 않습니다:
ul.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') { // 입력 필드가 아닌 li에 대한 클릭을 캐치합니다
let li = event.target;
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
});
}
});
이 경우 목록 항목을 순회하는 루프가 전혀 필요하지 않으며, 새로운 목록 항목을 생성하는 코드는 다음과 같이 줄어들게 됩니다:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});