패스트캠퍼스X야놀자 프론트엔드 개발 부트캠프_JS과제 리팩토링 후기
과제기간 : 23.08.08~23.08.18
깃헙코드 : https://github.com/furaha707/ListApp_JS
이번에는 느낀 점을 먼저 적어보자

처음에는 과제 안내를 받았을 때 정말 막막했다
내가 혼자 데이터 파일을 만들어서 불러오고 그것을 출력한 정도로만 만들어봤고
CRUD 앱을 제대로 만들어본 적이 없었다
그리고 실시간 DB를 연동한 경험도 많이 없어서 난이도가 매우 높게 느껴졌었다
처음엔 user flow를 머릿속으로 먼저 그려보고
디렉토리 구성을 하고
그리고 주로 버튼에 이벤트를 걸어서 동작하다 보니까
버튼과 기능에 따른 함수 이 두가지를 작성하다 보니
작업하면서 점차 확장시키고 싶은 기능들이 생각이 났다
그때그때 오류 나면 검색하면서 firebase 사용법도 익히고 예외처리도 마음껏 해보고,,
그렇게 나름 하나의 완성본을 만들어보니 아무런 소스 제공 없이 무에서 시작했지만,
내가 머릿속에서 원하는 대로 그 결과를 결국 만들어냈다는 이 과정이 많이 신기했다
그리고 배운 점들은 코드를 직접 적어 내리다 보면 막상 개념이 헷갈려서 만들어내는 오류가 많은 것 같다
데이터타입이라던지, 메서드의 정확한 쓰임새라던지,,
이런 부분은 직접 부딪혀봐야 정확하게 알 수 있는 부분이라고 생각이 들어서
어떤 개념을 배울 때는 저번에 멘토님이 추천해 주신 replit 사이트에서
코드를 쳐보고 그 결과를 내서 눈으로 확인하는 연습을 하면 좋을 것 같다
그리고 제일 귀감이 되었던 것은 수강생들의 공유된 코드들을 보면서 정말 많은 것을 느꼈는데
그중 제일은 바람직한 코드 습관이다
사실 지금까지는 웹 페이지를 구현할 때 성능이나 효율성보다 스타일에 신경을 많이 쓸 수밖에 없는 환경이었는데
이제는 코드가 깔끔한지 그리고 누군가에게 공유가 되어도 이해할 수 있는 구조인지
이 부분에 훨씬 더 비중을 많이 둬야겠다고 생각했다
앞으로 배울 타입스크립트, 리액트도 학습하다 보면 하나의 서비스를 만들어낼 수 있는 때가 있을 텐데
그때가 꼭 오면 좋겠다!!!
이번 과제에서 개선해야 할 부분과 코드리뷰를 토대로 수정을 해보았다
이전 포스팅 https://furaha.tistory.com/28
1. 배열에 담긴 아이템 삭제를 firestore 통신해서 한꺼번에 처리하기
// 변경 전
docIdsToDelete.forEach((docId) => {
db.collection('person')
.doc(docId)
.delete()
.then(() => {
console.log(`Document with ID ${docId} successfully deleted!`);
})
.catch((error) => {
console.error(`Error deleting document with ID ${docId}:`, error);
});
});
// 변경 후
Promise.all(
docIdsToDelete.map((docId) =>
db.collection('person')
.doc(docId)
.delete()
)
)
.then(() => {
console.log('All documents successfully deleted!');
render(); // 모든 삭제 작업이 완료되었을 때에만 렌더링 수행
})
.catch((error) => {
console.error('Error deleting documents: ', error);
});
삭제 기능을 구현하는 중이었다.
docIdsToDelete라는 배열에는 내가 삭제하려고 선택한 아이템들이 들어있다.
이 아이템들을 실제 firestore DB 에도 삭제가 되어야 하는데
배열들을 하나씩 돌면서 firestore DB와 통신하게 되면 통신이 여러 번 발생함으로 네트워크 비용이 많이 든다
forEach로 배열을 돌렸으니까
하나 삭제되면 -> 콘솔 찍고
하나 삭제되면 -> 콘솔 찍고
하나 삭제되면 -> 콘솔 찍고
계속 이 반복이라 배열의 아이템들을 한꺼번에 삭제 요청을 하도록 바꾸어주어야 했다.
그래서 promise.all()을 사용하여 모든 데이터의 통신이 완료되면 그 다음을 동작하게 했다
promise.all() 에는 배열을 매개변수로 받아야 하기 때문에 단순 순회만 하는 forEach에서
배열을 반환하는 함수 map으로 바꿔주었다

2. Object literals 사용
// 변경 전
let btnClass = '';
// 상담종류에 따라 버튼 색상 달라짐
if (doc.data().sort === '상담중') {
btnClass = 'btn-primary';
} else if (doc.data().sort === '치료중') {
btnClass = 'btn-danger';
} else if (doc.data().sort === '종결') {
btnClass = 'btn-warning';
}
// 변경 후
const counselingTypes = {
상담중: { className: 'btn-primary' },
치료중: { className: 'btn-danger' },
종결: { className: 'btn-warning' }
};
const counselingType = counselingTypes[doc.data().sort];
const btnClass = counselingType ? counselingType.className : '';
종류 3가지에 따라 버튼 색상이 바뀌도록 하고 싶었다
그래서 각각 종류에 따라서 버튼 클래스 명을 btnClass에 담았는데
사실 타입이라는 하나의 객체리터럴을 만들어서
그 객체에서 타입을 뽑아서 쓰는 것이 더 효율적인 것 같다
뽑아서 쓸 때는 counselingTypes[들어갈 값]
이렇게 이름을 직접 넣어서 그에 해당하는 값을 가져올 수 있기 때문에
바꾸고 나니 훨씬 가독성이 있는 코드로 바뀌었다.
3. 예외처리
아무것도 선택하지 않고 삭제했을 때 막기
해당 코드 추가!!
이번 과제에 로그인 기능을 넣었다 보니가 예외처리할 것이 꽤 많았다;
if (deleteList.length == 0) {
alert('삭제할 프로필이 없습니다')
return;
}
4. then 지옥을 async await로 바꾸어준 것
// 변경 전
imageInput.addEventListener('change', function (event) {
const selectedImage = event.target.files[0];
if (selectedImage) {
const reader = new FileReader();
reader.onload = function (e) {
imagePreview.src = e.target.result;
const storageRef = firebase.storage().ref();
const imageRef = storageRef.child('image/' + selectedImage.name);
imageRef
.put(selectedImage)
.then(() => {
console.log('Image uploaded successfully');
// 업로드한 이미지의 URL을 받아옴
imageRef
.getDownloadURL()
.then((imageUrl) => {
// imageUrl을 사용하여 Firebase Firestore에 업데이트
const newData = {
// ... 기타 필드 데이터 ...
image: imageUrl, // 업로드한 이미지의 URL
};
db.collection('person')
.doc(personId.get('id'))
.update(newData)
.then(() => {
console.log('Firestore updated with image URL');
})
.catch((error) => {
console.error('Error updating Firestore:', error);
});
})
.catch((error) => {
console.error('Error getting image URL:', error);
});
})
.catch((error) => {
console.error('Error uploading image:', error);
});
};
reader.readAsDataURL(selectedImage);
} else {
imagePreview.src = '';
}
});
} else {
alert('작성자만 수정 가능합니다');
}
hideLoadingImage();
})
.catch((error) => {
console.error('Error getting document:', error);
hideLoadingImage();
});
});
변경 후
imageInput.addEventListener('change', async function (event) {
const selectedImage = event.target.files[0];
if (!selectedImage) {
imagePreview.src = '';
return;
}
// firebase에 이미지 등록하고 url 가져오는 함수 후에
// data 업데이트 하는 함수로 인자를 전달
try {
const imageUrl = await uploadImage(selectedImage);
imagePreview.src = imageUrl;
await updateFirestoreImage(imageUrl);
} catch (error) {
console.error('Error:', error);
}
// firebase에 등록
async function uploadImage(imageFile) {
const storageRef = firebase.storage().ref();
const imageRef = storageRef.child('image/' + imageFile.name);
try {
await imageRef.put(imageFile);
const imageUrl = await imageRef.getDownloadURL();
return imageUrl;
} catch (error) {
throw new Error('Error uploading image: ' + error.message);
}
}
// 현재 이미지 url을 포함하도록 data 업데이트
async function updateFirestoreImage(imageUrl) {
const newData = {
image: imageUrl,
// ... 기타 필드 데이터 ...
};
try {
await db.collection('person').doc(personId.get('id')).update(newData);
} catch (error) {
throw new Error('Error updating Firestore: ' + error.message);
}
}
});
프로필 수정할 때
이미지를 업로드하면 업로드 성공 후에 이미지 url을 받아오고 그 url 을 Firestore DB에 데이터 업데이트를 해줘야 했었다
이 부분을 then을 사용해서 업로드 🔜 then 🔜 주소 받기 🔜 then 🔜 데이터 업데이트 이렇게 작성하다 보니 뎁스가 너무 깊어졌다
이것이 then 지옥인가 맞나 싶었는데 수정하려고 보니 async await를 사용하면 훨씬 가독성이 높아지는 것을 느꼈다
먼저, 기능별로 함수를 분리하고
데이터를 받아와야 하는 것은 async await 를 사용해서
넘겨줘야 할 데이터들은 함수의 매개변수로 넘겨주는,,
그래서 이 부분을 코드 수정하면서
웬만하면 async await 를 사용해야겠다고 느꼈다

그리고 then과 async await의 사용 용도가 아예 다르다
async await와 then을 비교하자면 then 을 많이 쓰지는 않는다고 한다.
이러한 then 지옥 때문에! 그러면 언제 사용하는 걸까?
요청한 데이터 값이 있어야만 화면을 그릴 수 있다면 비동기 요청에 대해서 async await로 기다려주는 것
응답값을 기다리지 않고 요청이 간 상태로 그 다음 코드가 진행이 될 때에는 then을 사용하는 것
(응답이 왔을 때 돌아온 순간 then 안에 있는 것이 실행이 됨)
그래서 then 필요한 경우가 많지는 않지만 중간에 실행시키고 싶은 구문이 있다면 사용한다고 한다
실제로 replit에서 then을 동작시켜 보니까 데이터를 받아오지 않았는데도 다음 구문을 실행하는 것을 확인했다
5. 변수 선언 범위!!!
블록 안에서 쓰이는 변수는 블록 안에서 변수를 선언해줘야 하는데
몇 개는 전역변수로 선언한 것들이 있었다
지역변수를 전역변수로 선언하면 안 좋은 점이 여러 가지이지만
제일 큰 것은 메모리 누수가 발생인 것 같다
지역변수는 정의된 블록 내에서 실행이 끝나면 자동으로 메모리에서 해제가 되는데
전역변수는 프로그램이 종료될 때까지 메모리에 남아있기 때문에 프로그램 성능을 낮출 수가 있다
변수 선언의 위치는 항상 주의해야겠다

추가로 넣은 기능 2가지
1. 검색어 기능 개선 (해당 단어를 포함한다면)
// 변경 전
db.collection('person')
.where('name', '>=', inputValue)
.where('name', '<=', inputValue + '\uf8ff')
.get()
.then((result) => { ... }
// 변경 후
db.collection('person')
.get()
.then((result) => {
const filteredResults =
result.docs.filter(doc => doc.data().name.includes(inputValue));
filteredResults.forEach((doc) => { ... }
where 메서드를 사용했을 때는
검색어보다 많거나 혹은 검색어 뒤에 문자가 붙었을 때 필터링을 했다 보니까
한계가 검색어의 중간 부분이나 뒷부분을 입력했을 때는 필터가 되지 않았다
ex) '김까칠'을 검색하기 위해 '까칠'이라고 입력하면 아무것도 필터 되지 않음
그래서 includes()를 사용해서 게시글의 제목이 입력값을 포함하고 있는 게시글들로 필터링을 하니까
어느 키워드로 입력하던지 검색이 되도록 기능을 조금 더 개선시켰다
2. 마스터 계정에는 모든 권한 허용하기
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /person/{docid} {
allow read: if true;
allow create: if request.auth != null;
allow write: if request.auth != null && (
// 추가한 부분 특정 id에 모든 권한을 부여하기
request.auth.uid == 'jusBruEPBGcrT4YlxuBR3wuquYo2'
|| request.auth.uid == resource.data.uid
);
}
}
}
마스터 계정 하나에는 모든 글에 대한 수정/삭제 권한을 주고 싶었다
그래서 user라는 컬렉션을 하나 더 추가해서 계정의 계급을 부여해 줄까 하다가
시간이 넉넉하지는 않았기 때문에 단순하게 firebase 규칙에 한 줄을 추가했다
규칙을 수정한 것은 백엔드 제어인데
백엔드에서 허용해 줘도 프론트에서 제어가 되어서 데이터가 넘어가지를 않기 때문에
내가 애먹은 부분은 프론트에서도 제어한 부분들을 다 수정해줘야 했었다
최종 수정하고 firebase deploy로 바로 호스팅까지 마무리!!