본문 바로가기
캠프 개발일지

4주차 개발일지

by JHBang 2023. 11. 23.

4추자 강의를 모두 수강했다. 4주차에 학습한 내용은 firebase 데이터베이스를 이용해 기존에 만들던 추억앨범에 카드를 추가하고 불러오는 내용이였다.

firebase는 생각보다 사용하기 편리했다. 진짜 실무에서 사용하는 데이터베이스라기엔 내 추억앨범의 데이터베이스는 빈약하기 그지없지만 어떤 식으로 데이터베이스가 구동되는지 감을 잡을 수 있었다.

강의에서는 데이터를 추가하고 불러오는 데 까지만 구현했지만 데이터베이스를 활용한다고 하면 역시 데이터 수정과 삭제가 가능해야 완벽한 활용이라고 할 수 있겠다. 그래서 이번에도 혼자서 구현해봤다.

캠프에서 제공해준 firebase 코드 스니펫에는 카드를 수정, 삭제하는데 필요한 deleteDoc이나 updateDoc이 임포트 되어있지 않으므로 직접 추가해줘야 한다.

import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
import { getFirestore } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
import { collection, addDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
import { getDocs } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
import { deleteDoc, doc, updateDoc, getDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";

추가한 건 deleteDoc, doc, updateDoc, getDoc이다.

일단 카드의 삭제, 수정을 가능하게 하려면 내가 원하는 카드의 문서에 접근이 가능해야 한다. 캠프에서 제공한 코드를 먼저 살펴보자.

let docs = await getDocs(collection(db, "albums"));
docs.forEach((doc) => {
    selectedDocumentId = doc.id;
    console.log(selectedDocumentId)
    let row = doc.data();


    let image = row['image'];
    let title = row['title'];
    let content = row['content'];
    let date = row['date'];
    let temp_html = `
                <div class="col">
                    <div class="card h-100">
                        <img class="cardimg"
                            src="${image}"
                            class="card-img-top" alt="...">
                        <div class="card-body">
                            <h5 class="card-title">${title}</h5>
                            <p class="card-text">${content}</p>
                        </div>
                        <div class="card-footer">
                            <small class="text-body-secondary">${date}</small>
                        </div>
                        
                        <div class="doc-id" id="docID" style="display: none;">${selectedDocumentId}</div>
                    </div>
                    
                </div>
                `;
    $('#card').append(temp_html)
});

강의엔 자세한 정보는 안 나왔지만 기능을 구현하기 위해선 코드의 이해가 필요했다. 위 코드의 맨 위를 살펴보면 docs에 getDocs의 반환값을 집어넣는 형태이다. getDocs는 내 firebase에 저장된 '컬렉션'을 불러오는 비동기 함수이다. 인수로는 collection이 들어갔는데, collection은 내 db의 "album"이라는 이름의 컬렉션의 참조를 얻는 함수이다. 이게 다 무슨 소리인가?

컬렉션은 firebase에서 문서들의 모음을 나타내는 단위이다. "album"컬렉션에 추억앨범 사이트에 있는 카드의 정보가 담긴 문서가 모여있는 것이다.

컬렉션의 이름을 통해 참조를 얻을 수 있는 메서드가 collection이고, 이 collection의 정보를 getDocs 메서드를 통해 얻는 형식이다.

getDocs뿐만 아니라 데이터베이스에 접근하는 함수들은 비동기 함수인데, 비동기 함수는 해당 함수가 진행되는 동안 함수의 완료를 기다리지 않고 다음 코드로

넘어가는 기능을 가진 함수이다. 그렇게 때문에 await라는 구문을 추가해서 함수가 끝나고 코드가 진행되도록 해 주어야 한다.

 

문서의 삭제와 수정을 구현하려면 각 문서 자체에 접근할 필요가 있다. 컬렉션의 참조를 얻는 메서드가 collection이였다면 문서의 참조를 얻는 메서드는doc이다. doc는 getDoc과 함께 사용한다. (getDocs에서 s가 빠졌다.) getDoc은

getDoc(doc(db, "album", docID)

이런식으로 사용하는데, docID는 각 문서의 고유 id를 뜻한다.( deleteDoc, updateDoc 모두 동일하게 사용)

이 id는 위에서 컬렉션 정보가 저장된 docs의 forEach안에

 docs.forEach((doc) => {
      selectedDocumentId = doc.id;

이런 식으로 .id만 추가하면 얻을 수 있다. selectedDocumentId는 문서의 id를 저장하는 전역변수이다.

또한 이 id를 사용하기 위해 temp_html에 문서의 id를 저장하는 보이지 않는 div를 추가했다. 이는 나중에 id를 사용할 때 해당 div의 값을 불러와 사용할 수 있다.

 

이제 삭제와 수정 기능을 구현할 기본적인 지식이 생겼다.

이전 주차에 카드를 클릭하면 팝업창으로 카드를 확대해서 보여주는 기능을 넣었었는데, 한 가지 문제가 생겼다.

테스트를 해 봤는데 카드를 클릭해도 modal 창이 뜨질 않았다.

그 이유는 firebase를 사용하기 위해 <script>에 type="module"을 추가했는데, 이게 추가되면 onclick메서드가 작동을 안 한다고 한다. modal창을 띄우는 함수는 onclick으로 동작했기 때문에 수정이 필요했다.

아래는 강의에서 알려준 카드 추가 함수이다.

$("#postingbtn").click(async function () {
            const postingconfirm = confirm('추억을 기록할까요?');
            if (postingconfirm) {
                let image = $('#image').val();
                let title = $('#title').val();
                let content = $('#content').val();
                let date = $('#date').val();
                let doc = {
                    'image': image,
                    'title': title,
                    'content': content,
                    'date': date
                };
                await addDoc(collection(db, "albums"), doc);
                alert('기록되었습니다.');
                window.location.reload();
            }
        })

여기선 onclick대신

$("#postingbtn").click(async function { 
......

을 사용했는데, 이는 'id가 postingbtn인 요소에 click이란 이벤트가 발생할 시 아래 함수를 시행해라' 라는 의미이다.

처음엔 위 함수처럼 modal을 띄우는 함수를 저렇게 썼었다. 하지만 새로 추가된 카드에는 클릭 이벤트가 작동하지 않았는데, 이는 추가된 카드는 동적으로 생성된 요소이기 때문에 이벤트 핸들러가 할당되지 않았기 때문이다.

동적으로 생성된 요소는 페이지가 모두 불러 와지고 난 후에 코드나 이벤트에 의해 새로 추가된 요소를 뜻한다.

이벤트 핸들러는 페이지가 불러 와질 때 요소들에게 정해진 이벤트를 할당하는데, 이 때문에 동적인 요소는 이벤트를 할당 받지 못한 것이다.

이때 필요한 것이 이벤트 위임이다. 아래는 이벤트 위임을 적용한 modal 창 띄우기 코드이다.

$("#card").on("click", ".col", function () {
    $('#modalcontent').empty();

    let image = $(this).find('.cardimg').attr('src');
    let title = $(this).find('.card-title').text();
    let content = $(this).find('.card-text').text();
    let date = $(this).find('.text-body-secondary').text();
    selectedDocumentId = $(this).find('.doc-id').text();
    console.log(docID);
    let temp_html_modal = `
    <div class="col2">
        <div class="card h-100">
            <img src=${image}
            <div class="card-body">
                <h5 class="card-title">${title}</h5>
                <p class="card-text">${content}</p>
            </div>
            <div class="card-footer">
                <small class="text-body-secondary">${date}</small>
            </div>
        </div>
        <div class="mybtn">
            <button id="revisebtn" type="button" class="btn btn-outline-info">
                <span class="doc-id" style="display: none;">${selectedDocumentId}</span>
                수정</button>
            <button id="deletebtn" type="button" class="btn btn-outline-danger">
                <span class="doc-id" style="display: none;">${selectedDocumentId}</span>
                삭제</button>
        </div>
    </div>`;
    $('#modalcontent').append(temp_html_modal);
    $('#myModal').show();
})

 

위 코드 중 $("#card").on("click", ".col", function ()을 정리해 보면 다음과 같다.

#card -> 이벤트를 연결할 요소

.on -> 이벤트 등록

click -> 등록할 이벤트 종류. 여기선 클릭

.col -> 이벤트를 위임할 자식요소

function() -> 이벤트 발생시 실행할 함수

 

let image = $(this).find('.cardimg').attr('src');에서 this는 위임을 받는 자식 요소를 가리킨다.

 

temp_html_modal부분에 mybtn이라는 클래스의 div가 추가됐는데, 이 div안에 수정과 삭제 버튼을 추가했다. 또한 <span>으로 해당 카드의 문서id를 보이지 않게 추가했는데, 이는 수정되거나 삭제될 문서id를 얻기 위함이다.

 

아래는 문서 삭제 버튼을 눌렀을 경우를 구현한 코드이다.

$("#myModal").on("click", "#deletebtn", async function () {
    // 삭제할 문서의 ID 가져오기
    selectedDocumentId = $(this).find('.doc-id').text();

    const deleteconfirm = confirm('정말로 삭제하시겠습니까?');
    if (deleteconfirm) {
        await deleteDoc(doc(db, "albums", selectedDocumentId));
        alert('삭제되었습니다.');
        window.location.reload();
    }
    else {
        alert('삭제가 취소되었습니다.');
    }
});

modal창의 삭제 버튼 또한 동적 요소기 때문에 이벤트 위임을 사용해 이벤트 핸들러를 할당해주었다.

그리고 deletebt 클래스에서 문서 id 요소를 찾아 deleteDoc를 사용해 문서를 삭제했다.

 

문서 수정은 조금 더 복잡한 구조를 가지는데, 일단 수정할 정보를 입력해야 하기 때문에 modal클래스의 div를 추가했다.

<div id="reviseModal" class="modal">
    <div class="modal-revise">
        <p id="revisecontent"></p>
        <div class="mybtn">
            <button id="editCompleteBtn" type="button" class="btn btn-primary">수정하기</button>
            <button id="reviseclose" type="button" class="btn btn-outline-info">닫기</button>
        </div>
    </div>
</div>

이전modal창에서 버튼 div를 추가하고 class의 이름을 수정했다.

 

아래는 수정하기 버튼을 눌렀을 때 수정할 정보를 입력하는 modal창이 팝업 되게 하는 코드이다.

$("#myModal").on("click", "#revisebtn", async function () {
    $('#myModal').hide();
    $('#revisecontent').empty();

    selectedDocumentId = $(this).find('.doc-id').text();

    let temp_html_reviseInfo = `
    <div class="mypostingbox" id="postingbox">
        <div class="form-floating mb-3">
            <input type="email" class="form-control" id="revise-image" placeholder="앨범 이미지">
            <label for="floatingInput">앨범 이미지</label>
        </div>
        <div class="form-floating mb-3">
            <input type="email" class="form-control" id="revise-title" placeholder="앨범 제목">
            <label for="floatingInput">앨범 제목</label>
        </div>
        <div class="form-floating mb-3">
            <input type="email" class="form-control" id="revise-content" placeholder="앨범 내용">
            <label for="floatingInput">앨범 내용</label>
        </div>
        <div class="form-floating mb-3">
            <input type="email" class="form-control" id="revise-date" placeholder="앨범 날짜">
            <label for="floatingInput">앨범 날짜</label>
        </div>
    </div>`
    $('#revisecontent').append(temp_html_reviseInfo);
    let revisedata = await getDoc(doc(db, "albums", selectedDocumentId));

    let row = revisedata.data();
    let currentImage = row['image'];
    let currentTitle = row['title'];
    let currentContent = row['content'];
    let currentDate = row['date'];

    $('#revise-image').val(currentImage);
    $('#revise-title').val(currentTitle);
    $('#revise-content').val(currentContent);
    $('#revise-date').val(currentDate);

    $('#reviseModal').show();
});

원래 modal창을 닫으며 시작한다.

수정 modal창엔 카드 업로드할 때 사용됐던 postingbox를 가져왔다. class이름만 조금 수정됐으며 수정될 문서의 정보를 받기 위해 getDoc을 사용했다.

받은 정보는 입력창에 넣어 수정되기 전 정보를 알려준다.

 

아래는 revisemodal 요소에 있던 '수정하기' 버튼을 누르면 실행될 코드이다.

$("#editCompleteBtn").click(async function () {
    // 수정된 정보 가져오기
    let revise_image = $('#revise-image').val();
    let revise_title = $('#revise-title').val();
    let revise_content = $('#revise-content').val();
    let revise_date = $('#revise-date').val();

    // Firebase에서 문서 수정
    await updateDoc(doc(db, "albums", selectedDocumentId), {
        image: revise_image,
        title: revise_title,
        content: revise_content,
        date: revise_date
    });
    alert('수정되었습니다.');
    window.location.reload

수정하기 버튼은 동적으로 생성된 요소가 아니므로 이벤트 위임을 할 필요가 없다.

revisemodal에서 받은 수정된 정보를 받아 updateDoc으로 문서를 업데이트함으로써 구현이 된다.

 

 

위 코드를 작성하면서 정말 많은 시행착오가 있었다. await 구문 하나를 빼 먹은 바람에 작동이 안돼 원인을 찾다가 몇시간을 쓰기도 하고, getDoc과 getDocs의 구분을 못해서 여기서도 몇시간을 소비했었다. 하지만 이렇게 원하는 결과물을 결국에는 얻고나니 몰랐던 개념도 더 자세하게 알게되고 기억에도 오래 남았다.

 

 

 

'캠프 개발일지' 카테고리의 다른 글

TIL - 23.11.30  (1) 2023.11.30
TIL - 2023.11.28  (1) 2023.11.28
3주차 개발 일지  (1) 2023.11.21
2주차 개발일지  (0) 2023.11.15
1주차 개발일지  (1) 2023.11.13