과제명 : 위스타벅스 (Westabucks)
제작 :WONKOOK LEE
분류 : 학습용 과제
제작 기간 : 2021.08.23 - 08.26
목적 : 시맨틱 HTML 학습, 유저 입력 유효성 검증, UI 조작, 기능 구현
사용 툴 :HTML
,CSS
,JavaScript
데모 :
위스타벅스: 로그인 페이지
위스타벅스: 음료 리스트 페이지
위스타벅스: 음료 상세 페이지
# checkbox로 토글 UI 만들기 ✅
# 댓글 창 흉내 내기 💬
# 반응형 레이아웃 📏
# 리다이렉트 🔗
# 리팩토링 🛠
인스타그램, 스타벅스의 일부 페이지를 모방하며 실제 페이지의 구성과 기능을 따라하고 웹 페이지 제작에 대한 이해를 높이는 학습 과정으로서 위스타벅스 제작이 진행됐다.
- 인스타그램의 로그인 페이지에선 사용자의 입력값 Validation과 기대한 값이 들어왔을 때 Valid UI를 표현하는 것을 목표로 만들었다.
- 스타벅스의 음료 상세 페이지의 기존 구성과 기능(레이아웃 + 줌 인터랙션)에 더하여 '좋아요' toggle UI와 댓글 창(UI Mockup)을 추가 구현하였다. 줌 인터랙션은 이 포스트를 참고
- 스타벅스의 음료 목록 페이지는 제품 리스트의 그리드 구성 및
media query
와flex
프로퍼티를 사용한 반응형 웹을 공부하기 위한 목표로 만들었다.
각 페이지에서 공부한 내용을 기능, 맥락 별로 나열하여 설명한다.
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>
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);}
}
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();
})
})();
리뷰 스레드를 포괄하는 ul
인 RvTarget
에 하나의 이벤트 핸들러를 걸고 삭제 버튼에 이벤트를 위임했다. 버튼을 클릭하면 버튼이 포함된 부모 요소 중 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.href
와 location.replace
두 가지 메소드가 있는데, 차이점은 아래와 같다.
location.href | location.replace | |
---|---|---|
기능 | 새로운 페이지로 이동된다. | 기존 페이지를 새로운 페이지로 변경시킨다. |
형태 | 프로퍼티 | 메소드 |
주소 히스토리 | 기록된다 | 기록되지 않는다. |
사용 예 | location.href='abc.php' | location.replace('abc.php') |
href
는 페이지를 이동하는 것이기 때문에 이전 페이지로 다시 되돌아갈 수 있지만 (히스토리가 있음) replace
는 새로운 페이지로 덮어 씌우기 때문에 이전 페이지로 이동이 불가하다.
하지만 cache
가 남지 않고 페이지 요소가 refresh되어야 할 필요성이 있다면 replace
가 좋은 방법이 될 것 같다.
<a>
태그를 사용하는 이유window.location.href
사용은 비표준 링크로 페이지 간의 연결을 숨기는 것으로써 World Wide Web
의 약속에 위배된다.window.location
는 그런 UX적인 면에서 기존 앵커 태그보다 뒤쳐진다.왜 커밋, 푸시하고 나서야 다른 문제점이 눈에 보이는 걸까? 무한 푸시
코드 리뷰에서 지적받은 점과 스스로 고친 것들을 적는다.
- Layout Properties (position, float, clear, display)
- Box Model Properties (width, height, margin, padding)
- Visual Properties (color, background, border, box-shadow)
- Typography Properties (font-size, font-family, text-align, text-transform)
- Misc Properties (cursor, overflow, z-index)
매번 CSS 작성할 때마다 display, position 관련 속성을 제외하고선 생각나는대로 적어서 항상 다시 찾을때 헤매곤 했는데, 명확한 기준을 배워서 많은 도움이 되었다.
예를 들어 아래와 같은 상황에서, 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);
});
줌 인터랙션에 사용된 객체명이 수 많은 조건문에서 사용되어서 boundary
를 bor
로 줄였는데, 다른 사람이 보기에 이게 뭐하는 객체인지 쉽게 알 수 있는 객체명이 좋다고 권고되었다. 따라서 bor를 boundary로 다시 고쳤다. 다시 보니 굳이 줄일 필요 없었는데 짧고 간결한 네이밍에 집착했었던 것 같다.
// 수정 전
const bor = { xMin: 153, xMax: 297, yMin: 117, yMax: 353 };
// 수정 후
const boundary = { xMin: 153, xMax: 297, yMin: 117, yMax: 353 };
데모 :
위스타벅스: 로그인 페이지
위스타벅스: 음료 리스트 페이지
위스타벅스: 음료 상세 페이지
ⓒ Wonkook Lee
참고한 해당 홈페이지의 리소스와 음료 사진의 모든 저작권은
스타벅스 코리아에게 있으며 문제가 생길 시 즉시 조치하겠습니다.
🙏🏻 잘못된 정보가 있다면 지적해주세요
와 볼때마다 대단하십니다..ㄷㄷ