Work In Progress - 위스타벅스 ☕️ 페이지 클론 기록하기 (2)

Wonkook Lee·2021년 8월 28일
18

Work in Progress

목록 보기
2/2
post-thumbnail

Work In Progress Aug 26



# 개요 📋

과제명 : 위스타벅스 (Westabucks)
제작 : WONKOOK LEE
분류 : 학습용 과제
제작 기간 : 2021.08.23 - 08.26
목적 : 시맨틱 HTML 학습, 유저 입력 유효성 검증, UI 조작, 기능 구현
사용 툴 : HTML, CSS, JavaScript

데모 :
위스타벅스: 로그인 페이지
위스타벅스: 음료 리스트 페이지
위스타벅스: 음료 상세 페이지


# 넘나 바쁜 당신을 위한 단락 바로가기 🚗

# checkbox로 토글 UI 만들기 ✅
# 댓글 창 흉내 내기 💬
# 반응형 레이아웃 📏
# 리다이렉트 🔗
# 리팩토링 🛠




# Overview 👀

인스타그램, 스타벅스의 일부 페이지를 모방하며 실제 페이지의 구성과 기능을 따라하고 웹 페이지 제작에 대한 이해를 높이는 학습 과정으로서 위스타벅스 제작이 진행됐다.

  • 인스타그램의 로그인 페이지에선 사용자의 입력값 Validation과 기대한 값이 들어왔을 때 Valid UI를 표현하는 것을 목표로 만들었다.
  • 스타벅스의 음료 상세 페이지의 기존 구성과 기능(레이아웃 + 줌 인터랙션)에 더하여 '좋아요' toggle UI와 댓글 창(UI Mockup)을 추가 구현하였다. 줌 인터랙션은 이 포스트를 참고
  • 스타벅스의 음료 목록 페이지는 제품 리스트의 그리드 구성 및 media queryflex 프로퍼티를 사용한 반응형 웹을 공부하기 위한 목표로 만들었다.

각 페이지에서 공부한 내용을 기능, 맥락 별로 나열하여 설명한다.




# checkbox로 토글 UI 만들기 ✅

input 타입 중 checkbox로도 토글 UI를 만들 수 있다. 예를 들어 좋아요 하트가 꺼졌다 켜지는 등 간단한 UI는 자바스크립트 없이 CSS 만으로 충분히 구현이 가능하다. 클릭 이벤트 리스너를 모든 요소에 일일히 위임하지 않아도 인터랙션이 작동되기 때문에 checkbox 트릭은 자주 사용되는 방법이다.

<input type="checkbox" name="like_bev" id="like_bev">
<i class="far fa-heart"></i>
<i class="fas fa-heart"></i>

## CSS는 checkbox의 체크 여부를 알 수 있다 🔎

checkbox는 체크가 된 상태와 체크가 되지 않은 상태, 두 가지로 나뉜다. 이는 값으로도 나타낼 수 있으며 CSS에서 가상 선택자로서 체크된 상태를 아래와 같이 참조할 수 있다.

#like_bev:checked { ...styles }

이로써 체크가 표시된 checkbox에 한하여 특정 스타일을 적용할 수 있다. 나는 checkbox는 사용자가 클릭했는지 여부만 전달하고 스타일 변경은 형제 요소에게 위임하는 방법을 사용했다.

  #like_bev_container #like_bev:checked ~ .far {
    opacity: 0;
  }

  #like_bev_container #like_bev:checked + i {
    opacity: 1;
  }

## 두근두근 깨알 같은 마이크로 인터랙션 ❤️

이전 포스트에서도 언급했듯 나는 깨알같은 디테일을 사랑하기 때문에 좋아요가 눌러지는 순간 두근두근 하는 애니메이션이 작동되도록 키프레임을 설정해놓았다.

  #like_bev_container #like_bev:checked ~ .fas {
    color: crimson;
    opacity: 1;
    animation-name: heartbeat;
    animation-duration: 1s;
    animation-timing-function: ease;
    animation-fill-mode: forwards;
  }
  
    @keyframes heartbeat {
    0% {transform: scale(1);opacity: 0;}
    25% {transform: scale(1.3);}
    50% {transform: scale(1);opacity: 1;}
    75% {transform: scale(1.3);}
    100% {transform: scale(1);}
  }

## 요소가 겹칠 땐 z-index를 활용하자 🏗

Font Awesome에서 가져온 i 태그 두 개를 겹쳐놓고, checkbox가 체크되지 않았을 땐 속이 빈 하트가 표시되며, 체크되었을 땐 속이 찬 하트가 애니메이션과 함께 보이도록 display 또는 opacity를 사용해주면 된다.

여기서 주의할 점은 여러 요소를 겹쳐놓을 경우 input 태그는 z-index 값을 상대적으로 높여 클릭을 인식할 수 있도록 만들어주어야 하는 점이다.




# 댓글 창 흉내 내기 💬

원본 사이트에는 없는 댓글 창 UI를 만들게 되었다. 과제는 댓글 추가와 삭제였지만 최소 열자 이상 작성해야 댓글을 달 수 있는 알림 UI도 만들어봤다. DOM으로 HTML 요소만 추가되고 삭제되는 UI 목업이며 CRUD가 아니기 때문에 흉내냈다고 표현했다. 다른 유저 댓글을 지워버리는 것 자체가..


## 댓글 추가 ⌨️

변수 html에 HTML 태그 뭉치를 넣고, 아이디와 댓글 내용만 바꾸어 insertAdjacentHTML로 댓글 리스트에 새로운 li 요소로써 추가하는 방법을 사용했다. 나는 document.createElement()로 DOM 요소를 만들어 prepend 또는 append하는 방법보다 태그를 직접 삽입하는 편이 간편해서 더 선호한다.

본래 의도는 새로 생성되는 댓글 스레드의 배경색을 번갈아가며 생성되도록 만들고자 하였지만, nth-child(odd)로 CSS 스타일 처리만 했다. 댓글이 많아져서 레이아웃이 깨지지 않도록 overflow-y: scroll를 사용하여 창을 스크롤로 만들었다.

userComment.length < 10을 넣어 댓글의 글자수가 10글자가 이하라면 Guard Clause로 예외 처리 되도록 만들었다.
invalidAlert()는 유저에게 왜 댓글이 작성되지 않는지 알려주는 UI 조작 함수다.

const reviewInputField = document.getElementById('review_field');
  
const addComment = (event, userName='oneook', userComment) => {
  event.preventDefault();
  if (userComment.length < 10) {
    invalidAlert();
    return;
  };

  let html = `<li class="review_thread"><span class="id">${userName}</span><span class="comment">${userComment}</span><div id="closeBtn">X</div></li>`;
  document.getElementById('RvTarget').insertAdjacentHTML('afterbegin', html);

  reviewInputField.value = '';
}

reviewInputField.addEventListener('keypress', (event) => {
  event.key === 'Enter' && addComment(event, undefined, event.target.value);
})

## 댓글 최소 글자수 제한 🚫

댓글 글자수를 충족하지 못하면 알림 UI가 발생된다. 시간이 흐르면 천천히 사라지는 모습을 연출하기 위해 애니메이션을 사용했고, 애니메이션이 끝난 후 프로퍼티를 기본값으로 되돌리기 위해 setTimeout을 사용했다.

const validTag = document.getElementById('validTag');

const invalidAlert = () => {
  validTag.style.animationName = 'notValid';
  setTimeout(() => { validTag.style.animationName = '' }, 1500);
}

조작이 이루어지는 영역이 아니기 때문에 간단히 투명도만 조절되는 애니메이션을 넣었다.

@keyframes notValid {
  0% { opacity: 0; }
  10% { opacity: 1; }
  60% { opacity: 1; }
  100% { opacity: 0; }
}

## 댓글 삭제 🗑

댓글 삭제 버튼은 삭제할 댓글 스레드에 커서를 올려(Hovering)야 비로소 보이게 만들어봤다. 댓글을 삭제할 수 있다는 것은 확실히 알려주고 싶은데 노멀 상태의 UI가 복잡해지는건 원치 않았다. 삭제 버튼과 댓글 사이에 거리가 있기 때문에 Hover시 스레드의 색상을 다르게 바꾸어 어떤 스레드를 지우는지 명확하게 알려주고자 하였다.

// Delete Comment

const deleteComment = (function() {
  const reviewField = document.getElementById('RvTarget');

  reviewField.addEventListener('click', event => {
    if (event.target.id !== 'deleteBtn') return;
    event.target.closest('.review_thread').remove();
  })

})();

리뷰 스레드를 포괄하는 ulRvTarget에 하나의 이벤트 핸들러를 걸고 삭제 버튼에 이벤트를 위임했다. 버튼을 클릭하면 버튼이 포함된 부모 요소 중 li 요소인 review_thread를 지우도록 만들었다. closest()는 자신을 포함한 부모, 조상 요소 중 해당 선택자(querySelector처럼 작동한다)를 가진 첫번째 요소(Upward)를 선택한다. DOM Traversing 메소드 중 가장 유용하게 사용되는 메소드다.




# 반응형 레이아웃 📏

스타벅스 홈페이지는 반응형으로 설계되어있다. 데스크탑, 태블릿, 스마트폰 순으로 break point가 존재하며, 960px보다 뷰포트가 작아지면 GNB는 숨겨지고 태블릿, 스마트폰용 UI(햄버거 메뉴 등)로 전환된다. 음료 목록 그리드는 flex를 사용하여 길이가 줄어들면 wrap 되어 다음 행으로 컴포넌츠를 보내도록 만들었다.

## 미디어 쿼리 @

현재까지 구현한 사이즈 별 쿼리는 다음과 같다. 중간 중간 로고가 찝히거나 구성이 애매한 경우를 대비해서 겹치는 구간도 스타일을 넣어주었다.

@media (min-width: 961px) { ... }
@media screen and (min-width: 961px) and (max-width: 1099px) { ... }
@media screen and (min-width: 481px) and (max-width: 960px) { ... }
@media screen and (min-width: 641px) and (max-width: 960px) { ... }
@media screen and (min-width: 481px) and (max-width: 660px) { ... }




# 리다이렉트 🔗

anchor 태그를 넣기 애매했던 form 속의 button는 JS에서 페이지 이동 메소드를 사용했다. location.hreflocation.replace 두 가지 메소드가 있는데, 차이점은 아래와 같다.


## relocation.replace와 relocation.href의 차이


location.hreflocation.replace
기능새로운 페이지로 이동된다.기존 페이지를 새로운 페이지로 변경시킨다.
형태프로퍼티메소드
주소 히스토리기록된다기록되지 않는다.
사용 예location.href='abc.php'location.replace('abc.php')

href는 페이지를 이동하는 것이기 때문에 이전 페이지로 다시 되돌아갈 수 있지만 (히스토리가 있음) replace는 새로운 페이지로 덮어 씌우기 때문에 이전 페이지로 이동이 불가하다.

하지만 cache가 남지 않고 페이지 요소가 refresh되어야 할 필요성이 있다면 replace가 좋은 방법이 될 것 같다.

출처


## 그럼에도 <a> 태그를 사용하는 이유

  1. 자바스크립트가 다운되더라도 링크가 작동되기 때문에
  2. 스파이더나 구글 봇들이 자바스크립트를 통해 링크로 침투하는 것을 막기 위해
  3. window.location.href 사용은 비표준 링크로 페이지 간의 연결을 숨기는 것으로써 World Wide Web의 약속에 위배된다.
  4. UX에도 좋지 않다. 앵커 태그로 작성된 링크는 마우스를 가져가면 status bar에 링크를 표시하거나, 링크를 복사하고, 새로운 탭에서 여는 등 다양한 인터랙션이 가능하다. 반면 window.location는 그런 UX적인 면에서 기존 앵커 태그보다 뒤쳐진다.
  5. 훨씬 쉽기 때문에 좋다.



# 리팩토링 🛠

왜 커밋, 푸시하고 나서야 다른 문제점이 눈에 보이는 걸까? 무한 푸시
코드 리뷰에서 지적받은 점과 스스로 고친 것들을 적는다.


## CSS 리팩토링


### CSS 속성 작성 순서는 레이아웃에 영향을 많이 주는 순서대로, 인접 속성끼리 묶어서 작성할 것

같은 선택자 내 CSS 속성 작성 순서 (권장)

  1. Layout Properties (position, float, clear, display)
  2. Box Model Properties (width, height, margin, padding)
  3. Visual Properties (color, background, border, box-shadow)
  4. Typography Properties (font-size, font-family, text-align, text-transform)
  5. Misc Properties (cursor, overflow, z-index)

매번 CSS 작성할 때마다 display, position 관련 속성을 제외하고선 생각나는대로 적어서 항상 다시 찾을때 헤매곤 했는데, 명확한 기준을 배워서 많은 도움이 되었다.



## JS 리팩토링


### 억지로 삼항 연산자(Ternary Operator)를 사용하려고 하지말 것

예를 들어 아래와 같은 상황에서, includes 메서드와 e.target.value.length >= 8이란 표현식은 어차피 boolean을 반환하는데 삼항 연산자로 반환 값을 true or false로 지정할 필요가 없다.

짧은 코드가 멋있어보이고, 뭔가 초보적으로 보이는 if..else는 안쓰려고 했던 내 마음을 들켜버린것 같았다.

// 주석 처리된 것이 수정 전

form.addEventListener('input', e => {
  if (e.target.type === 'text') {
    // isValid.id = e.target.value.indexOf('@') !== -1 ? true : false;
    isValid.id = e.target.value.includes('@');
  }
  if (e.target.type === 'password') {
    // isValid.pw = e.target.value.length >= 8 ? true : false;
    isValid.pw = e.target.value.length >= 8;
  }
  validUI(isValid);
});

### 짧은 네이밍에 집착하지 말고 정확한 의미를 전달할 수 있도록 만들 것

줌 인터랙션에 사용된 객체명이 수 많은 조건문에서 사용되어서 boundarybor로 줄였는데, 다른 사람이 보기에 이게 뭐하는 객체인지 쉽게 알 수 있는 객체명이 좋다고 권고되었다. 따라서 bor를 boundary로 다시 고쳤다. 다시 보니 굳이 줄일 필요 없었는데 짧고 간결한 네이밍에 집착했었던 것 같다.

  // 수정 전
const bor = { xMin: 153, xMax: 297, yMin: 117, yMax: 353 };
  
  // 수정 후
const boundary = { xMin: 153, xMax: 297, yMin: 117, yMax: 353 };



내용이 너무 많아져서 글은 여기서 끊었다.

데모 :
위스타벅스: 로그인 페이지
위스타벅스: 음료 리스트 페이지
위스타벅스: 음료 상세 페이지



ⓒ Wonkook Lee
참고한 해당 홈페이지의 리소스와 음료 사진의 모든 저작권은
스타벅스 코리아에게 있으며 문제가 생길 시 즉시 조치하겠습니다.

🙏🏻 잘못된 정보가 있다면 지적해주세요

profile
© 가치 지향 프론트엔드 개발자

4개의 댓글

comment-user-thumbnail
2021년 8월 31일

와 볼때마다 대단하십니다..ㄷㄷ

1개의 답글
comment-user-thumbnail
2021년 8월 31일

호오...오늘도 잘 보고 갑니다

1개의 답글