개발 낙서장

[TIL] 내일배움캠프 5일차 - 모듈 - 본문

Java/Sparta

[TIL] 내일배움캠프 5일차 - 모듈 -

권승준 2023. 12. 28. 17:54

 

 

오늘의 학습 키워드📚

모듈

https://dachomi97.tistory.com/51

 

HTML과 CSS, Script 파일 나누기

페이지에 이것저것 추가할 수록 HTML은 물론 CSS, Script 등 같이 늘어나기 때문에 유지보수가 힘들어진다. 내가 만든 추억 앨범 페이지만 해도 되게 기능이 별거 없지만 코드만 400줄이 넘어간다. 꼭

dachomi97.tistory.com

여기에 포스팅한 것처럼 HTML 파일이 너무 길어져서 CSS와 Script를 나누고자 했다.
그래서 css와 js를 각각 파일로 만들고

<script type="module" src="album_js.js"></script>

script 부분을 수정하니 짠!

아무것도 나오질 않는다.😩

문제가 무엇인가 하니 나는 스크립트를 모듈로 작성했기 때문인 것 같다.

아직 자세한 개념은 모르지만 대충 요약하면 모듈은 '기능이 구현된 인터페이스'와 비슷한 역할을 한다.
모듈 내부에서 구현한 함수 혹은 변수, 객체 등을 export 하고 그걸 메인 스크립트에서 import 해서 상황에 맞게 사용해야 된다고 한다.
즉 저 스크립트를 그대로 옮겨쓰는 것이 아니라 실행 부분은 따로 메인 js를 만들고 모듈 내부에서는 함수만 구현해야 한다.

더보기
// Firebase SDK 라이브러리 가져오기
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, updateDoc, doc, deleteDoc } 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";

// Firebase 구성 정보 설정
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
    apiKey: "AIzaSyCLaWzL8oAv7E0itukYJvxSeVI6r4q_j50",
    authDomain: "sparta-6aa4c.firebaseapp.com",
    projectId: "sparta-6aa4c",
    storageBucket: "sparta-6aa4c.appspot.com",
    messagingSenderId: "863460667202",
    appId: "1:863460667202:web:9b21503063c801036062db",
    measurementId: "G-Q6NW6NL4JT"
};

// Firebase 인스턴스 초기화
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

var albumDic = {};
var currentId;

$("#postingBtn").click(async function () {
    albumCreate();
});

$('.posting-toggle').click(async function () {
    $('#postingbox').toggle();
})

let url = "http://spartacodingclub.shop/sparta_api/seoulair";
fetch(url).then(res => res.json()).then(data => {
    let IDEX_NM = data['RealtimeCityAir']['row'][0]['IDEX_NM'];
    $('#msg').text(IDEX_NM);
})

let count = 0;
$('#cardsBox').empty();
let docs = await getDocs(collection(db, "albums"));
docs.forEach((doc) => {
    let row = doc.data();

    let image = row['image'];
    let title = row['title'];
    let content = row['content'];
    let date = row['date'];

    albumDic[doc.id] = row;

    let cardHtml =
        `
            <div class="col">
                <div class="card h-100">
                    <img src="${image}" class="card-img-top" alt="..." style="max-width: 100%; height: auto;">
                    <div class="card-body">
                        <h5 class="card-title">${title}</h5>
                        <p class="card-text text-hidden">${content}</p>
                    </div>
                    <div class="card-footer">
                        <small class="text-body-secondary">${date}</small>
                    </div>
                    <button class="open-album" type="button" data-bs-toggle="modal"
                        data-bs-target="#album-modal" data-album-id="${doc.id}">상세 보기</button>
                </div>
            </div>
            `;

    $('#cardsBox').append(cardHtml);
});

$('.open-album').click(function () {
    currentId = $(this).data('album-id');
    let albumData = albumDic[currentId];

    modalToggle(true);

    $('#modal-writer').text('작성자 : ' + albumData['name']);
    $('#modal-title-label').text(albumData['title']);
    $('#modal-content-label').text(albumData['content']);
    $('#modal-image').attr('src', albumData['image']);
})

$('#modal-edit-btn').click(function () {
    modalToggle(false);

    $('#modal-title-input').val($('#modal-title-label').text());
    $('#modal-content-input').val($('#modal-content-label').text());
    $('#modal-image-input').val($('#modal-image').attr('src'));
    $('#modal-password').val('');
})

$('#modal-save-btn').click(async function () {
    albumEdit();
})

$('#modal-delete-btn').click(async function () {
    albumDelete();
})

async function albumCreate() {
    let image = $('#imageInput').val();
    let title = $('#titleInput').val();
    let content = $('#contentInput').val();
    let name = $('#nameInput').val();
    let password = $('#passwordInput').val();
    let passwordRe = $('#passwordReInput').val();

    let today = new Date();
    let year = today.getFullYear(); // 년도
    let month = today.getMonth() + 1;  // 월
    let date = today.getDate();  // 날짜

    if (checkInput(image, title, content, name, password, passwordRe) == false)
        return;

    let data = {
        'image': image,
        'title': title,
        'content': content,
        'date': year + '/' + month + '/' + date,
        'name': name,
        'password': password
    }
    await addDoc(collection(db, "albums"), data);

    alert('저장 완료!');
    window.location.reload();
}

async function albumEdit() {
    let image = $('#modal-image-input').val();
    let title = $('#modal-title-input').val();
    let content = $('#modal-content-input').val();
    let name = albumDic[currentId]['name'];
    let password = $('#modal-password-input').val();
    let passwordRe = albumDic[currentId]['password'];

    let today = new Date();
    let year = today.getFullYear(); // 년도
    let month = today.getMonth() + 1;  // 월
    let date = today.getDate();  // 날짜

    if (checkInput(image, title, content, name, password, passwordRe) == false)
        return;

    let data = {
        'image': image,
        'title': title,
        'content': content,
        'date': year + '/' + month + '/' + date,
        'name': name,
        'password': password
    }
    await updateDoc(doc(db, "albums", currentId), data);

    alert('저장 완료!');
    window.location.reload();
}

async function albumDelete() {
    var passwordInput = prompt('삭제하시려면 비밀번호를 입력하세요.');

    if (passwordInput == albumDic[currentId]['password']) {
        if (confirm('정말' + albumDic[currentId]['title'] + ' 앨범을 삭제하시겠습니까?')) {
            await deleteDoc(doc(db, "albums", currentId));
            alert('삭제가 완료되었습니다.');
            window.location.reload();
        }
    }
    else {
        alert('비밀번호가 틀렸습니다.');
    }
}

function checkInput(image, title, content, name, password, passwordRe) {
    if (!image.trim() || !title.trim() || !content.trim() || !name.trim() || !password.trim() || !passwordRe.trim()) {
        alert('빈 값을 채워주세요.');
        return false;
    }

    if (/\.(png|jpg|jpeg|gif)(\?.*)?$/.test(image) == false) {
        alert('이미지 주소를 확인해주세요.');
        return false;
    }

    if (password.trim() != passwordRe.trim()) {
        alert('비밀번호가 같은지 확인해주세요.');
        return false;
    }

    return true;
}

function modalToggle(flag) {
    if (flag == true) {
        $('#modal-title-label').css('display', '');
        $('#modal-title-input').css('display', 'none');

        $('#modal-content-label').css('display', '');
        $('#modal-content-input').css('display', 'none');

        $('#modal-image').css('display', '');
        $('#modal-image-input').css('display', 'none');

        $('#modal-password').css('display', 'none');

        $('#modal-save-btn').css('display', 'none');
        $('#modal-edit-btn').css('display', '');
    }
    else {
        $('#modal-title-label').css('display', 'none');
        $('#modal-title-input').css('display', '');

        $('#modal-content-label').css('display', 'none');
        $('#modal-content-input').css('display', '');

        $('#modal-image').css('display', 'none');
        $('#modal-image-input').css('display', '');

        $('#modal-password').css('display', '');

        $('#modal-save-btn').css('display', '');
        $('#modal-edit-btn').css('display', 'none');
    }
}

이게 현재 스크립트인데, click 이벤트나 onload 동작들을 main 스크립트로 옮기고 나머지 함수나 변수들은 export 형식으로 바꿔주는 작업이 필요하다.

우선 진행을 하려고 하니 로컬에서 html 파일을 열변 CORS 보안 때문에 분리된 js 파일을 읽어올 수 없다는 에러가 발생한다.
CORS란 교차 출처 리소스 공유인데 이  동일 출처 정책으로 인해 호스트, 포트, 프로토콜이 다른 url 으로부터의 응답은 script에서 받을 수 없다. 보통 서버단에서 이러한 문제가 발생하는데 왜 로컬에서 실행하는데도 이런 에러가 발생할까?

<script type="module" src="album_js.js"></script>

정답은 type 속성 때문이다. 모듈을 import, export하기 위해 type 속성을 모듈로 js 파일을 불러오는데 자바 스크립트 모듈 보안 요구사항 때문에 CORS 보안 에러를 발생 시킨다고 한다.

해결법은 간단하다. vs code의 확장 프로그램인 live server를 설치해서
html 파일을 우클릭 -> Open with Live Server(혹은 Alt + L + Alt + O) 방법으로 실행하면 해결된다.

참고 : https://dkrnfls.tistory.com/298


어쨌든 코드 수정은 다음과 같이 했다.

더보기

album_module.js

// Firebase SDK 라이브러리 가져오기
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, updateDoc, doc, deleteDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";

// Firebase 구성 정보 설정
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
    apiKey: "AIzaSyCLaWzL8oAv7E0itukYJvxSeVI6r4q_j50",
    authDomain: "sparta-6aa4c.firebaseapp.com",
    projectId: "sparta-6aa4c",
    storageBucket: "sparta-6aa4c.appspot.com",
    messagingSenderId: "863460667202",
    appId: "1:863460667202:web:9b21503063c801036062db",
    measurementId: "G-Q6NW6NL4JT"
};

// Firebase 인스턴스 초기화
export const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);

export async function albumCreate() {
    let image = $('#imageInput').val();
    let title = $('#titleInput').val();
    let content = $('#contentInput').val();
    let name = $('#nameInput').val();
    let password = $('#passwordInput').val();
    let passwordRe = $('#passwordReInput').val();

    let today = new Date();
    let year = today.getFullYear(); // 년도
    let month = today.getMonth() + 1;  // 월
    let date = today.getDate();  // 날짜

    if (checkInput(image, title, content, name, password, passwordRe) == false)
        return;

    let data = {
        'image': image,
        'title': title,
        'content': content,
        'date': year + '/' + month + '/' + date,
        'name': name,
        'password': password
    }
    await addDoc(collection(db, "albums"), data);

    alert('저장 완료!');
    window.location.reload();
}

export async function albumEdit() {
    let image = $('#modal-image-input').val();
    let title = $('#modal-title-input').val();
    let content = $('#modal-content-input').val();
    let name = albumDic[currentId]['name'];
    let password = $('#modal-password-input').val();
    let passwordRe = albumDic[currentId]['password'];

    let today = new Date();
    let year = today.getFullYear(); // 년도
    let month = today.getMonth() + 1;  // 월
    let date = today.getDate();  // 날짜

    if (checkInput(image, title, content, name, password, passwordRe) == false)
        return;

    let data = {
        'image': image,
        'title': title,
        'content': content,
        'date': year + '/' + month + '/' + date,
        'name': name,
        'password': password
    }
    await updateDoc(doc(db, "albums", currentId), data);

    alert('저장 완료!');
    window.location.reload();
}

export async function albumDelete() {
    var passwordInput = prompt('삭제하시려면 비밀번호를 입력하세요.');

    if (passwordInput == albumDic[currentId]['password']) {
        if (confirm('정말' + albumDic[currentId]['title'] + ' 앨범을 삭제하시겠습니까?')) {
            await deleteDoc(doc(db, "albums", currentId));
            alert('삭제가 완료되었습니다.');
            window.location.reload();
        }
    }
    else {
        alert('비밀번호가 틀렸습니다.');
    }
}

function checkInput(image, title, content, name, password, passwordRe) {
    if (!image.trim() || !title.trim() || !content.trim() || !name.trim() || !password.trim() || !passwordRe.trim()) {
        alert('빈 값을 채워주세요.');
        return false;
    }

    if (/\.(png|jpg|jpeg|gif)(\?.*)?$/.test(image) == false) {
        alert('이미지 주소를 확인해주세요.');
        return false;
    }

    if (password.trim() != passwordRe.trim()) {
        alert('비밀번호가 같은지 확인해주세요.');
        return false;
    }

    return true;
}

export function modalToggle(flag) {
    if (flag == true) {
        $('#modal-title-label').css('display', '');
        $('#modal-title-input').css('display', 'none');

        $('#modal-content-label').css('display', '');
        $('#modal-content-input').css('display', 'none');

        $('#modal-image').css('display', '');
        $('#modal-image-input').css('display', 'none');

        $('#modal-password').css('display', 'none');

        $('#modal-save-btn').css('display', 'none');
        $('#modal-edit-btn').css('display', '');
    }
    else {
        $('#modal-title-label').css('display', 'none');
        $('#modal-title-input').css('display', '');

        $('#modal-content-label').css('display', 'none');
        $('#modal-content-input').css('display', '');

        $('#modal-image').css('display', 'none');
        $('#modal-image-input').css('display', '');

        $('#modal-password').css('display', '');

        $('#modal-save-btn').css('display', '');
        $('#modal-edit-btn').css('display', 'none');
    }
}

 

album_js.js

import {
    db,
    albumCreate, albumEdit, albumDelete, modalToggle
} from "./album_module.js";
import { getDocs, collection } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";

var albumDic = {};
var currentId;

$("#postingBtn").click(async function () {
    albumCreate();
});

$('.posting-toggle').click(async function () {
    $('#postingbox').toggle();
})

$(async function () {
    let url = "http://spartacodingclub.shop/sparta_api/seoulair";
    fetch(url).then(res => res.json()).then(data => {
        let IDEX_NM = data['RealtimeCityAir']['row'][0]['IDEX_NM'];
        $('#msg').text(IDEX_NM);
    })

    $('#cardsBox').empty();
    let docs = await getDocs(collection(db, "albums"));
    docs.forEach((doc) => {
        let row = doc.data();

        let image = row['image'];
        let title = row['title'];
        let content = row['content'];
        let date = row['date'];

        albumDic[doc.id] = row;

        let cardHtml =
            `
            <div class="col">
                <div class="card h-100">
                    <img src="${image}" class="card-img-top" alt="..." style="max-width: 100%; height: auto;">
                    <div class="card-body">
                        <h5 class="card-title">${title}</h5>
                        <p class="card-text text-hidden">${content}</p>
                    </div>
                    <div class="card-footer">
                        <small class="text-body-secondary">${date}</small>
                    </div>
                    <button class="open-album" type="button" data-bs-toggle="modal"
                        data-bs-target="#album-modal" data-album-id="${doc.id}">상세 보기</button>
                </div>
            </div>
            `;

        $('#cardsBox').append(cardHtml);
    });
})

$('.open-album').click(function () {
    currentId = $(this).data('album-id');
    let albumData = albumDic[currentId];

    modalToggle(true);

    $('#modal-writer').text('작성자 : ' + albumData['name']);
    $('#modal-title-label').text(albumData['title']);
    $('#modal-content-label').text(albumData['content']);
    $('#modal-image').attr('src', albumData['image']);
})

$('#modal-edit-btn').click(function () {
    modalToggle(false);

    $('#modal-title-input').val($('#modal-title-label').text());
    $('#modal-content-input').val($('#modal-content-label').text());
    $('#modal-image-input').val($('#modal-image').attr('src'));
    $('#modal-password').val('');
})

$('#modal-save-btn').click(async function () {
    albumEdit();
})

$('#modal-delete-btn').click(async function () {
    albumDelete();
})

module 부분에 Firebase 초기화와 이벤트 발생시 필요한 함수들을 구현했고 js 부분에서는 모듈을 import해 이벤트를 발생시켰다.
(** import한 변수나 함수는 '사용'만 가능하다. 수정은 불가능하다. 외부에서 수정하려면 수정하는 함수를 따로 만들어 export 해주고 해당 함수를 import 해 사용하면 된다.)

이렇게 파일을 총 4개(html, css, js 두 개)로 분리했다.
html 파일은 약 400줄에서 114줄로 줄었고 스크립트도 모듈과 실행 부분이 나눠져서 만약 앞으로 수정 사항이 생길 경우 작업이 훨씬 수월해질 것 같다.

이벤트 바인딩💦

❗❗ 그런데 한 가지 문제가 발생했다.

분명 잘만 실행되던 상세 보기 버튼이 먹통이 돼버린 것이다.
선언 부분을 옮겨보기도 하고 import를 수정해보기도 해도 똑같았다.

문제는 앨범 카드들을 동적으로 생성해주면서 이벤트 바인딩이 되지 않는 문제였다.
(진짜 무슨 문제인지 전혀 몰라서 구글링만 1시간을 한 것 같다😥)

정확하진 않을 수 있지만, 카드를 생성하는 건 페이지가 전부 로딩되고 나서 실행되는 $(document).ready() 내부에서 생성한다.
이 과정에서 아직 카드가 생기지도 않았는데 이벤트를 바인딩해버리기 때문에 이후에 생성된 카드에서는 이벤트가 바인딩되지 않아 아무리 클릭해도 동작하지 않았던 것이다.

https://minkdak.tistory.com/5
이 블로그에 훨씬 자세한 설명이 나와있다.

그래서 동적으로 이벤트를 발생시켜주는 $(document).on()을 사용하면 해결된다.

$(document).on('click', '.open-album', function(e) {
    e.preventDefault();
    currentId = $(this).data('album-id');
    let albumData = albumDic[currentId];

    modalToggle(true);

    $('#modal-writer').text('작성자 : ' + albumData['name']);
    $('#modal-title-label').text(albumData['title']);
    $('#modal-content-label').text(albumData['content']);
    $('#modal-image').attr('src', albumData['image']);
}

근데 사실 기존 클릭 이벤트를 $(document).ready() 안에서 카드가 생성되고 난 다음 줄에 넣어주면 잘 작동하긴 한다.

어쨌든 새로운 방법을 공부하고 알게 됐다.


오늘의 회고💬

웹 개발 주차는 오늘로 끝났다. 이제 내일부터는 Java 공부를 시작해야 한다.
지금 취미로 OpenAPI를 만지고 있어서 아마 웹 개발은 계속 병행할 것 같긴 한데 그래도 나는 백엔드 개발자가 될 것이니까 이제부터는 자바에 많은 집중을 쏟아야겠다.

 

내일의 계획📜

자바 START💨

Comments