프로그래밍을 하다가 트러블이 생기면 항상 해결한 뒤 이쁘게 포장해서 글을 올리곤 했다. 하지만 글 하나를 쓰는데 시간이 너무 오래 걸리기 때문에 짧게 핵심만 담을 카테고리가 필요했다.
👀 문제 정의
이번 팀 과제는 프론트엔드를 바닐라 자바스크립트만을 이용해 구현한다. 일부 DOM 엘리먼트를 변화시키고 싶을 때 getElementById나 querySelector로 특정 요소를 지정해야 하는데, 만약 어떤 DOM 엘리먼트가 로딩 되기 전에 엘리먼트를 선택하려고 하면 아래와 같이 친숙한 오류를 볼 수 있다.
이벤트리스너를 붙이려고 보니 null값이네? 라는 오류다. 해당 라인으로 가서 어떤 코드인지 보자.
document.getElementById("btnLogout").addEventListener("click", goLogout);
btnLogout이라는 id 선택자의 DOM 엘리먼트를 가져와서 click 이벤트가 발생했을 때 goLogout함수를 실행하도록 한다. 그럼 null을 리턴한 부분을 찾아보자.
console.log(document.getElementById("btnLogout")); // null
btnLogout id를 가진 DOM 엘리먼트를 제대로 가져오지 못하고 있었다.
🔍 조사
현재 진행하고 있는 프로젝트에서 btnLogout id를 가진 요소는 로그인한 유저가 클릭했을 때 로그아웃을 시켜주는 로그아웃 버튼이다.
네비게이션 바를 각 페이지마다 복붙하지 않기 위해 inject.js를 이용해 페이지가 로딩될 때 nav.html을 fetch함수로 가져와 보여주도록 구성했는데, 이때 fetch함수로 가져온 nav.html을 화면에 보여주기 전에 getElementById로 선택한 게 문제였다. 당연히 로딩되기 전에 '쟤 쓸거야!'라고 지목하면 null을 뱉어내는 게 맞았다.
🧐 해결과정
1. setTimeout
처음 시도한 해결 방법은 이거다.
setTimeout(() => {
// nav바 로딩 지연으로 setTimeout설정
document.getElementById("btnLogout").addEventListener("click", goLogout);
const logos = document.querySelectorAll(".logo");
for (let logo of logos) {
logo.addEventListener("click", goHome);
}
document.getElementById("menuStart").addEventListener("click", goQuiz);
document.getElementById("menuLogin").addEventListener("click", goLogin);
}, 500);
시간을 지연시키는 setTimeout함수 안에 넣어버리는 것.
nav.html이 늦게 로딩되는 게 문제라면 그만큼 지정해주는 시간을 늦춰버리면 되는 거 아닌가? 적당히 500ms만큼 지연을 주고 실행하니 제대로 btnLogout id의 DOM요소가 선택됐고 잘 실행이 됐다.
하지만 문제가 있었다.
크롬 개발자도구 네트워크 탭에서 네트워크 제한을 걸 수 있는데 느린 3G로 설정하고 실행하니 다시 에러가 발생했다. 당연하게도 접속환경이 느리다면 500ms를 지연시켜줘도 nav.html를 불러오는데 걸리는 시간이 5초, 10초가 되니 500ms 지연시키는 것 가지고는 제대로 작동시킬 수 없었다.
그렇다고 지연시간을 5000ms 10000ms로 늘리면 빠른 환경에서 접속한 사람들이 5초 10초동안 버튼을 작동시킬 수 없는 문제가 발생하고 10ms, 50ms로 짧게 설정하면 느린 환경에 있는 사람들이 작동시킬 수 없는 것이다. 수동으로는 스마트하게 문제를 해결할 수 없다.
2. promise객체
fetch함수는 promise객체를 리턴한다. promise객체는 3가지 상태를 가진다. 바로 Pending(대기), Fulfilled(이행), Rejected(실패) 3가지 상태다. 데이터를 다 받아오기 전까지 promise객체는 Pending상태로 존재하다가 데이터를 가져오는 데 성공하면 Fulfilled상태로 바뀌고, 실패하면 Rejected상태로 바뀐다. promise객체에 대해서는 따로 정리하도록 하고 어쨌든 promise객체가 fulfilled상태가 될 때까지 기다렸다가 getElementById함수를 실행하면 해결되는 문제였다. 따라서 해결한 코드는 다음과 같다.
// 네비바, 푸터 전부 불러온 다음에 이벤트리스너 부착
Promise.all([injectNavbar(), injectFooter()]).then(() => loadComponent());
function loadComponent() {
// 각 컴포넌트에 이벤트리스너 부착
document.getElementById("btnLogout").addEventListener("click", goLogout);
const logos = document.querySelectorAll(".logo");
for (let logo of logos) {
logo.addEventListener("click", goHome);
}
document.getElementById("menuStart").addEventListener("click", goQuiz);
document.getElementById("menuLogin").addEventListener("click", goLogin);
}
Promise.all([함수1, 함수2])는 promise객체를 생성하는 Promise 생성자의 all메소드를 이용한 것으로 함수1, 함수2가 둘다 fulfilled상태가 되면 promise객체가 fulfilled상태로 바뀐다. 따라서 함수1, 함수2는 둘다 promise객체를 리턴하는 함수여야 한다.
promise객체가 fulfilled상태가 되면 then메소드가 동작한다. then메소드는 promise객체가 fulfilled상태가 되면 argument안의 함수를 실행한다. 여기서는 () => loadComponent()함수를 실행한다. nav.html을 불러오는 injectNavBar함수, footer.html을 불러오는 injectFooter함수가 모두 실행되고 fulfilled상태가 된 뒤에 loadComponent함수가 실행되고 안에 있는 각 getElementById가 실행되므로 환경에 관계없이 잘 불러올 수 있다.
'프로그래밍 > 트러블슈팅' 카테고리의 다른 글
[Oracle] 002. Ids_OracleConfigDlg_DatabaseConfigFailedMsg An error occurredwhile configuring Oracle XE database (0) | 2023.09.09 |
---|