원티드 프리온보딩 프론트엔드 코스에 지원했을 당시에 제출했던 선발 과제에 대해서 포스팅해보려고 한다. 과제를 그대로 포스팅해도 될지 몰라서 멘토님께 여쭤봤는데, 마음껏 가능하다고 하셨다!
💡 과제 요구사항
[선발 과제]: 원티드 페이지 상단 영역을 React 기반으로 클론해주세요.
💡 요구사항 분석
원티드 상단 영역을 분석해보면 다음과 같다.
- 슬라이드 되어야 할 이미지가 어떠한 반응형에도 항상 페이지의 정중앙에 유지되도록 설정
- 슬라이드 이미지가 3~4 초마다 자동으로 슬라이드 되어야 함
- 슬라이드 이미지에 마우스를 hover 하게 되면 슬라이드를 멈춤
- 이미지가 무한 루프로 연결되어 있음
- 브라우저 배율을 500%로 최대한 줄여도 이미지가 끊이지 않고 계속 보여야 함
- 페이지를 새로 고침 할 때마다 나오는 이미지 순서가 바뀜
- 화면이 반응형으로 줄어들 때마다 이미지 width 값을 동적으로 변경해줘야 함
- 이미지의 width 값이 동적으로 변할 때는 애니메이션과 화면 이동을 막아야 함
- 슬라이드 이동 버튼을 클릭하면 0.6초 정도 비활성화된 후 다시 활성화됨
- 데스크톱, 모바일에서 이미지 스와이프 기능이 있음 (특정 거리만큼 움직이면 그쪽 방향으로 스와이프 됨)
- 스와이프를 하면 실시간으로 이미지가 마우스를 따라와야 함
- 스와이프 중에 마우스가 이미지 영역 밖으로 나가면, 특정 거리만큼 이동했을 경우에는 슬라이드 해주고 아닐 경우 원 상태로 돌아옴
- 모든 요소는 반응형으로 동작해야 한다
💡 링크
깃허브 코드 보러가기
배포 페이지 보러가기
💡 이슈 사항 해결 과정
무한 이미지 슬라이드(캐러샐) 기능
처음에 시작할 땐 무한 이미지 슬라이드를 너무 쉽게 생각했다. 단순히 이미지가 슬라이드 될 때마다 첫 번째 요소를 지우고 맨 뒤에다가 연결해 주고 이런 식으로 하면 될 줄 알았는데, 막상 해보니 첫 번째 요소를 지우게 되면 인덱스가 하나씩 당겨지기 때문에 이미지도 같이 앞쪽으로 슬라이드 되는 현상이 일어났다. 어떻게 해야 해야 할지 고민하다가 예전에 대학교 동기들과 마켓 컬리를 클론 코딩을 했었던 기억이 났다. 마켓 컬리도 원티드 메인과 비슷한 무한 이미지 슬라이드가 있었는데, 그 당시에 했던 로직이 원본 이미지 배열을 2개 복사해서 [... 원본 이미지 리스트 복사본, ...원본 이미지 리스트, ...원본 이미지 리스트 복사본] 이렇게 만든 뒤에 가운데 이미지 리스트 중앙에서 시작해서 오른쪽(또는 왼쪽)으로 이동하다가 시작했던 이미지와 같은 이미지가 나오게 되면, 사용자 몰래 처음에 시작했던 이미지 리스트로 바꿔치기 하는 로직이다. 이렇게 하게 되면 이미지가 계속해서 무한으로 나오는 거처럼 보이게 된다.
하지만 여기서 또 해결해야 할 과제가 생겼다. 그렇다면 이 [...원본 이미지 리스트 복사본, ...원본 이미지 리스트, ...원본 이미지 리스트 복사본] 배열에서 어떻게 첫 시작을 가운데 위치부터 나오도록 할까?이다. 위치를 바꿔주는 css인 transform 으로 랜더링 되자마자 가운데 위치로 이동시켜주는 방식으로 처음 구현을 하였지만 그렇게 되면 첫 랜더링 되자마자 시작 위치에서부터 가운데 위치까지 이동하는 모션이 보인다. 그래서 transform 처럼 움직이는 게 아니라 위치를 잡아주는 absolute를 이용하기로 했다. 이미지 리스트 박스 아래에 div 태그를 하나 더 만든 뒤에 position: absolute를 주어서 시작 위치 값을left: 슬라이드 이미지 width 값 x 배열 가운데 인덱스 이렇게 해서 시작 위치를 가운데로 설정했다.
이제 시작 위치도 이미지 배열 중앙으로 잡아주었고 캐러샐 기능만 구현하면 된다. 캐러샐 기능은 쉬워서 쉽게 구현하였는데, 구현하니 또 이슈가 발생했다. 이미지를 무한으로 연결시켜주는 로직 부분에서 사용자 몰래 이미지를 바꿔치기한다고 했었는데, 이미지 슬라이드를 하다 보면 바꿔치기할 때 바꿔 치는 장소로 슬라이드가 되돌아가는 모션이 보인다. 바꿔치는 순간에는 슬라이드 모션이 보이지 않게 바꿔야 하기 때문에 const [isAnimation, setIsAnimation] = useState(true)라는 상태를 만들어서 이미지 위치를 바꿔 치는 순간에만 isAnimation을 false로 만들어서 애니메이션 효과를 끄는 것으로 해결했다.
버튼을 클릭하지 않아도 자동으로 이미지 슬라이드 되게 설정
원티드 페이지를 보면 양옆 버튼을 클릭하지 않아도 이미지가 3~4 초마다 자동으로 슬라이드가 되고 있다. 자동 슬라이드가 되는 것은 setInterval을 통해 구현하였는데, 여기서 한 가지 조건이 더 들어간다. 원티드 페이지를 보면 이미지에 마우스를 hover 하면 이미지가 자동으로 슬라이드 되는 기능이 멈추게 된다. 그래서 const [isFlowing, setIsFlowing] = useState(true)라는 상태를 만들어서 isFlowing이 true일 때만 자동 슬라이드가 되도록 설정하고 이미지에 마우스를 올렸을 땐 isFlowing을 false로 설정해줘서 자동 슬라이드가 되지 않도록 하였다.
useEffect(() => {
let intervalId: NodeJS.Timer;
if (isFlowing) { // isFlowing이 true일 경우에만 자동 슬라이드 작동
intervalId = setInterval(() => {
setCurrentSlide(currentSlide + slideCount);
}, 3500);
}
return () => clearTimeout(intervalId);
}, [isFlowing, currentSlide, slideCount]);
반응형으로 화면이 줄어들 때 이미지 width 값이 동적으로 바뀌어야 함
처음에는 반응형으로 구현할 때 이미지가 특정 픽셀마다 줄어드는 것으로 생각했다. 예를 들어서 화면이 1200px 일 경우에는 이미지가 1060px이고 화면이 1060px 일 땐 이미지가 760px, 화면이 760px 일 땐 이미지가 500px 이런 식으로 말이다. 하지만 원티드 페이지를 잘 분석해 보니 화면이 1200px 아래로 줄어들게 되면 이미지가 화면에 맞게 동적으로 계속 바뀌게 되어있다. window.innerWidth 값을 이용하면 브라우저의 width 값을 받아올 수 있었는데, 원티드 페이지를 잘 분석해 보니 현재 브라우저의 width 값 - 80px로 이미지의 width 값이 결정되고 있는 걸 확인했다. 그래서 브라우저 width값이 1200px 아래로 내려가게 되면 동적으로 이미지 width 값을 설정하도록 구현하였다.
하지만 또 이슈가 발생했는데, 화면을 줄이는 와중에 이미지가 자동으로 슬라이드가 되면서 슬라이드로 이동하는 거리와 이미지 크기가 달라지게 되어서 UI가 이상하게 나오는 현상이 발생했다. 원티드에서는 이걸 어떤 식으로 해결했나 분석해보니 화면이 줄어들거나 늘어날 때는 이미지가 슬라이드 되지 않도록 설정해놨다. 그래서 나도 다음 코드와 같이 화면이 resize 될 동안에는 이미지 슬라이드가 안되도록 구현하였다.
useEffect(() => {
const handleResize = () => {
// resize 되는 동안에는 애니매이션, 이동을 막아줌
setIsAnimaion(false);
setIsFlowing(false);
// resize 가 끝난 0.5초 이후에 다시 활성화
setTimeout(() => {
setIsFlowing(true);
setIsAnimaion(true);
}, 500);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
슬라이드 이동 버튼을 빠르게 클릭하면 애니메이션이 엄청 빠르게 이동되는 현상이 생김
슬라이드 이동 버튼을 연속해서 빠르게 클릭하면, 클릭 한 횟수만큼 애니메이션이 빠르게 움직이는 현상이 나타난다. 원티드 페이지를 보니 버튼을 클릭하면 0.6초 정도 버튼을 비활성화시킨 후에 다시 활성화시키는 방식으로 구현되어 있었다. 그래서 나도 아래 코드와 같이 버튼이 클릭되면 0.6초 정도 비활성화됐다가 다시 활성화시키는 방식으로 구현하였다.
const onDisabledButton = () => {
setIsDisabled(true);
timer.current = setTimeout(() => {
setIsDisabled(false);
}, 600);
};
useEffect(() => {
return () => {
if (timer.current) {
clearTimeout(timer.current);
}
};
}, [timer]);
데스크톱, 모바일 스와이프 기능 구현
스와이프 기능은 지금까지 구현해왔던 코드들을 토대로 조합하는 식으로 구현하면 되어서 다소 쉽게 진행됐다. 스와이프 기능은 크게 모바일 스크린 터치 이벤트, 피씨 마우스 클릭 이벤트 이렇게 두 가지로 나눠서 구현하였는데, 클릭(터치)한 순간의 px 값으로 이미지 배열들을 움직이게 해 주고, 클릭(터치)이 완료된 순간에 px 값을 통해서 움직인 거리에 따라 스크린을 이동 시키 도록 구현하였다.
이때 onMouse 이벤트와 onTouch 이벤트의 차이에서 발생하는 이슈가 있었다. onTouch 이벤트의 경우에는 영역 밖에서 터치를 취소하게 돼도 onTouchEnd 이벤트가 실행이 되었는데, onMouse 이벤트는 해당 영역 내에서 마우스를 취소할 경우에만 onMouseUp 이벤트가 실행되었다. 그래서 onMouse 이벤트에서는 onMouseOut 이벤트를 추가적으로 실행하여 영역 밖으로 나가게 되면 이벤트를 취소시키도록 설정하였다.
이미지를 스와이프 할 때 반응속도가 느리게 움직이는 이슈도 있었다. 개발자 도구를 통해서 이미지를 스와이프 할 때 이미지의 px이 잘 작동하는지 체크해보았는데, 너무 잘 작동하고 있었고, useState의 상태가 잘 변화하는지도 체크해봤는데 잘 작동하고 있었다. 그래서 원인이 뭐지..? 하고 계속 보다가 아! 지금 이미지에 애니메이션이 걸려 있었구나 하고 생각이 났다. 그래서 이미지 스와이프가 시작되면 애니메이션을 잠깐 끄고 스와이프가 완료되면 다시 애니메이션을 실행하는 식으로 수정하여서 해당 이슈를 해결하였다.
스와이프 기능의 마지막 고민으로는 어느 정도 스와이프를 해야 해당 이미지를 슬라이드 시켜줄지에 관한 고민이다. 원티드 홈페이지에서 분석을 해봐도 정확히 어느 정도 스와이프 해야 슬라이드가 되는지 잘 모르겠어서, 이 부분은 내가 아래 코드와 같이 짐작으로 이동 기준을 정했다. 마우스를 조금 스와이프 했을 때는 다시 원상태로 돌아오고, 이미지 크기의 5분의 1 이상 스와이프 하게 되면 그쪽 방향으로 슬라이드가 되도록 구현을 하였다.
// 스와이프 이동 기준
const moveRange = useMemo(() => {
return Math.floor(slideItemWidth / 5);
}, [slideItemWidth]);
이동 범위가 스와이프 이동 기준보다 크면 슬라이드를 이동시키도록 하는 코드
if (touchMoveDistance > moveRange) onPrevSlide();
if (touchMoveDistance < moveRange * -1) onNextSlide();
💡 마무리
스와이프 기능은 평소에 해보지 않았던 기능인데, 이번 기회에 경험하게 되어서 또 성장한 거 같다ㅎ 평소 서비스 개발을 할 때 라이브러리를 자주 사용하였는데, 너무 라이브러리에 의존하다 보면 코딩 실력이 많이 떨어지는 거 같아서 걱정이다ㅠ (근데 라이브러리가 너무 편함...) 나중에는 나도 라이브러리를 만드는 개발자가 되었으면 좋겠다....!!
'원티드 프리온보딩' 카테고리의 다른 글
원티드 프리온보딩 과제 - 견적 요청서 페이지 (0) | 2022.02.12 |
---|---|
원티드 프리온보딩 과제 - 환율 계산기 (1) | 2022.01.30 |
댓글