[클라우딩 어플리케이션 엔지니어링 TIL] - DAY 15 (가계부 기능 추가)
이번주는 JS가 서버와 통신을 위해 사용하는 API 통신 과정을 비동기적으로 동작하는 async - await 패턴을 이용해 가계부 페이지를 구현했다. 강의를 따라가며 가계부 기능을 구현하면서 개념적으로만 알고 있었던 API 통신에 대해 배울 수 있어서 좋았다.
개인적으로 가계부 기능을 일부 수정, 추가하고 삭제 API도 사용해 보았다.
1. 날짜 입력 기능
1-1) 날짜 입력 필드 추가
각 날짜의 default value는 현재 시간을 기준으로 년, 월, 일 설정
const initForm = () => {
...
// 날짜 value 초기화
$yearInput.value = today.getFullYear();
$monthInput.value = today.getMonth() + 1;
$dayInput.value = today.getDate();
}
1-2) 입력받은 날짜 저장
현재 시간만 저장하던 createAt을 입력한 날짜를 저장하도록 변경
const createAt = new Date($yearInput.value, $monthInput.value - 1, $dayInput.value);
1-3) 입력 편의 고려
날짜 입력 칸은 자릿수에 따라 일정 길이 이상 입력하면 다음 입력칸으로 빠르게 유도
focus() 메서드를 통해 구현
$yearInput.addEventListener('keyup', () => {
if ($yearInput.value.length >= 4) {
$monthInput.value = "";
$monthInput.focus()
}
...
});
+ 닫기(back) 추가
닫기 버튼은 $addItemDetail을 숨기고, 다시 $addItemButton을 보여주면서 입력 필드들을 초기화
const addEventListener = () => {
...
// back-button
$itemBackButton.addEventListener("click", function () {
toHidden($addItemDetail);
toShow($addItemButton);
initForm();
});
};
2. 카테고리 변경
카테고리 값을 지출과 수입을 구분하는 라디오 버튼으로 변경
지출(value="minus")은 기존과 동일한 기능
수입(value="plus")은 지출의 반대 작업, 현재 자산보다 큰 값 입력 가능
지출인지 수입인지 판단하여 현재 자산에서 입력한 금액을 계산 후 fundsAtTheTime에 저장
handleAddCurrentAsset()로 현재 자산 갱신
const handleAddConsumptionDetail = async () => {
try {
...
// 정규표현식으로 "₩100,000"에서 숫자만 추출
const currentAssetPrice = Number($currentAssetValue.textContent.replace(/ , | \\ /g, ""));
// 입력한 금액 부호 설정
const inputItemPrice = isPlus? Number($itemPrice.value) : -Number($itemPrice.value);
const fundsAtTheTime = currentAssetPrice + inputItemPrice;
...
// currentAsset update
await handleAddCurrentAsset(fundsAtTheTime);
...
} catch (err) {
console.error(err);
}
};
사용한 정규표현식 패턴: (/ 문자열 1 | 문자열 2 | ... /g, 치환 값) => / / 안에 있는 문자열을 찾고 설정한 치환 값으로 변경.
replace는 가장 처음 찾은 문자열만 치환하므로 g(Global) 플래그로 모든 문자열을 찾도록 함.
3. 소비 내역 정렬
모든 소비 내역 목록은 날짜를 기준으로 최근 내역이 가장 위로 오도록 정렬.
같은 날짜일 경우 나중에 작성한 내역이 위로 올라옴.
const handleGetConsumptionDetails()에서 map 연산을 하기 전에 list를 한 번 sort 하고 진행
const list = await getConsumptionDetails();
list.sort((a, b) => {
if (Date.parse(b.createAt) === Date.parse(a.createAt)) return b.id - a.id;
else return (Date.parse(b.createAt) - Date.parse(a.createAt));
})
.map(({ createAt, category, description, price, ...
4. 삭제 기능 구현
4-1) delete api
소비 내역은 고유한 id를 가지고 있으므로 id를 통해 삭제할 데이터에 접근
import axios from "axios";
export const deleteConsumptionDetail = async (id) => {
const {data} = await axios.delete("http://localhost:3002/consumption-details/" + id);
return data;
}
4-2) 삭제 버튼에 ID 추가
const handleGetConsumptionDetails()에서 id를 map 연산의 파라미터로 추가한 후
어떤 삭제 버튼이 눌렸는지 파악하기 쉽게 삭제 버튼을 생성할 때 속성에 id를 추가, 각 버튼에 이벤트 리스너 등록
const list = await getConsumptionDetails();
list.sort((a, b) => {...})
.map(({ createAt, category, ..., id }) => {
...
// deleteButton에 id 추가
const $deleteSection = createElement("div", "delete-section");
const $deleteButton = createElement("button", "delete-button", id);
...
$deleteButton.addEventListener("click", clickDeleteButton);
});
};
map() 안에서 구성했던 소비 내역의 계층 구조를 살펴보면 다음과 같은데 필요한 부분만 표로 나타냈다.
*$li* | ||
$itemSection | ||
$itemSectionColumn | ||
$deleteSection | $consumptionDetailsDetail | |
*$deleteButton* | $consumptionDetailsDetailTitle | *$consumptionDetailsDetailSubtitle* |
삭제 기능에 필요하다고 생각하는 건
- DB에서 삭제하기 위해서 $deleteButton에 추가해 두었던 id
- 클릭된 삭제 버튼에 해당하는 $li
- 소비 내역의 금액을 가지고 있는 $consumptionDetailsDetailSubtitle
4-3) clickDeleteButton 함수 구현
$li에 요소들이 appendChild() 형태로 연결되어 있기 때문에
클릭된 삭제 버튼을 통해 역으로 타고 올라가서 해당하는 $li까지 알아낼 수 있다.
const clickDeleteButton = async (event) => {
try {
const $deleteButton = event.target;
const $itemSectionColumn = $deleteButton.parentElement.parentElement;
const $li = $itemSectionColumn.parentElement.parentElement;
// 현재 자산 ex) "₩100,000"
const $currentAssetValue = document.querySelector(".current-asset-value");
const currnetValue = Number($currentAssetValue.textContent.replace(/,|\\/g, ""));
// 삭제할 목록의 금액, 부호는 그대로 사용 ex) "±10,000원"
const $consumptionDetailsDetailSubtitle = $itemSectionColumn.querySelector(".consumption-details-detail-subtitle");
const deleteValue = Number($consumptionDetailsDetailSubtitle.textContent.replace(/,|원/g, ""));
// delete from $consumptionDetailsList
$li.remove();
// delete from DB
await deleteConsumptionDetail($deleteButton.id);
// 현재 자산에 삭제한 소비 내역 금액 되돌리기
const value = currnetValue - deleteValue;
// 현재 자산, 소비 내역 목록 갱신
await handleAddCurrentAsset(value);
handleGetConsumptionDetails();
} catch (err) {
console.error("failed delete.");
}
}
5. 개선해야 할 부분
5-1) 가계부의 목적에 맞는 기능
나름 기능을 추가한다고 했는데 재정 상황이 생각보다 한눈에 들어오지 않는다.
카테고리 값에 따라 지출 또는 수입 부분만 모아서 보기, 카테고리 별 합계, 월별 합계 같이 내역을 요약하여 보여줄 수 있으면 좋을 것 같다.
5-2) 애니메이션
버튼을 클릭할 때나, 호버링, 스크롤 등에 아무런 애니메이션이 없어서 좀 삭막하다거나 딱딱한 분위기가 드는 것 같다.
간단한 애니메이션을 추가해서 유동적인 움직임을 표현해 부드러운 느낌을 연출해보고 싶다.
5-3) 코드 깔끔하게 작성하기
함수들을 구현할 때 일일이 값을 출력해 보고 구조를 찾아가면서 조금 즉흥적으로 구현한 거라 좀 코드가 어지럽다.
가독성을 위해서라도 더 좋은 방법을 고민하고 깔끔하게 코드를 작성하려고 노력해야겠다.
후기
기능 구현 강의를 보다 보니 흥미가 생겨 개인적으로 추가 작업을 해봤는데, 내가 얼마나 부족한지 다시 한번 느꼈다.
삭제 기능을 구현할 때 구조를 정확하게 파악하는 데 좀 걸리기도 했고, 다른 디테일한 기능을 구현하려고 애쓰다가 시간을 많이 썼다. 모아 놓고 보니 별거 아닌 기능들 같은데 저거 구현한다고 중간에 말도 안 되는 걸로 삽질을 엄청 해버렸다.
잘 모르면서 왜 일을 크게 벌였을까,,, 라는 생각이 들기도 했지만 그래도 추가적인 기능을 구현해 보면서 html, css, js의 전반적인 이해와 API 통신 과정을 파악하는데 도움이 많이 됐다.
관련글