개발 공부/자바스크립트

[엘리의 드림코딩 JS] 직접 쇼핑몰 사이트 HTML, CSS, JS로 만들기

5묘 2022. 12. 7. 13:31

https://youtu.be/We2Kv1HMGvc

이제까지 배운 JavaScript, 그리고 HTML과 CSS를 활용해 동적으로 움직이는 온라인 쇼핑몰을 만드는 실습! 뒤에 있을 해설 강의를 아예 안보고 한 5시간 정도 걸려서 혼자 만들었다. 프로젝트 하는 동안 react와 jsx만 써오다가, Vanilla JS로 DOM을 조작하는 방식을 하게 되니 querySelector, createElement 등 예전에 배운 메서드를 다시 복습하게 됐다. 그리고 CSS media query를 배운 후 자주 안쓰다 보니 많이 까먹어서 다시 찾아봤다(부끄럽군..복습 열심히 해야지😥)
 

1. 설계(개발 순서)

디자인을 개발할 컴포넌트대로 나눈다.

항상 플젝 할 때 본격적인 코드 구현 전에는, 저렇게 디자인을 <div>태그로 묶어서 개발할 부분을 나눈다.
그래야 나중에 CSS로 flex를 적용할 때 편하더라.
그리고 개발 순서는 1) html, css 이용해 flex로 정렬까지 된 정적 웹사이트 구현 -> 2) JavaScript에서 DOM 조작해 동적으로 움직이도록 만들기 -> 3) media query 추가해 반응형 웹으로 만들기 였다. 각각의 작업에 시간 배분도 했는데 하다보니 좀 시간이 많이 초과됐다.

2. 완성된 모습

데스크탑 버전

버튼을 누르면, 버튼에 해당하는 조건에 맞게 필터링된 리스트가 보이는 간단한 구조의 웹 페이지다.

모바일에선 이렇게 보인다

3. 소스코드(HTML, CSS, JavaScript)

1) HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>mini game</title>
  <style>
  /*CSS는 밑에*/
  </style>
  <script defer src="main.js"></script>
</head>
<body>
  <div id="board">
    <img id="home" src="imgs/imgs/logo.png">
    <div class="menu">
      <img id="t-button" src="imgs/imgs/blue_t.png">
      <img id="p-button" src="imgs/imgs/blue_p.png">
      <img id="s-button" src="imgs/imgs/blue_s.png">

      <button id="blue-box"> Blue</button>
      <button id="yellow-box">Yellow</button>
      <button id="pink-box">Pink</button>
    </div>
    <div class="product-list">
      <!-- <div class="product">
        <img src="imgs/imgs/blue_s.png">
        <p>female, small size</p>
      </div> -->
    </div>
  </div>
</body>
</html>

2) CSS

    @media (max-width: 450px) {
      #home {width: 100px}
      .menu {width: 250px;}
      .menu > button {
        width: fit-content;
        font-size: 10px;
      }
      .product {width: 300px; }
    }

    @media (min-width: 450px) {
      .product {width: 500px; }
    }

    html,
    body {
      width: 100%;
      height: 100%;
      background-color: #3A3D44;
    }

    #board {
      display:flex;
      flex-direction: column;
      justify-content:flex-start;
      align-items: center;
      margin-top: 3rem;
      width: 100%;
      height: 100%;
    }

    #board > div {
      margin-top: 2.5rem;
    }

    #home {
      cursor: pointer;
    }

    .menu {
      display:flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
    }

    .menu > img {
      margin-left: 1rem;
      width: 10%;
      cursor: pointer;
    }
    .menu > button {
      margin-top: 5px;
      text-align: center;
      font-weight: 400;
      width: fit-content;
      font-size: 19px;
      margin-left: 1rem;
      cursor: pointer;
    }
    
    #blue-box{
      background-color: #3190D7;
    }
    #yellow-box{
      background-color: #F8CA2E;
    }
    #pink-box{
      background-color: #F82E5C;
    }

    .product-list {
      display: flex;
      flex-direction: column;
    }
    .product {
      display: flex;
      align-items: center;
      border: 1px solid black;
      border-radius: 10px;
      margin-top: 1rem;
      background-color: white;
    }

    .product > img {
      margin-left: 1rem;
      width: 10%;
    }

    .product > p {
      margin-left: 1rem;
      font-size: 19px;
      font-weight: 400;
    }

3) JavaScript

'use strict';

// 1. 제품 객체를 만든다.(each product)
// 2. 객체가 저장된 배열을 만든다.(all products)
// 3. 배열이 loop를 돌며 각 객체가 div의 html innerText에 적히게 한다.

// 1. 객체는 클래스, 생성자 이용해 만들어주자.
class Product {
  constructor (img, sex, size, color) {
    this.img = img;
    this.sex = sex;
    this.size = size;
    this.color = color;
  }
}

// raw data(객체들)
// blue
const bluePant = new Product(
  "imgs/imgs/blue_p.png",
  'man',
  'small size',
  'blue',
);
const blueSkirt = new Product(
  "imgs/imgs/blue_s.png",
  'female',
  'small size',
  'blue',
);
const blueTshirt = new Product(
  "imgs/imgs/blue_t.png",
  'male',
  'large size',
  'blue',
);

// Yellow
const yellowPant = new Product(
  "imgs/imgs/yellow_p.png",
  'man',
  'large size',
  'yellow',
);
const yellowSkirt = new Product(
  "imgs/imgs/yellow_s.png",
  'man',
  'large size',
  'yellow',
);
const yellowTshirt = new Product(
  "imgs/imgs/yellow_t.png",
  'man',
  'small size',
  'yellow',
);

// Pink
const pinkPant = new Product(
  "imgs/imgs/pink_p.png",
  'female',
  'small size',  
  'pink',
);
const pinkSkirt = new Product(
  "imgs/imgs/pink_s.png",
  'man',
  'small size',
  'pink',
);
const pinkTshirt = new Product(
  "imgs/imgs/pink_t.png",
  'female',
  'large size',
  'pink',
);


// 2. 객체가 저장된 배열을 만든다.(all products)
const allProducts = [
  bluePant, blueSkirt, blueTshirt, 
  yellowPant, yellowSkirt, yellowTshirt, 
  pinkPant, pinkSkirt, pinkTshirt
]

showProductList(allProducts);

function showProductList (products) {
  const productList = document.querySelector('.product-list');
  productList.innerHTML = '';

  // 3. 배열이 loop를 돌며 각 객체가 div를 createElement하고
  // create한 div의 html innerText에 적히게 한다. 
  products.forEach((product) => {
    let productDiv = document.createElement('div');
    productDiv.setAttribute('class', 'product');
    productList.append(productDiv)
    
    let productImg = document.createElement('img');
    productImg.setAttribute('src', product.img);
    let productDescription = document.createElement('p');
    productDescription.innerText = `${product.sex}, ${product.size}`;
    productDiv.append(productImg, productDescription);
  
    // 86 이렇게도 만들었는데 어떤 게 더 좋은지 모르겠음.
    // let descriptionText = document.createTextNode(`${product.sex}, ${product.size}`);
    // productDescription.append(descriptionText);
  });
}

// 4. 홈버튼이 눌리면 전체를, 각 버튼 눌릴 때마다 해당하는 상품만 보여주기.
// 리스트 그리는 걸 function으로 해서, switch-case로 각 상황에서 들어가는 인자 상품 리스트 다르게 하자.
// .preventDefault() 잊지 말기
const homeButton = document.querySelector('#home');
const tButton = document.querySelector('#t-button');
const sButton = document.querySelector('#s-button');
const pButton = document.querySelector('#p-button');
const blueButton = document.querySelector('#blue-box');
const pinkButton = document.querySelector('#pink-box');
const yellowButton = document.querySelector('#yellow-box');

const buttons = [homeButton,tButton, sButton, pButton, blueButton, pinkButton, yellowButton];
buttons.forEach(button => button.addEventListener('click', function(event) {
  event.preventDefault();
  switch (event.target.id){
    case 'home' :
      showProductList(allProducts)
      break;

    case 'pink-box':
      showProductList(allProducts.filter((product) => product.color === 'pink'));
      break;

    case 'blue-box':
      showProductList(allProducts.filter((product) => product.color === 'blue'));
      break;

    case 'yellow-box':
      showProductList(allProducts.filter((product) => product.color === 'yellow'));
      break;

    case 's-button':
      showProductList(allProducts.filter((product) => product.sex === 'female'));
      break;

    case 't-button':
      showProductList(allProducts.filter((product) => product.sex === 'man'));
      break;

    case 'p-button':
      showProductList(allProducts.filter((product) => product.sex === 'male'));
      break;
    }
}));

4. 개발 중 일어났던 문제(혹은 궁금증) 들과 해결

1) 별건 아니지만, chrome에서 console.log를 찍으면 <html> 태그 형태로 다시보기가 됐다가, F5를 눌러 page refresh를 하면 object 형태가 되는 이상한 광경을 목격했다🤔 '이거 대체 왜 이러는거지..?' 싶었다.기능 구현과 1도 상관 없이 HTML 태그가 querySelector로 잘 선택돼서 들어오는지만 확인할 목적이었지만, 그냥 '그런가 봐~' 하고 넘어가면 안될 것 같다는 생각이 들어 stackoverflow에 질문했다https://stackoverflow.com/questions/74712108/why-console-shows-different-things-when-i-use-queryselector

 

Why console shows different things when I use querySelector()?

when I use document.querySelector(), At first, I can see html tag in console like this. // this is JS code allProducts.forEach((product) => { **const productList = document.querySelector('.product-

stackoverflow.com

올린 내용.

요약하면 '왜 console.log() 찍고 F5 누르기 전과 후 모습이 다른걸까?? 누가 나한테 좀 알려주라'란 내용이었다. 그랬더니 한 친절한 개발자가 답을 해주었다.

console.dir을 쓰면 항상 object만 보인다.

크로니엄 DevTools의 콘솔 상에서 console.log()에 querySelector로 선택한 HTML 태그를 집어넣었을 때, console.log()는 HTML을 찍고 console.dir은 object 형태로 반환한다. 그럼 내 console 창에는 HTML 태그로만 떠야 하는데 F5를 누르면 왜 오브젝트가 보이는 것인가..? 아쉽지만 Teemu씨도 그건 모르겠다고, 자긴 Firefox로 testing 하는 걸 선호한다고 하신다. 아마 크롬 브라우저 상의 버그인 듯 하다. (내가 오류의 원인을 모르니까 버그라고 우기기😂)

2) 처음에 분명 최상단 div에 해당하는 board에 100% height를 줘도 사이트에 배경색이 절반으로 짤려 있어서 왜 이런가 싶었다.🤔 이건 css에서 html, 그리고 body 태그 자체에도 width: 100%;로 해줘야 해결되는 문제임을 알았다. 
https://codingbroker.tistory.com/56

 

[HTML, CSS] div 요소를 전체화면으로 설정하기, 끝부분 적용안되는 문제, css reset

HTML에서 최상위 div를 화면 전체로 설정하는 방법입니다. 1. % 사용 width와 height를 100%로 설정하면 전체화면이 될 것만 같습니다만 그렇지 않습니다. %는 부모 요소 길이의 몇%를 차지할 것인지 나

codingbroker.tistory.com

 

3) document.getElementbyId()만 기억하고 있어서 클래스 이름으로 DOM 조작 가능한 메서드를 찾아봤더니 document.getElementsbyClassName() 메서드가 있었다(심지어 예전에 배운 거임🤣). 하지만 결론적으로 둘 다 안쓰고 document.querySelector()로 클래스 이름은 .querySelector('.클래스명'), 아이디는 .querySelector('#아이디')하나의 메서드에 다양한 변수를 쓰는 것이 조금 더 편했다.
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName

 

Document.getElementsByClassName() - Web APIs | MDN

The getElementsByClassName method of Document interface returns an array-like object of all child elements which have all of the given class name(s).

developer.mozilla.org

4) switch ... case 문에서 switch 에 특정 case에 해당하는 조건을 넣었다고 해서 그 case만 실행되는 것 아니다. 그 break 안 걸면 case 이후의 모든 case가 쭉~ 실행된다. 쉽게 말하자면 조건에 맞는 case로 '점프'해서 실행되는 것이라고 볼 수 있다. (조건문과 이런 면에서 차이가 있다.)
https://www.codeit.kr/community/threads/12429

 

break를 안하면 다음 case의 값들이 모두 출력되는게 잘 이해가 안됩니다 | 코드잇

var inputNumber = window.prompt("한 자리 숫자를 입력하세요") switch (inputNumber) { case '0': alert('zero'); case '1': alert('one'); case '2': alert('two'); default: alert('none'); } 이런 코드가 있을 때 설명하신 것처럼 1을 입력

www.codeit.kr

파이썬 배울 때도, 정처기 할 때도 switch ...case와 break에 대한 설명을 분명 여러 번 들었지만, 역시 직접 실수해서 오류를 맞닥뜨리니 머리에 더 잘 남는다. switch ... case는 조건에 맞는 것만 실행되는 조건문과 다르다! 그 case로 점프하는 것이다! 멈추려면 break 꼭 필요하다!

5. 엘리님의 해설 & 코드리뷰 강의(무료임!)

https://academy.dream-coding.com/courses/player/mini-shopping/lessons/183

 

드림코딩 아카데미 | Dream Coding Academy

 

academy.dream-coding.com

1) HTML 작성 시 emmet을 더 많이 사용하자!
2) obj.key로 하면 undefined 가 뜨는데, obj[key]로 불러오면 정상적으로 value가 들어있는 현상이 나타났다.

function onButtonClick(event, items) {
  const dataset = event.target.dataset;
  const key = dataset.key;
  const value = dataset.value;

  if (key == null || value == null) {
    return;
  }
  console.log(items[0].key); //undefined
  console.log(items[0][key]); //pants(정상)
  
  // displayItems(items.filter((item) => item[key] === value));

왜인지 궁금해 찾아봤더니 이런 자료를 봤다.(https://medium.com/sjk5766/javascript-object-key-vs-object-key-%EC%B0%A8%EC%9D%B4-3c21eb49b763

 

JavaScript Object[‘key’] vs Object.key 차이

JavaScript 객체의 property를 접근 하는 방법에는 []와 . 을 사용하는 방법이 있습니다. 가령 아래와 같이 a라는 객체가 있다면 속성에 접근하는 방법이 두 가지가 있는거죠

medium.com

내가 참조한 부분

[]를 사용하면, 변수 안에 들어간 문자열을 key값으로 쓸 수 있다. 즉, 해설 강의의 index.html에서 key 라는 변수에 'type'을 넣었기 때문에, item[key]를 하면 key 변수 안의 'type'이 들어가서 item['type'] 이라고 정상적으로 string 타입의 키가 들어가 검색이 가능하다. 그러나 item.key의 경우 item이라는 객체의 속성 key를 찾게 되는데, json에 설정된 속성에는 'key'란 문자열은 없다.(ex: 'type', 'color', 'size'...) 그래서 찾을 수 없다는 undefined가 뜨게 되는 것이다.
3) 이벤트 위임에 대해 추가적으로 공부해볼 것! window처럼 광범위한 범위에 이벤트 리스너 추가하지 말고 특정 영역에 등록하기!
4) 주석을 잘 정리해두자.(/**/ 나 // 써서 깔끔하게!)
5) switch...case문은 이렇게 refactoring 할 수 있다

buttons.forEach((button) =>
  button.addEventListener("click", function (event) {
    event.preventDefault();
    switch (param) {
      case "pink-box":
      case "blue-box":
      case "yellow-box":
        showProductList(
          allProducts.filter((product) => product.color === param)
        );
        break;

      case "s-button":
      case "t-button":
      case "p-button":
        showProductList(
          allProducts.filter((product) => product.type === param)
        );
        break;
     default:
     	throw Error('unknown type') 
        //default는 마지막 남은 케이스가 아니라, 
        //여기에 해당하지 않는 케이스 들어올 때 이에 대한 경고를 날려주는 용도로 써라!
    }
  })
);

6) 함수명은 누구나 쉽게 기능을 유추할 수 있게, 함수는 1가지 기능만 수행하도록!
가령 내 코드에서 아래와 같이 하나의 함수에 여러 기능이 있는 건 별로 안 좋은 사례이다.😥

...
function showProductList(products) {
  const productList = document.querySelector(".product-list");
  productList.innerHTML = "";

  // 3. 배열이 loop를 돌며 각 객체가 div를 createElement하고
  // create한 div의 html innerText에 적히게 한다.
  products.forEach((product) => {
    let productDiv = document.createElement("div");
    productDiv.setAttribute("class", "product");
    productList.append(productDiv);

    let productImg = document.createElement("img");
    productImg.setAttribute("src", product.img);
    let productDescription = document.createElement("p");
    productDescription.innerText = `${product.sex}, ${product.size}`;
    productDiv.append(productImg, productDescription);
  });
}
...

7) 함수 선언부 내 코드를 줄이는 것이 중요하니, 당연히 인자를 넣을 때도 이를 생각하자.
8) <button> html 태그 자체에 onclick 속성 넣는 경우 있다(react 습관으로). 하지만 Vanilla JS로 코딩한다면 HTML에는 웹사이트 골격을 담당하는 UI적 요소만 남겨두고, business logic은 javascript에서만 보이도록 하는 게 좋다. 
9) Readme.md에 들어갈 내용: 프로젝트명, 스택(기술)과 왜 썼는지, 기간은 얼마나 걸렸는지, 주요 기능(스크린샷 gif나 시연영상 있음 좋다), 아키텍쳐(다이어그램 등) 를 포함한다.
10) DRY 원칙 잊지 말자. Don't Repeat Your self. 나는 이번에 querySelector 하는 방법이나, addEventListener에서 반복 너무 많았으니까 이런 건 이벤트 위임과 함수로 최대한 중복 줄이자.
11) Early Exception : 조건에 맞지 않으면 return 한다는 내용을 함수 선언부 위쪽에 두자.
12) Camel Case랑 snake case 혼재되지 않고, 함수.변수명은 Camel Case, CSS에서 클래스명에 snake case 쓰자.

6. 후기 + 이후 개선할 점

뿌듯한 점😊:
1) JS에서 클래스를 쓰는 방법을 익혔고, 클래스를 이용해 속성을 가진 객체를 생성해보기까지 했다!
2) 버튼 누를 때마다 HTML elemet를 다시 만든다던가 하는 반복적 연산은 showProductList()라는 이름의 function으로 만들어서 코드 수를 줄이려고 했다. 아직 Loc(Line of code)가 많아서 리팩토링 더 해야하긴 하지만..😅
3) jsx만 쓰느라 잊고 있었는데, Vanilla JS로 DOM 조작하는 코드를 짜면서 다시 한번 복습할 수 있었다.
4) 사소한 부분이더라도, 모르는 부분을 그냥 넘어가지 않고 stackoverflow에 질문하는 모습!

개선할 점🧐:
1) 모바일은 아이폰 12 사이즈에 딱 좋아보이게 맞춰서 만든 것이지만, font나 이미지 size에 px같은 절대값을 쓴 탓에 아직도 부드러운 반응형이 아니라 뚝뚝 끊기는 형태의 반응형을 만든 것 같다. bootstrap의 grid system을 이용하거나, media query를 좀 더 size를 세분화해서 더 부드럽게 만들어보자! 
2) 아직 코드 라인 수가 많다.. 리팩토링!
3) querySelector() 의 결과값인 Element가 대체 html인지 object인지 헷갈린다. 이 부분은 다시 MDN 보며 오늘 내로 해결해서 이해하자! => 
4) 내가 만든 이 긴 코드 토요일에 refactoring 하자! =>