Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2주차 기본/심화 과제] 웨비들의 냠냠 🍰 창업🏠 손님을 모셔오자!🌈 #4

Merged
merged 32 commits into from
May 4, 2023

Conversation

simeunseo
Copy link
Member

@simeunseo simeunseo commented Apr 18, 2023

🔗 구현 페이지

✨ 구현 기능 명세

  • 기본 과제

    • 상품 데이터

      • 상품 데이터를 상수파일에 저장해 사용
    • nav

      • 종류 선택시 태그를 카드섹션 위에 하나씩 부착
      • 태그별 상품 리스트 필터링
      • 태그의 X 클릭시 태그 삭제, 체크해제, 재필터링
    • card article

      • 해시태그 리스트의 + 아이콘 클릭시 태그 모달 띄우기
      • 해시태그 전체 목록 보여주기
      • x버튼 또는 모달 자체를 누르면 모달 닫기
  • 심화 과제

    • 목록

      • 새 상품 추가 페이지 이동
    • 새상품 추가 페이지

      • label, input을 연결시켜 form 구현
      • 태그 종류는 ,로 구분해서 처리
      • 이미지 미리보기
      • 폼 제출시 localStorage에 추가 후 홈으로 이동
      • 새로 추가된 상품 확인 가능
    • 카드 애니메이션

      • 상품 카드들이 달라질 때 fadeIn 애니메이션

🌼 PR Point

  • template 태그 사용

    정찬우씨가 template 태그라는 신문물을 알려주셔서 적극 활용해보았습니다! 추가로 https://youtu.be/sgJMeiV0tyc 이 영상 보니 이해가 확 되었어요! 정말 유용해요!!
    template 태그를 사용하면, 동적으로 사용할 태그 '템플릿'을 html문서에 만들어놓고 javascript에서 가져다 쓸 수 있어요. MDN에서는 콘텐츠 조각을 나중에 사용하기 위해 담아놓는 컨테이너 라고 소개하고 있습니다. template 태그 내의 콘텐츠는 페이지가 로드될 때 즉시 렌더링되지 않기 때문에 사용자에게는 보이지 않구요, 나중에 자바스크립트로 해당 콘텐츠를 복제해서 렌더링하는 방식으로 사용합니다.
    예를들어 이번 과제에서 아이템 카드들을 동적으로 생성해야 하잖아요. 이걸 javascript로 하나하나 createElement...하면 만들게 너무 많아지니까 아래와 같이 template태그로 아이템 카드의 틀을 미리 만들어 놓습니다. 보시면 템플릿 안에서 동적으로 변경되어야 하는 내용들은 {item_name}이런식으로 작성해두었어요.

      <template id="cards__template">
          <article class="cards__card">
            <div class="cards__card__modal">
              <div class="cards__card__modal__tags">
                <button class="tag__close-btn" type="button">
                  <i class="fa-solid fa-x"></i></button
                >{modal_tags}
              </div>
            </div>
            <h3>{item_name}</h3>
            <div class="cards__card__tags__container">
              <div class="cards__card__tags">{tags}</div>
              <button class="tags__plus-btn" type="button">
                <i class="fa-solid fa-circle-plus"></i>
              </button>
            </div>
            <img alt="{img_alt}" src="{img_src}" />
            <i class="fa-solid fa-heart"></i>
          </article>
        </template>

    그리고 이걸 js파일에서 가져와서, replace() method로 변경할 부분을 변경하고 집어넣는 방식이에요!

    const cardsSection = document.getElementById("cards"); //card들이 들어갈 부모노드
    const cardTemplate = document.getElementById("cards__template"); //card 템플릿
    
    //list를 탐색하면서 요소를 하나씩 card 노드로 만드는 함수
    function listToCard(list) {
      cardsSection.replaceChildren();
      list.forEach((item) => {
        //tag들 또한 리스트이므로 그 안에서 map을 돌린다.
        let tags = ``;
        item.tags.forEach((tag) => {
          tags += `<small>` + tag + `</small>\n`;
        });
    
        let content = cardTemplate.cloneNode(true); //템플릿 복사
        let newHtml = content.innerHTML; //템플릿 안의 html 복사
        newHtml = newHtml //복사한 html에서 필요한 부분을 item 내용에 맞게 변경
          .replace("{item_name}", item.name)
          .replace("{tags}", tags)
          .replace("{modal_tags}", tags)
          .replace("{img_alt}", item.name)
          .replace("{img_src}", item.img);
    
        content.innerHTML = newHtml; //새롭게 바뀐 html을 템플릿에 적용
        cardsSection.appendChild(content.content); //부모노드 안에 넣기
      });
    }

    이런식으로 카드아이템을 구현했고, modal또한 동적으로 반복되는 부분이기에 template태그로 구현하였습니다. 와우 너무 스마트해요

  • 카테고리 필터링

    카테고리 필터링에서는 체크가 된건지 해제된건지를 나타내는 isChecked, 변화가 나타난 카테고리 이름인 categoryName, 그리고 최종적으로 화면에 보여지는 아이템 목록을 담을 list가 핵심입니다. newItemList는 localStorage에서 가져온 전체 아이템 리스트입니다.

    if (categoryName === "전체") {
      isChecked //전체 카테고리 선택 시
        ? newItemList.forEach((item) => {
            //list에 ITEM_LIST의 모든 항목을 넣음
            list.push(item);
            list = Array.from(new Set(list));
          })
        : checkBoxList.forEach((item) => {
            //전체 카테고리 선택 해제 시
            item.checked || //체크박스 목록에서 체크가 안된것은 list에서 제거
              (list = removeByCategoryName(list, CATEGORY_NAME[item.id]));
          });
    }

    변화가 감지된 카테고리가 "전체"일 때와 아닐 때로 나누어서 구현했습니다. 먼저 "전체" 카테고리를 선택했을 때는 단순히 newItemList의 모든 항목을 보여줄 아이템으로 push했습니다. "전체" 카테고리 선택 해제시에는 현재 체크박스 목록을 다시 훑으면서, 선택되어있는 카테고리는 "전체" 카테고리가 해제되더라도 보여줘야하므로, 선택되어있지 않은 카테고리의 아이템만 제거하였습니다.

      isChecked //전체가 아닌 다른 카테고리 선택 시
        ? newItemList.forEach((item) => {
            item.category === categoryName && //해당 카테고리에 속하는 item들을 list에 넣음
              (list.push(item), (list = Array.from(new Set(list))));
          })
        : //카테고리 선택 해제 시, 'check-all' 체크박스가 선택이 안되어있는 상태라면
          checkBoxAll.checked || //해당 카테고리에 속하는 item들을 list에서 제거
          (list = removeByCategoryName(list, categoryName));

    카테고리 필터링은 기본적으로 체크박스의 변화를 감지할 때 위의 반복문을 돌려서 구현했는데요. newItemList의 요소를 하나씩 보면서 현재 변화가 감지된 카테고리에 해당하는 아이템이라면 list에 push하거나, 삭제합니다. 그런데 "전체"카테고리가 선택되어 있으면 삭제하면 안되므로 선택되어있지 않을 때만 삭제하도록 했습니다. (이 단순한 아이디어를 생각해내는데 꽤 오래걸렸네요...) removeByCategoryName()은 categoryName에 해당하는 아이템을 list에서 제거하는 함수로, 다음과 같이 filter() 메소드를 사용했습니다.

    function removeByCategoryName(list, categoryName) {
      list = list.filter((item) => item.category != categoryName);
      return list;
    }
  • 카테고리 태그 만들기

    <template id="category-tags__template">
        <span class="category-tag" id="{category-tag_id}">{category_name}
          <label for="{checkbox_id}">
            <i class="fa-solid fa-circle-xmark"></i>
          </label>
        </span>
    </template>
    function makeCategoryTag(checkBox) {
      if (checkBox.checked) {
        let content = categoryTagTemplate.cloneNode(true); //템플릿 복사
        let newHtml = content.innerHTML; //템플릿 안의 html 복사
        newHtml = newHtml
          .replace("{category_name}", CATEGORY_NAME[checkBox.id])
          .replace("{checkbox_id}", checkBox.id)
          .replace("{category-tag_id}", "tag__" + checkBox.id);
        content.innerHTML = newHtml;
        categoryTagSection.appendChild(content.content);
      } else {
        const target = document.getElementById("tag__" + checkBox.id);
        target.remove();
      }
    }

    카테고리 태그를 만드는 부분은 걱정했던 것 보단 별 문제 없이 구현했습니다. 왜냐면 카테고리 태그의 X버튼을 label의 for속성을 통해서 실제 nav의 체크박스와 연동(?)을 했더니 굉장히 간편하게 구현이 되더라구요!

  • 이미지 미리보기

    const imageInput = document.getElementById("image");
    const imageThumbNail = document.getElementById("image-thumbnail");
    imageInput.addEventListener("input", () => {
      const reader = new FileReader();
      reader.addEventListener("load", () => {
        imageThumbNail.src = reader.result;
      });
      reader.readAsDataURL(imageInput.files[0]);
    });

    이미지 미리보기도 fileReader()라는 객체를 사용하니 간편하게 구현할 수 있었습니다. fileReader()는 비동기적으로 파일을 읽어들일 때 사용하는 객체인데, 위 코드를 보면 imageInput.files[0]가 사용자가 업로드한 파일이고, readAsDataURL()을 통해 파일의 url을 읽어들입니다. 그렇게 reader객체가 load가 되면 html상의 img태그를 붙잡아온 imageThumbNail의 src속성에 해당 url을 넣어줌으로써 사용자가 업로드한 이미지를 화면에 띄울 수 있습니다. 이 url을 그대로 localStorage에도 넘겨주니 홈에서도 사용자가 직접 업로드한 이미지를 보여줄 수 있었습니다!

  • 소소한 디테일 🙂

    • 체크박스에서 꼭 label이나 체크박스 상자를 눌러야하는 것이 아니라 네모 박스 영역을 눌러도 체크/체크해제가 되도록!
      image
    • 전체 카테고리는 처음에 디폴트로 선택되어있도록 하기! 그냥 checked=true로 해놓으면 되는건줄 알았는데 의외로 그리 간단하진 않더라구요...
    • form input들에 required 속성을 넣어서, 빈 입력값이 넘어가지 않도록 validation했습니다!
    • 헤더에 로고 누르면 홈으로 가기!
    • img태그에 alt잊지 않기! 새로 추가하는 아이템들까지 alt태그가 붙어있답니다. (아이템 이름으로 넣어놨어요)

🥺 소요 시간, 어려웠던 점

  • 12h

  • javaScript 문법

    제가 javaScript에 아직 능숙하지 않은데, 세미나에서 배운 여러 문법을 활용하고 싶어서 삼항연산자도 써보고 && || 이런것도 써보려고 노력했습니다! 근데 어떤 때 어떨 걸 써야 좋을지 아직 잘 모르겠더라구요. 뭔가 사람들이 map을 많이 사용하는 것 같은데, 언제 forEach를 쓰고 언제 map을 써야할지 이런... 저는 단순 반복탐색이 필요할 때가 많아서 대부분 forEach를 쓴 것 같습니다. 그리고 반복이 중첩되는건 피하려고 했어요! 근데 이렇게 자스를 제대로(?) 써본 게 거의 처음이라 정말 재밌었어요!!! 그런한편 이게 맞나.. 싶은게 많은 것 같네요 최대한 깔끔한 코드를 짜려고 노력은 했는데 결국은 좀 덕지덕지가 된 것 같은 ㅎ_ㅠ

  • 상수파일 따로 분리해서 사용하기

    data를 상수로 저장할 때, data를 처리하는 부분이랑 같이 저장해둘 수도 있지만 data는 파일을 아예 따로 분리해두고 싶더라구요. 근데 그렇게 따로 분리해둔 파일을 어떻게 가져와서 사용하지?에 대해 좀 생각해야 했던 것 같아요. data를 저장한 파일에서 해당 상수를 export default를 통해 내보내기해주고, 이걸 사용할 파일에서는 import를 통해 불러올 수 있었는데, 자꾸 다음과 같은 오류가 나더라구요...

    Uncaught SyntaxError: Cannot use import statement outside a module
    

    구글링 결과, html파일에서 js파일을 연결하는 script 태그에 type=”module” 속성을 추가하여 해결할 수 있었습니다. 이렇게 해야 script 태그가 참조하는 파일을 모듈로 인식할 수 있다더라구요. 참고자료

  • HTMLCollection

    이번 과제, 배열을 잡아와서 탐색해야 하는 상황이 굉장히 많았는데, 어떨 땐 forEach같은 걸로 탐색을 해도 자꾸 undefined가 뜨더라구요! 그래서 초반에 헤맸는데, HTMLCollection과 Array의 차이 때문이었습니다.HTMLCollection은 getElementsByClassName과 getElementsByTagName 메소드를 통해 얻을 수 있는 객체이고, 이는 일반 배열이 아닌 ‘유사 배열’이며, 일반 배열과의 차이는 ‘살아있다’라는 점이라고 합니다. 정적으로 존재하는 것이 아니라, 실제 노드 객체의 상태 변화를 감지하고 반영하고 있기 때문이에요. 그래서 HTMLCollection을 배열로서 사용하기 위해서는 배열로 변환을 해줘야하는데, 보통 Array.from({HTMLCollection})을 많이 쓰는 것 같아요. 저는 spread 연산자도 활용했습니다. 참고자료

  • "전체" 카테고리 필터링

    처음에는 정말 단순히, "전체"카테고리를 선택했을 때는 다보여주고, 선택 해제했을 때는 다 삭제 하면 되는거 아닌가? 했는데요. "전체"카테고리를 선택 해제 하더라도 현재 체크되어있는 카테고리의 아이템들을 그대로 남겨놓아야하는 부분이 정말 어려웠습니다. 현재 선택되어있는 카테고리 목록을 저장하는 배열을 하나 따로 만들어야 하나? 현재 보여지고 있는 아이템 목록과 전체 아이템 목록의 교집합 배열을 구해서 해당하는 것들을 삭제해야하나??? 여러 시도를 해보다가, 현재 체크되어있는 카테고리를 확인하고 그 카테고리에 속하지 않는 것만 삭제하는 식으로 구현했습니다. 또 배열에서 특정 요소를 삭제할 때 splite() 메소드를 이용해서 삭제할 요소의 인덱스 값을 주어야 하다보니, 하나를 삭제하면 인덱스 값이 바뀌고 이래서 이 부분에서도 시행착오를 많이 겪었습니다. 그래서 배열에서 요소를 삭제할 때 splite()가 아니라 filter()를 사용했습니다!


🌈 구현 결과물

  • 카테고리 필터링

Untitled1.mp4
  • 해시태그 더보기

Untitled2.mp4
  • 새 상품 추가

Untitled3.mp4

@simeunseo simeunseo requested review from kwonET and borimong April 18, 2023 11:32
@simeunseo simeunseo self-assigned this Apr 18, 2023
@simeunseo simeunseo changed the title [1주차 기본/심화 과제] 웨비들의 냠냠 🍰 창업🏠 손님을 모셔오자!🌈 [2주차 기본/심화 과제] 웨비들의 냠냠 🍰 창업🏠 손님을 모셔오자!🌈 Apr 20, 2023
@simeunseo simeunseo requested a review from ljh0608 April 22, 2023 06:53
Copy link

@kwonET kwonET left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카드 / 카테고리 태그 를 구현하는 부분을 나와 다르게 구현한 점이 인상 깊었다!
유지보수 적인 면에서는 은서 코드가 좀 더 캡슐화되어 있는 면에서 더 깔끔하다는 생각이 들어.

또 주석도 꼼꼼하고 생각을 많이 한 느낌을 받아서 많이 배우고 가 .ㅎㅎ

Comment on lines +70 to +77
<label for="cateogry"><h4>카테고리</h4></label>
<select required name="category">
<option value="">=== 선택 ===</option>
<option value="veg">채소</option>
<option value="mush">버섯</option>
<option value="tofu">두부</option>
<option value="etc">기타</option>
</select>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시맨틱 태그 넘 좋다 !


const form = document.getElementById("add-card-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼하다!
나도 넣어줘야겠어 ㅎㅎ

Comment on lines +28 to +32
<ul>
<li>
<input class="main__nav__checkbox" type="checkbox" id="check-all" />
<label for="check-all">전체</label>
</li>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ul, li, input, label.. 정말 꼼꼼한 시맨틱 태그다!

Comment on lines +199 to +202
#cards {
display: grid;
grid-template-columns: repeat(5, 13rem);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 우연히 발견했는데

    grid-template-columns: repeat(auto-fill,minmax(최소너비,auto));

이런 식으로 사용하면 아래 미디어 쿼리로 따로 개수 일일이 조정 안해줘도
알아서 너비에 따라 repeat되게 해주더라고?
덕분에 나도 코드 몇줄 줄였어 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헐 이거 하드코딩하는거 진짜 맘에 안들었는데 완전 꿀팁!!

Comment on lines +105 to +108
#header__button:hover + #header__menu,
#header__menu:hover {
display: block;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중복되는 스타일링을
같이 써준 거 좋다 !!

Comment on lines +85 to +87
/*************
필터링된 데이터 기반으로 화면에 보여주기
**************/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 한 줄 한 줄마다 주석 처리해준 거 너무 좋다!
덕분에 곰방 곰방 이해함

Comment on lines +41 to +43
item.checked, //감지된 변화가 체크인가, 체크 해제인가
CATEGORY_NAME[item.id], //변화가 감지된 checkBox의 카테고리명
curItemList
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나는 이벤트 하면 'click'밖에 생각을 못했는데
input (type: checkbox)로 해두고 해당 변화에 대한 값으로 해체/클릭까지 구현해준 점이
인상깊고 꼼꼼하다!

(카테고리 nav단에서도 카테고리 선택/해제 기능을 해야하는 지 모른 1인)

Copy link

@borimong borimong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시험 기간라 바빴을텐데 심화과제까지 한 은서 언니 너무 멋져,,, 정말 최고다!!!!! 💯


//list를 탐색하면서 요소를 하나씩 card 노드로 만드는 함수
function listToCard(list) {
cardsSection.replaceChildren();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 replaceChildren 대신 innerHTML("")을 사용하면 어떨까??
replaceChildren를 사용하면 자식 노드를 모두 삭제한 뒤 다시 null 을 추가하게 되는데 innerHTML을 사용하면 기존의 것을 삭제하지 않고 바로 빈 문자열로 대체할 수 있어서 자원 낭비를 줄일 수 있을 것 같아!! :)

Copy link
Member Author

@simeunseo simeunseo May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오대박!! 그런방법이 있다니 같은 기능도 참 여러가지로 쓸수있구나... 고마워유

Comment on lines +96 to +100
//tag들 또한 리스트이므로 그 안에서 map을 돌린다.
let tags = ``;
item.tags.forEach((tag) => {
tags += `<small>` + tag + `</small>\n`;
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const tags = item.tags.map((tag) => <small>${tag}</small>).join("\n");
이 부분은 이렇게 바꾸어 보면 어떨까???? :)

Copy link
Member Author

@simeunseo simeunseo May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 대박이네 join 메소드 진짜 유용하다...

Comment on lines +102 to +111
let content = cardTemplate.cloneNode(true); //템플릿 복사
let newHtml = content.innerHTML; //템플릿 안의 html 복사
newHtml = newHtml //복사한 html에서 필요한 부분을 item 내용에 맞게 변경
.replace("{item_name}", item.name)
.replace("{tags}", tags)
.replace("{modal_tags}", tags)
.replace("{img_alt}", item.name)
.replace("{img_src}", item.img);

content.innerHTML = newHtml; //새롭게 바뀐 html을 템플릿에 적용

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분 넘넘 잘했는데 const content = cardTemplate.content.cloneNode(true); 이렇게 해서 node 중에 content 만 복사해서 content 에 넣고, replace 대신에 content.querySelector 를 사용해 요소를 선택하고, 요소 변경은 textContent 와 setAttribute 이용해 보는 건 어떨까??

  • 추가로 객체같은 경우에는 프로퍼티를 바꾸는 건 const 로 선언해도 수정 가능하기 때문에, let 대신에 const 많이 활용해주면 좋을 것 같아!! :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 그렇게하면 애초에 content만 복사할 수 있구나... replace로 텍스트자체를 갈아끼우는게 별로라고는 느껴졌는데 섬세히 대안 남겨줘서 넘 고마워...

});
});

//overlay 영역 클릭시 모달 close

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 overlay 영역 클릭 시 모달 닫히게 하는 것까지 구현하다니!!! 최고다 은서 언니!!! "v

Comment on lines +124 to +138
function makeCategoryTag(checkBox) {
if (checkBox.checked) {
let content = categoryTagTemplate.cloneNode(true); //템플릿 복사
let newHtml = content.innerHTML; //템플릿 안의 html 복사
newHtml = newHtml
.replace("{category_name}", CATEGORY_NAME[checkBox.id])
.replace("{checkbox_id}", checkBox.id)
.replace("{category-tag_id}", "tag__" + checkBox.id);
content.innerHTML = newHtml;
categoryTagSection.appendChild(content.content);
} else {
const target = document.getElementById("tag__" + checkBox.id);
target.remove();
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기두 위에서 얘기한 방식으루 바꿔봐도 좋겠다!!! :)

@simeunseo simeunseo merged commit 92a3a5c into main-old May 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants