개선 사항
1. 추가 버튼 클릭 시에 input의 포커스가 사라지는 현상 개선.
2. 빈 목록이 추가되지 않도록 기능 개선.
3. 여러 아이템을 입력한 경우에 스크롤이 자동으로 움직이도록 개선.
4. 기능 별로 함수 분리.
5. 전반적인 코드 가독성 개선.
리팩토링 왜?
사실 서버를 가지고 운영하는 TO DO LIST도 아니고, 어딘가에 제출할 필요가 있는 프로젝트도 아닌 마당에 리팩토링까지 하면서 시간을 쓰는게 별로일 수 있다. 하지만, JavaScript
의 동작원리를 다 알고 구현하는게 아닌 나는 작은 문제에도 고민하고 어려워하는 아주 작은 반딧불이다.(별이 되고 싶지만... 🥲)
그런 내가, 조금이라도 더 공부를 해볼 수 있는 부분이 리팩토링이 아닐까 생각했다. 물론, 다 구현한 후에 엘리님의 코드를 보면서 배운 부분이 많다.
코드의 경우에 HTML, CSS 의 경우에는 짧기도 하고 복잡한 부분이 없었기 때문에 더 손을 대지 않았다. 우선 기능을 구현해보자는 마음으로 마구잡이로 쳤던 JavaScript 코드가 문제였다. 특히, 사용자의 경험을 중요하게 생각해야 하는 프론트엔드의 입장에서 배울 것들이 많았다.
1. 포커스 개선하기!
부끄럽게도 이 부분은 처음에 만들면서 전혀 생각하지 못했다. 사실, 혼자 여러 개의 목록을 만들고 지워보면서 "오오오 된다!🤭" 하며 기뻐했던 나는 어떤게 부족한지, 개선할 수 있는 부분이 무엇인지 보려는 노력이 부족했다. 그래서 마우스 클릭을 통해서 목록을 추가하는 경우에는 입력창의 포커스가 날아가면서 다시 한 번 입력창을 클릭해야한다는 점을 놓쳤다. 아마 서비스가 되는 웹이었고 사용자가 이를 사용했다면 많이 불편했을 것이다.
그래서 이 부분을 개선하기로 했다. 개선하는 방법은 정말 간단하다. input.focus()
를 addList()
함수에 추가해주면 된다.
const = addList() => {
// 이 코드 한 줄로 사용성이 개선될 수 있다!
input.focus()
2. 빈 목록 추가 막기
완성이 된 TO DO LIST에 여러가지 동작을 해보니 문제가 더 있었다. 바로, 아무것도 입력하지 않고 클릭이나 엔터를 입력하는 경우에 빈 목록이 계속 생성된다는 점이었다. 불필요한 동작이었다. 이를 막아주기 위해서 addList()
함수에 간단한 조건문을 추가했다.
// input 에 아무것도 없는 경우 return
const inputValue = input.value;
if (!inputValue) {
input.focus();
return;
}
이 때, 아무 것도 없는 경우에 return
만을 하는 경우에는 또 포커스가 날아가게 된다. 이유는 추가 버튼을 클릭할 때 포커스가 버튼으로 옮겨오는데 return
되면서 아래의 로직들이 실행되지 않고 함수를 종료하게 된다. 그렇기 때문에 포커스를 다시 input으로 옮긴 후에 return
할 수 있도록 했다.
3. 자동 스크롤링 추가
현재, 여러 개의 목록을 계속 생성하는 경우에 스크롤바가 생기기는 하지만 밑으로 스크롤이 자연스럽게 내려가면서 생기지 않고 위에 고정된 채로 목록만 쌓이게 된다. 그래서 굳이 새 아이템이 잘 생성되었는지 보려면 사용자가 직접 내려야하는 불편함이 있다. 이를 개선하기 위해서 자동으로 새 아이템을 따라 스크롤링 되는 기능을 추가했다.
엘리님의 강의에 있는 실전 프로젝트들을 먼저 해보면서 느끼는 점은 작은 실전 문제들의 구성이 정말 좋다는 점이었다. 갑자기 광고는 아니지만...🤣 각 문제들을 해결하기 위해서 찾아보고 고민했던 웹API나 메소드들이 이후에도 활용하기 좋은 것들이라는 생각이 들었다. 특히 scroll 관련해서는 여러 가지를 사용해봤었는데 그 중에 하나를 유용하게 사용해 볼 수 있었다.
newList.scrollIntoView({ block: "center" });
4. 전반적인 코드 가독성 개선
사실, 리팩토링을 하고 싶었던 이유는 이게 컸다. 코드들이 길어질 수록, 원하는 기능들을 마구잡이로 넣을 수록 나중에 나조차도 보기 힘들 것 같다는 생각이 들었다. 그래서 함수들을 보기 좋게 분리하고 불필요하게 길게 작성된 부분은 줄여보기로 했다.
let id = 0;
const createList = (inputValue) => {
const itemRow = document.createElement("li");
itemRow.setAttribute("class", "item-row");
itemRow.setAttribute("data-id", id);
itemRow.innerHTML = `
<div class="item">
<span class="item-content">${inputValue}</span>
<button type="button" class="item-delete">
<i class="fas fa-trash-alt" data-id="${id}"></i>
</button>
</div>
`;
id++;
return itemRow;
};
이 부분은 정말로 줄이는 방법에 대해서 고민을 많이 하다가 결국 엘리님의 강의를 보게 되었는데, innerHTML
로 작성하시는 걸 보고 깜짝 놀랐다. 이미 많이 사용해봤던 것임에도 불구하고 고려조차 못했던 나...🥲 이렇게 하니 각 요소 하나하나를 createElement
해주고 또 appendChild
하는 일이 없어졌다.
또, 기존에는 addList()
함수 안에서 지저분하게 각 요소들을 생성하고 호출했는데, 이를 따로 createList()
라는 함수를 만들어서 보기 편하게 만들었다.
여기서 굳이 id
를 만들면서 휴지통 기능과 관련이 있다.
items.addEventListener("click", (event) => {
const id = event.target.dataset.id;
if (id) {
const toBeDeleted = document.querySelector(`.item-row[data-id="${id}"]`);
toBeDeleted.remove();
}
});
원래는 삭제하는 기능을 새 목록을 만들때마다 이벤트 리스너를 통해서 등록했었다. 하지만, 이벤트 위임(event delegation)
을 이용해서 아이콘의 조상 요소인 items(ul)
에 이벤트 리스너를 등록한 다음에 조건문을 통해서 data-id
를 가지고 있는 경우를 이용할 수 있도록 했다.
toBeDeleted
의 경우는 동일한 data-id
를 갖는 li
를 가져오는 변수이고 이를 삭제할 수 있도록 했다.
여기서 굳이 id
를 임의로 부여해주면서 처리한 이유는 무엇일까? 처음 기능을 개선했을 때는 id
를 이용하지 않고 parentNode
를 여러 번 이용해서 구현을 했다. 작동도 잘 되는 것을 확인했다. 하지만, 확장성을 고려했을 때 parentNode
를 다중으로 사용하는 경우에는 구조에 변동이 생겼을 경우에 예기치 않은 오류가 생길 확률이 높다. 그렇기 때문에 더 명시적인 id
를 통해 이런 부분을 예방할 수 있었다.
다음은?
이제 다음 실전 프로젝트는 간단한 게임을 만들어 볼 예정이다. 고민해야하는 부분도 많고, 아마 눈물의 후기가 되지 않을까 싶다🥲
초보 코린이... 개발자가 되기 위해서 한 걸음, 한 걸음 더 디뎌보자.