바닐라 자바스크립트로 작은 웹앱을 만들어 보면서 innerHTML을 쓰는 경우가 많았다. 그런데 항상 다른 상황에서 다른 형태로 만들다 보니 그 때마다 다른 문제에 부딪쳤다. 지금까지 DOM 추가를 위해서 어떤 코드를 짰고, 각각 어떤 문제가 있었는지, 어떻게 해결했는지 공유해보고자 한다.
//add localstorage comments on the html one by one
const addCommentDOM = (comment) => {
const commentEl = document.createElement('li')
if (comment.text !== '') {
viewCommentsBtn.innerText = `View ${comments.length === 1 ? '' : 'all'} ${comments.length} comment${comments.length === 1 ? '' : 's'}`
commentEl.classList.add('comment__content-list')
commentEl.innerHTML = ''
commentEl.innerHTML = `
<div class="comment__content-box">
<span class="comment__content"><span class="comment__user user-link">workoutbutlazy</span>${comment.text}</span>
<i class="comment__content-delete fas fa-times" id="delete-btn"token interpolation">${comment.id})"></i>
</div>
<i class="far fa-heart comment__heart" id="comment-heart"></i>
`
commentList.appendChild(commentEl)
}
}
이 기능을 구현하는 데는 큰 어려움은 없었지만, HTML에서 임시로 하드코딩을 먼저 한 후, css를 입힌 다음에 마지막으로 innerHTML을 구현하다 보니 document.createElement('li')
부분에서 리스트에 클래스가 없는 상태로 DOM이 생성되었다. 간단히 classList.add()
메서드를 넣어서 해결했다.
//making search result DOM
const addSearchResultDOM = (inputText, userInfo) => {
const searchResultEl = document.createElement('li') //li
if (inputText !== '') {
searchResultEl.classList.add('search-list__result')
searchResultEl.innerHTML = `
<div class="search-list__user-image-container">
<img src="${userInfo.userProfile}" alt="User profile" class="search-list__user-image">
</div>
<div class="search-list__user-info-container">
<div class="search-list__user-id user-link">${userInfo.userId}</div>
<div class="search-list__user-name">${userInfo.userName}</div>
</div>
`
searchList.classList.add('open')
searchList.appendChild(searchResultEl)
return searchList
} else {
searchList.classList.remove('open')
}
}
//search users
const renderSearchResult = () => {
const searchText = searchInput.value
let filteredUsers = userInfos.filter((userInfo) => {
if (userInfo.userId.toLowerCase().includes(searchText.toLowerCase())
|| userInfo.userName.toLowerCase().includes(searchText.toLowerCase())) {
return userInfo
}
})
searchList.innerHTML = ''
filteredUsers.forEach((filteredUser) => addSearchResultDOM(searchText, filteredUser))
}
정말 너무 해결이 안 돼서 며칠 동안 붙잡고 있었던 기능이다. 왜인지는 아직도 모르겠는데, 검색을 하면 userInfos
라는 mock data 의 마지막 요소만 검색 결과로 나오는 문제가 계속 생겼었다. 또 다른 문제는 검색 결과가 타이핑을 할 때마다 새로고침이 되어야 하는데 계속 누적이 되는 것이었다. 예를 들어 'b'를 타이핑하면 1, 2번 유저의 정보가 나오고, 'ba'를 타이핑하면 1번 유저만 나와야 하는데, 1, 2, 1 번 유저의 정보가 연달아서 나오는 것이었다.
그래서 장현님의 조언에 따라, 먼저 어떤 검색어를 입력해도 전체 유저가 검색되도록 만들어준 후, 검색어에 따라 필터를 하는 기능을 나중에 입혀주도록 했다. 또 타이핑을 할 때마다 필터링된 DOM을 추가해주기 직전에 searchList.innerHTML = ''
를 먼저 써 줌으로써 검색 결과가 누적되지 않고 새로고침 되도록 만들었다. 그리고 아무런 검색어가 없는 경우 검색결과 컨테이너까지 말끔하게 사라지도록 searchList.classList.remove('open')
를 사용해서 정리했다.
//rendering stories / getting data from user-infos.js
const renderStories = () => {
const storyContainer = document.getElementById('stories-container')
const randomIndexArray = selectIndex(24, 15)
randomIndexArray.map((index) => {
const story = document.createElement('div')
story.classList.add('story')
story.innerHTML = `
<div class="story__view-button"><img src="${userInfos[index].userProfile}" alt="User image" class="story__user-image"></div>
<div class="story__user-id">${userInfos[index].userId}</div>
`
storyContainer.appendChild(story)
})
}
이전 글에서 만들어 둔 중복 제외한 랜덤숫자 여러 개 뽑기 함수를 활용해서, 미리 만들어 둔 mock data에서 랜덤한 유저들의 인스타 스토리를 출력하는 코드를 짰다. 큰 어려움은 없었지만 array.map()
메서드를 활용하는 방법을 제대로 알게 되었다. 처음에는 제대로 출력되지 않았다가, array.map()
은 요소를 하나씩 돌아가면서 괄호 안의 함수를 실행한다는 점을 이해한 후, 함수 안에서 appendChild()
를 하도록 수정했더니 잘 출력됐다.
//Add transactions to DOM list
const addTransactionDOM = (transaction) => {
//Get sign
//single transaction is gonna be passed into this
const sign = transaction.amount < 0 ? '-' : '+'
const item = document.createElement('li')
// transactions = sortTransactions(transactions)
//Add class based on value
item.classList.add(transaction.amount < 0 ? 'minus' : 'plus')
item.innerHTML = ''
item.innerHTML = `
<button id="delete-btn" class="delete-btn"token interpolation">${transaction.id})">삭제</button>
<div class="transaction-list">
<div class="transaction-list-info">
<div class="transaction-date"> ${transaction.date.slice(0,4)}. ${transaction.date.slice(4,6)}. ${transaction.date.slice(6,8)}</div>
<div class="transaction-text"> ${transaction.text} </div>
</div>
<span class="transaction-amount">${sign}${numberWithCommas(
Math.abs(transaction.amount))}</span>
</div>
`
list.appendChild(item)
}
//rendering transactions
const renderTransactions = () => {
if (sort_plus.classList.contains('sort')) {
list.innerHTML = ''
const incomeTransactions = transactions.filter((transaction) => transaction.amount > 0)
incomeTransactions.forEach((transaction) => {
addTransactionDOM(transaction)
})
} else if (sort_minus.classList.contains('sort')) {
list.innerHTML = ''
const expenseTransactions = transactions.filter((transaction) => transaction.amount < 0)
expenseTransactions.forEach((transaction) => {
addTransactionDOM(transaction)
})
}
}
다양한 형태의 input 값을 가지고 DOM에 추가하는 기능이다. 폼에 입력된 날짜 데이터를 받아서 slice()
메서드로 날짜를 보기 좋게 표현하는 것을 처음 시도한 코드였다. 그런데 단순히 소비내역을 리스트화하는 것으로 그치는 게 아니라, 나중에 기능을 추가하면서 수입과 지출을 따로 분류해서 볼 수 있도록 코드를 수정해야 했다. 그래서 renderTransactions
라는 함수를 새로이 만들어서, 특정 클래스가 포함된 DOM 엘리먼트끼리 따로 필터를 해서 array.forEach()
메서드로 추가해주도록 수정했다.
수입 또는 지출 radio 버튼을 누른 상태에서 폼을 제출하면 수입이나 지출 내역만 보는 기능이 활성화된 상태에서도 전체 거래 내역이 목록으로 뜨는 문제가 있었다. 그러니까, 수입만 보여주는 메뉴를 누른 상태였는데도 불구하고, 지출 내역을 새로 업로드하는 경우, 새로 업로드한 지출 내역이 수입 내역 리스트에 추가되는 문제점이 있었다. 그래서 renderTransaction
함수를 통해, 각 메뉴가 선택됐을 경우에 알맞는 filter()
메서트 결과값을 차례로 돔에 넣어주도록 하고, 그것이 폼 제출 때마다 실행되도록 했다.