2차 공부/TIL

24.07.17 미니프로젝트 script, style 모듈 분리하기 / try{...} catch(err){...}

공대탈출 2024. 7. 17. 21:32

한 html에서 5명이 작업을 하다보니, 아래와 같이 script와 style태그가 엄청나게 길어졌다.

<script type="module">
      // 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,
      } 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";
      const firebaseConfig = {
        apiKey: "apiKey",
        authDomain: "authDomain",
        projectId: "projectId",
        storageBucket: "storageBucket",
        messagingSenderId: "messagingSenderId",
        appId: "appId",
        measurementId: "measurementId",
      };
      // Firebase 인스턴스 초기화
      const app = initializeApp(firebaseConfig);
      const db = getFirestore(app);

      let docs = await getDocs(collection(db, "members"));
      let memberArray = [];
      docs.forEach((doc) => {
        memberArray.push(doc.data());
      });

    //카드 스크립트
    $("#cardArea").empty();
    for (let i = 0; i < memberArray.length; i++) {
      let cardDesc = memberArray[i]["desc"];
      let cardId = memberArray[i]["id"];
      let cardImg = memberArray[i]["img"];
      let cardLikes = memberArray[i]["likes"];
      let cardName = memberArray[i]["name"];
      let cardComment = $('#inputContent').val();


      let tempCard_html = `
        <div class="col">
        <div id="${cardId}" class="wrapBox btnOpenModal">
          <li class="box1">
            <img
              class="img"
              src="${cardImg}"
            />
            <p>이름: ${cardName}</p>
            <p>한줄 소개: ${cardDesc}</p>
          </li>
          <li class="box2">
            <p id="${cardComment}">짱구 : 반가워요!</p>
            <p id="${cardComment}">짱아 : 반가워요!</p>
            <p id="${cardComment}">흰둥이 : 반가워요!</p>
          </li>
        </div>
        <div class="inputbox">
          <input id="inputContent" type="text" />
          <button id="inputBtn">입력</button>
        </div>
      </div>
        `;
      $("#cardArea").append(tempCard_html);
    }

      // 모달관련 스크립트
      const modal = $(".modal");
      let parent = $(".memberDesc");
      $(".btnOpenModal").click(function (e) {
        modal.css("display", "flex");
        const targetMemberId = this.id;
        const { name, img, desc, comment } = memberArray[targetMemberId - 1];
        let commentBox = ``;
        comment.forEach(
          (el) =>
            (commentBox += `<p><span>${el.cName}</span> : <span>${el.cDesc}</span></p>`)
        );
        let temp_html = `
                  <div class="memberIntroduce">
                      <image
                          class="memberImage"
                          src=${img}
                          alt="멤버 이미지"
                      />
                      <div>
                          <p><span>이름</span> : <span>${name}</span></p>
                          <p>
                              <span>간단한 소개</span> :
                              <span
                                  >${desc}</span
                              >
                          </p>
                      </div>
                  </div>
                  <div class="memberCommentBox">
                  ${commentBox}
                  </div>`;
        parent.append(temp_html);
      });
      const btnCloseModal = $(".btnCloseModal");
      btnCloseModal.click(function () {
        modal.css("display", "none");
        parent.empty();
      });
    </script>

 

<style>
      /* 카드 CSS*/
      .row row-cols-1 row-cols-md-2 g-4 {
        list-style: none;
      }
      .col {
        list-style: none;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin-top: 150px;
      }

      .img {
        width: 160px;
        height: 160px;
        float: left;
        padding: 15px 0px 0px 15px;
        margin-right: 10px;
      }

      .box1 {
        width: 400px;
        height: 180px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 8px;
        font-size: 15px;
        font-weight: 100;
      }

      .box2 {
        width: 400px;
        height: 100px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 8px;
        padding: 10px;
        font-size: 12px;
      }

      .box1 > p {
        padding: 15px;
      }

      .inputbox {
        width: 400px;
        height: 35px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 10px;
        margin-top: 10px;
        text-align: right;
        margin-bottom: 10px;
      }

      .inputbox > input {
        width: 320px;
        border: none;
      }

      .inputbox > button {
        height: 15px;
        border: none;
        font-weight: 100;
        padding-inline: 20px;
        background-color: transparent;
        padding-top: 5px;
      }

      /* 카드 CSS*/

      .mainContainer {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin: 5px auto;
      }
      /* 모달 관련 CSS */
.modal {
    position: fixed;
    display: none;
    justify-content: center;
    z-index: 100;
    width: 100%;
    height: 100%;
    background-color: rgb(0, 0, 0, 0.3);
    backdrop-filter: blur(10px);
    transition: all 0.1s;
  }
  .memberModal {
    background-color: white;
    border: 1px solid black;
    border-radius: 15px;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    gap: 5px;
    margin: auto;
    width: 40vw;
    min-height: 80vh;
    max-height: 100%;
    padding: 10px;
  }
  .closeBtnWrap {
    display: flex;
    flex-direction: row;
    justify-content: flex-end;
    width: 100%;
  }
  .btnCloseModal {
    background-color: transparent;
    border: none;
    color: red;
    font-weight: bold;
    font-size: 20px;
    width: 30px;
    height: 30px;
    background-image: url("https://cdn-icons-png.flaticon.com/512/7124/7124232.png");
    background-position: center;
    background-size: cover;
  }
  .btnCloseModal > image {
    object-fit: fill;
  }
  .memberDesc {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
  }
  .memberIntroduce {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    border: 1px solid black;
    border-radius: 15px;
    padding: 20px;
    gap: 20px;
    width: 35vw;
    height: 35vh;
    max-height: 100%;
    overflow-y: auto;
  }
  .memberImage {
    width: 200px;
    height: 200px;
  }
  .memberCommentBox {
    border: 1px solid black;
    border-radius: 15px;
    padding: 5px 20px;
    width: 35vw;
    height: 35vh;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    overflow-y: auto;
  }
  .memberCommentBox > p {
    width: 100%;
    border-bottom: 1px solid lightgray;
  }
  /* 모달 관련 CSS */
  @font-face {
    font-family: "ClimateCrisisKR-1979";
    src: url("https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2212@1.0/ClimateCrisisKR-1979.woff2")
      format("woff2");
    font-weight: 900;
    font-style: normal;
    }

    .title,
    .title * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .titleContainer {
      width: 100vw;
      height: 100vh;
      background-color: rgb(255, 221, 110);
      color: white;
      text-align: center;
      display: flex;
      flex-direction: column;
      justify-content: center;
      position: relative;
    }

    .title p {
      font-family: "ClimateCrisisKR-1979";
      font-size: 7rem;
      height: 8rem;
    }

    .scrollDownArrow {
      position: absolute;
      bottom: 10%;
      left: 50%;
      transform: translate(-50%, 0);

      display: inline-block;
      width: 5rem;
      height: 5rem;
      border-left: 0.3rem solid white;
      border-bottom: 0.3rem solid white;
      transform: rotate(-45deg);
    }

    .scrollDownArrow:hover {
      border-color: rgb(234, 234, 234);
    }
</style>

 


그래서 내가 작성한 style과 script들을 모듈화 시켜 분리작성해볼 생각이었다.

한 html파일내에서 다양한 기능과 다양한 view들이 작업되다보니 git관련 문제들도 많았고, 코드가 길어져 가독성이 떨어지므로 가능한 최소단위의 기능별로 모듈화시키기로 결정했다.

 

먼저 쉬운 style분리부터 진행하였다. 모달관련 style태그를 modal.css라는 파일을 생성하여 그곳에 몰아놓고,  index.html의 head에서 아래와 같이 해당 파일을 css로 불러왔다.

<link rel="stylesheet" type="text/css" href="modal.css" />

다행히 오류없이 정상작동하였다.

 

다음으로 script를 모듈화 시키는 것이었다. html파일에서 script를 모듈화 시켜본 적이 없어 예시를 찾기위해 바닐라 자바스크립트 script 모듈화에 대한 검색부터 시작했다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script>
        console.log('안녕!')
    </script>
</head>
<body>
</body>
</html>

예를들어 이러한 코드가 있다고 생각해보자. 우리는 script에 있는 test함수를 분리하여 js파일로 관리해야한다.

여기서는 한가지 함수만 모듈화 시키지만, 한 페이지에 여러개의 함수가 들어가므로 해당 모듈들을 합쳐줄 main 모듈이 필요하다. 나는 app.js를 메인모듈로 만들어 모듈들을 합쳐주었다.

 

export function test() {
    alert('안녕!')
}

이렇게 원하는 동작을 하는 함수를 js파일로 분리시키고 해당 함수를 export 시켜준다.

이때 export하고 다른 파일에서 import하여야 모듈화 된 test()를 사용할 수 있다.

 

다음으로 test-app.js에서 모든 모듈들을 모아야한다.

import { test } from "./test.js"

test()

위에서 export한 test함수를 import해와 실행시킨다. 다음으론 html파일에 모듈을 모아둔 test-app.js를 import해야한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script type="module" src="./test-app.js"></script>
</head>
<body>
</body>
</html>

type을 module로 지정하고 src를 잘 설정하여 특정 파일이 로드되도록 하면 모아진 모듈들이 잘 실행된다.


먼저 우리코드에서 firebase관련 스크립트를 분리시켰다. 페이지가 로드되면 firebase에서 데이터를 가져와 각 멤버에 대한 멤버카드를 뿌려주고, 멤버카드에 기능과 모달을 붙여주어야했기 때문이다.

 

 

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,
    getDocs
} from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";

let memberArray = [];
export async function firebaseFn() {
    console.log('firebaseFn실행')
    // Firebase SDK 라이브러리 가져오기
    const firebaseConfig = {
        apiKey: "apiKey",
        authDomain: "authDomain",
        projectId: "projectId",
        storageBucket: "storageBucket",
        messagingSenderId: "messagingSenderId",
        appId: "appId",
        measurementId: "measurementId",
    };
    // Firebase 인스턴스 초기화
    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);
    let docs = await getDocs(collection(db, "members"))
    console.log('fbFn= ', memberArray)
    docs.forEach((doc) => {
        memberArray.push(doc.data());
    });
    console.log('firebaseFn종료')
}
export default memberArray;

이런식으로 모듈을 분리했고 각 기능에 사용되는 memberArray를 export default해주어 다른 함수모듈에서도 사용가능하도록 하였다.

 

 

import memberArray from "./firebase.js";
// 모달관련 스크립트
export function modalFn() {
    console.log('modalFn실행')
    const modal = $(".modal");
    let parent = $(".memberDesc");
    $(".btnOpenModal").click(function (e) {
      modal.css("display", "flex");
      const targetMemberId = this.id;
      const { name, img, desc, comment } = memberArray[targetMemberId - 1];
      let commentBox = ``;
      comment.forEach(
        (el) =>
          (commentBox += `<p><span>${el.cName}</span> : <span>${el.cDesc}</span></p>`)
      );
      let temp_html = `
                <div class="memberIntroduce">
                    <image
                        class="memberImage"
                        src=${img}
                        alt="멤버 이미지"
                    />
                    <div>
                        <p><span>이름</span> : <span>${name}</span></p>
                        <p>
                            <span>간단한 소개</span> :
                            <span
                                >${desc}</span
                            >
                        </p>
                    </div>
                </div>
                <div class="memberCommentBox">
                ${commentBox}
                </div>`;
      parent.append(temp_html);
    });
    const btnCloseModal = $(".btnCloseModal");
    btnCloseModal.click(function () {
      modal.css("display", "none");
      parent.empty();
    });
    console.log('modalFn종료')
}

다음은 먼저 내가 작업한 모달부터 분리하였다. 물론 카드를 뿌려주는 작업을 분리해놓지 않아 바로 작동하진 않았지만 크게 다른곳에 의존하는 부분이 없어 걱정하진 않았다. 그리고 앞에서 export해준 memberArray를 import해와 데이터를 사용하였다.

 

import memberArray from "./firebase.js";
export function cardFn() {
    console.log('cardFn실행')
    console.log(memberArray)
    //카드 스크립트
    $("#cardArea").empty();
    for (let i = 0; i < memberArray.length; i++) {
        let cardDesc = memberArray[i]["desc"];
        let cardId = memberArray[i]["id"];
        let cardImg = memberArray[i]["img"];
        let cardLikes = memberArray[i]["likes"];
        let cardName = memberArray[i]["name"];
        let cardComment = $('#inputContent').val();


        let tempCard_html = `
        <div class="col">
        <div id="${cardId}" class="wrapBox btnOpenModal">
            <li class="box1">
            <img
                class="img"
                src="${cardImg}"
            />
            <p>이름: ${cardName}</p>
            <p>한줄 소개: ${cardDesc}</p>
            </li>
            <li class="box2">
            <p id="${cardComment}">짱구 : 반가워요!</p>
            <p id="${cardComment}">짱아 : 반가워요!</p>
            <p id="${cardComment}">흰둥이 : 반가워요!</p>
            </li>
        </div>
        <div class="inputbox">
            <input id="inputContent" type="text" />
            <button id="inputBtn">입력</button>
        </div>
        </div>
        `;
        $("#cardArea").append(tempCard_html);
    }
    console.log('cardFn종료')
}

cardFn에서도 앞에서 export한 memberArray를 import해와서 데이터로 사용하는 것이다.

 

그리고 app.js에서 해당 모듈들을 한번에 모아 실행시켜준다.

import { firebaseFn } from "./firebase.js"
import { cardFn } from "./cards.js"
import { modalFn } from "./modal.js"

console.log('appjs실행')
firebaseFn().then(()=> {
    cardFn()
    modalFn()
})

처음엔 .then을 사용하지 않아 동시에 실행되다보니, 데이터가 넘어오지 않은 상태에서 memberArray를 조회하고, 그 과정에서 오류가 발생하였다. 따라서 .then을 사용하여 데이터가 넘어온 후 순차적으로 실행하도록 하였다.

 

이때까지는 문제가 없었다.


서버로부터 GET해온 데이터를 그냥 뿌려주기만 하면 되다보니 큰 문제 없이 잘 실행되었지만, 댓글을 어떤 데이터의 배열에 추가하거나, 서버에 존재하는 좋아요 count값을 더하거나 빼는 과정에서 문제가 발생하였다.

앞에서 memberArray를 export default해주었는데 이 방식은 데이터가 비동기적으로 처리될 때 비어있거나 유효하지 않은 상태일 수도 있어 문제가 생긴 것이었다.

단순하게 comment라는 배열이 있다고 가정해보자. 서버에 하드코딩된 comment는 내부에 객체들을 가지고있고, 우리는 하드코딩 되어있기 때문에 당연히 comment라는 값이 배열이라고 생각한다.

하지만 export default해준 memberArray가 특정 상황에 완전하지 않거나 비어있는 상태로 존재하여 우리가 배열로 생각하던 comment는 그냥 undefined이었던 것이다.

 

당연하게도 배열이라 생각했던 터라 문제가 어디서 발생했는지 생각하기가 어려웠다.

좀더 구글링해보니 async await와 try catch를 사용하여 데이터가 유효할 때에 .then의 인자로 넣어주고, 해당 인자를 cardFn과 modalFn에 보내주어 우리가 생각하는 형태와 동일하게 사용할 수 있었다.

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

    export let db;
    export async function firebaseFn() {
        let memberArray = [];
        // Firebase SDK 라이브러리 가져오기
        const firebaseConfig = {
            apiKey: "apiKey",
            authDomain: "authDomain",
            projectId: "projectId",
            storageBucket: "storageBucket",
            messagingSenderId: "messagingSenderId",
            appId: "appId",
            measurementId: "measurementId",
        };
        // Firebase 인스턴스 초기화
        const app = initializeApp(firebaseConfig);
        db = getFirestore(app);
        try {
            let docs = await getDocs(collection(db, "members"))
            docs.forEach((doc) => {
                memberArray.push(doc.data());
            });
            return memberArray
        } catch(err) {
            console.log('에러 발생, ', err)
            return;
        }
    }

이런 식으로 try의 마지막에서 return하여 원하는 데이터를 품고있는 memberArray를 보내주고,

 

import { firebaseFn } from "./firebase.js"
import { cardFn } from "./cards.js"
import { modalFn } from "./modal.js"
import { setScrollEvent } from "./title.js"

firebaseFn().then((memberArray)=> {
    cardFn(memberArray)
    modalFn(memberArray)
})

setScrollEvent();

해당 인자값을 cardFn과 modalFn으로 보내주어 사용하였기 때문에 유효하지 않은 값이 들어있을 수가 없는 것이다.

 


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- 부트스트랩 -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
      crossorigin="anonymous"
    />
    <!-- jQuery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!-- 모달 css -->
    <link rel="stylesheet" type="text/css" href="modal.css" />
    <!-- 타이틀 css -->
    <link rel="stylesheet" href="title.css">
    <!-- script import -->
    <script type="module" src="./modules/app.js"></script>
    <title>간계밥</title>
    <style>
      /* 카드 CSS*/
      .row row-cols-1 row-cols-md-2 g-4 {
        list-style: none;
      }
      .col {
        list-style: none;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin-top: 150px;
      }

      .img {
        width: 160px;
        height: 160px;
        float: left;
        padding: 15px 0px 0px 15px;
        margin-right: 10px;
      }

      .box1 {
        width: 400px;
        height: 180px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 8px;
        font-size: 15px;
        font-weight: 100;
      }

      .box2 {
        width: 400px;
        height: 100px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 8px;
        padding: 10px;
        font-size: 12px;
      }

      .box1 > p {
        padding: 15px;
      }

      .inputbox {
        width: 400px;
        height: 35px;
        box-shadow: 0px 0px 3px 0px gray;
        border-radius: 10px;
        margin-top: 10px;
        text-align: right;
        margin-bottom: 10px;
      }

      .inputbox > input {
        width: 320px;
        border: none;
      }

      .inputbox > button {
        height: 15px;
        border: none;
        font-weight: 100;
        padding-inline: 20px;
        background-color: transparent;
        padding-top: 5px;
      }

      /* 카드 CSS*/

      .mainContainer {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin: 5px auto;
      }
    </style>
  </head>

  <body>
    <!-- 메인화면 HTML-->
    <div class="titleContainer">
      <div class="title">
        <p>안녕하세요.</p>
        <p>저희는 팀 간계밥 입니다.</p>
        <p>간계밥 만들듯이 뚝딱뚝딱!</p>
        <p>잘 비벼보겠습니다.</p>
      </div>
      <i class="scrollDownArrow"></i>
    </div>
    <!-- 메인화면 HTML-->
    
    <!-- 카드화면 HTML-->
    <div id="cardArea" class="row row-cols-1 row-cols-md-2 g-4"></div>
    <!-- 카드화면 HTML -->

    <!-- 모달화면 HTML -->
    <div class="modal">
      <div class="memberModal">
        <div class="closeBtnWrap">
          <button class="btnCloseModal"></button>
        </div>
        <div class="memberDesc"></div>
      </div>
    </div>
    <!-- 모달화면 HTML -->
  </body>
  
</html>

아직 완벽하게 style이 분리되진 않았지만, 최대한 style을 분리하고 script를 모듈화 시키니 본 html도 많이 줄어든 것을 볼 수 있다.

또한 기능에따라 script가 분리되어있어 좀 더 직관적인 코드모음이 되었다고 생각한다.