FE 개발자 실무 기출문제 - 캐러셀 라이브러리 활용편
Carousel UI 라이브러리는 왜 필요한가?
웹 서비스에는 보통 메인 페이지가 하나씩 있기 마련이다.
메인 페이지에서 자주 사용하는 UI가 바로 캐러셀이다.
캐러셀은 제한된 공간 안에서 여러 개의 콘텐츠를 슬라이드 형태로 순차적으로 보여주는 UI 패턴이다.
캐러셀 UI는 직접 구현할 수도 있고 라이브러리를 사용할 수 있는 선택지가 있지만
가능하면 라이브러리를 사용하는 쪽을 권한다.
왜냐하면 캐러셀 라이브러리는 다양한 옵션과 확장성, 크로스 브라우저 대응에 대한 이슈가 축적되어 있기 때문에
이 모든 것을 직접 파악하고 대응할 수 있는 슈퍼 개발자가 아닌이상 라이브러리를 적용하는 것이 정신 건강에 이롭기 때문이다.
물론 내부 동작원리가 어떻게 되는지 이해하고 파악하는 것은 충분히 훌륭한 자세이다!
캐러셀을 적용하고 있는 다양한 웹 서비스
정말 많은 웹 서비스들이 홈 페이지에서 캐러셀을 적용하고 있을까?
자주 방문하는 웹 서비스의 홈 페이지를 방문해보았다.
FE 개발자에게 캐러셀을 잘 핸들링하는 소양은 기본에 가깝다.
Embla Carousel 소개
캐러셀을 손쉽게 구현할 수 있는 라이브러리를 소개한다.
Embla Carousel이라는 친구다.
그냥 Swiper 쓰면 되는거 아니에요? 라고 궁금해 할 수 있다.
Npm Trends는 거짓말을 하지 않는다.
설치 및 사용법을 알아보자.
Get Started 페이지로 이동하면 사용하고 있는 FE 프레임워크에 따른 설치 방법을 안내해준다.
우리는 React를 사용할 것이므로 이쪽으로 가자.
다음의 커맨드를 실행한다.
1$ pnpm add embla-carousel-react
패키지 설치는 끝.
캐러셀을 만들기 위한 코드는 간단하다.
EmblaCarousel.tsx
1import React from 'react';
2import useEmblaCarousel from 'embla-carousel-react';
3
4export function EmblaCarousel() {
5 const [emblaRef] = useEmblaCarousel();
6
7 return (
8 <div className="embla" ref={emblaRef}>
9 <div className="embla__container">
10 <div className="embla__slide">Slide 1</div>
11 <div className="embla__slide">Slide 2</div>
12 <div className="embla__slide">Slide 3</div>
13 </div>
14 </div>
15 );
16}
스타일은 공식 가이드 문서 예제를 따르고 시각적으로 확인하기 쉽게 조금 수정했다.
global.css
1.embla {
2 overflow: hidden;
3}
4.embla__container {
5 display: flex;
6}
7.embla__slide {
8 flex: 0 0 100%;
9 min-width: 0;
10 border: 1px solid #ccc;
11 border-radius: 8px;
12 height: 300px;
13 padding: 20px;
14}
캐러셀 구현이 끝났다!
이제 팀장님께 보고하여 칭찬을 받으러 가자.
여기서 퇴근할 수 있지만 조금만 더 알아보자.
embla, embla__container, embla__slide 클래스 이름은 왜 부여된 것일까?
스타일 시트로 돌아가보자.
루트 엘리먼트에는 embla 클래스를 지정하여 너비를 넘어서는 요소는 보이지 않도록 overflow: hidden이 지정된다.
첫번째 자식 엘리먼트에는 embla__container 클래스를 지정하여 flex 컨테이너가 된다.
이 처리를 통해 슬라이드들이 가로로 나열되기 시작한다.
"아! 사실 슬라이드들은 가로로 쭉 나열되어 있지만 루트 엘리먼트의 영역만큼만 보여지고 있는 거구나!" 라는 것을 알 수 있다.
"그런데 어떻게 슬라이드를 좌우로 넘길 수 있는거지?" 라는 궁금증이 든다면 당신은 좋은 개발자다.
우선 슬라이드 하나의 너비가 864px로 찍히고 있는 것을 기억해두자.
컴포넌트를 렌더링 한 이후 개발자 도구를 열어서 embla_container 엘리먼트의 스타일 속성을 확인하자.
transform: translate3d(0px, 0px, 0px); 이 지정되어 있는 것을 확인할 수 있다.
1<div class="embla__container" style="transform: translate3d(0px, 0px, 0px);">
2 {...Slides}
3</div>
그 다음 슬라이드를 오른쪽으로 한칸 이동하여 보자.
translate3d 첫번째 인자인 x 값이 -864px로 변경되었다.
이 크기는 루트 엘리먼트 또는 슬라이드 한장의 너비와 동일하다.
1<div class="embla__container" style="transform: translate3d(-864px, 0px, 0px);">
2 {...Slides}
3</div>
슬라이드가 이동한 것이 아닌 슬라이드 컨테이너 자체가 좌측으로 이동한 것이다.
그 결과 루트 엘리먼트의 가시 영역 내에서 두번째 슬라이드가 보여지게 된다.
OK. 캐러셀 동작에 대한 부분은 여기까지다.
Embla Carousel 커스터마이징
이제 디자이너와 기획자가 요구하는 대로 동작할 수 있는 캐러셀을 만들어보자.
"최고에요!", "역시 OO님" 이라는 이야기가 벌써 당신의 귓가에 들려온다.
커스터마이징은 Embla Carousel 의 세가지 요소의 조합으로 달성한다.
- 캐러셀을 구성하는 HTML 엘리먼트에 대한 스타일링
useEmblaCarousel 훅을 통한 캐러셀 인스턴스 제어
- plugins을 통한 캐러셀 기능 확장
기본적인 커스터마이징 예시는 Guides 탭에서 설명하고 있으니 그곳을 참고하자.
여기서는 놓치기 쉬운 중요한 내용을 다룬다.
좌우 화살표 버튼을 추가해주세요!
디자이너와 기획자가 "좌우 슬라이드로 한칸씩 이동하는 화살표 버튼을 추가해주세요!" 라는 요청을 했다.
앞이 캄캄하다. 다음처럼 구현하면 되지 않을까?
1import { useCallback } from 'react';
2import useEmblaCarousel from 'embla-carousel-react';
3
4export function EmblaCarousel() {
5 const [emblaRef, emblaApi] = useEmblaCarousel();
6
7 const slidePrev = useCallback(() => {
8 if (emblaApi) {
9 emblaApi.scrollPrev();
10 }
11 }, [emblaApi]);
12
13 const slideNext = useCallback(() => {
14 if (emblaApi) {
15 emblaApi.scrollNext();
16 }
17 }, [emblaApi]);
18
19 return (
20 <div className="embla" ref={emblaRef}>
21 <div className="embla__container">{...Slides}</div>
22 <div>
23 <button onClick={slidePrev}>이전</button>
24 <button onClick={slideNext}>다음</button>
25 </div>
26 </div>
27 );
28}
이전, 다음 버튼을 클릭하면 잘만 동작한다.
이번에는 "이전" 버튼 위에서 마우스 다운한 상태로 포인터를 좌우로 이동해보자.
의도치 않게 슬라이드가 움직이기 시작한다. 왜 그런것일까?
embla 클래스를 부여하고 있는 루트 엘리먼트는 마우스 포인터 또는 터치 제스처를 수신하고 있다는 점이다.
그러므로 별도의 동작 UI를 루트 엘리먼트내에 추가하는 것은 바람직하지 못하다.
친절하게도 라이브러리는 embla__viewport 라는 클래스를 부여하는 엘리먼트를 추가하는 것으로 이 문제를 해결하도록 도와준다.
캐러셀의 구현을 변경하자.
1import useEmblaCarousel from 'embla-carousel-react';
2
3export default function EmblaCarousel() {
4 const [emblaRef] = useEmblaCarousel();
5
6 return (
7 <div className="embla">
8 <div className="embla__viewport" ref={emblaRef}>
9 <div className="embla__container">{...Slides}</div>
10 </div>
11 <div>
12 <button onClick={slidePrev}>이전</button>
13 <button onClick={slideNext}>다음</button>
14 </div>
15 </div>
16 );
17}
이제 동일한 상황에서 슬라이드가 의도치 않게 좌우로 이동하지 않는다.
이 처리가 해결하는 것은 포인터와 터치 이벤트의 수신을 embla__viewport 엘리먼트로 이전시킨 것이다.
버튼 엘리먼트와의 관계는 Sibling 관계가 되므로 의도하지 않은 이벤트가 발생하지 않게 된다.
나머지 일은 당신에게 맡긴다.
한번에 N장씩 슬라이드를 넘기게 해주세요!
힘들게 화살표 버튼을 추가했지만 다음날 "버튼 클릭 한번에 2장씩 슬라이드를 넘기게 해주세요!" 라는 요청이 왔다.
코드 네줄만 바꾸면 끝난다. 퇴근하자.
1import { useCallback } from 'react';
2import useEmblaCarousel from 'embla-carousel-react';
3
4export default function EmblaCarousel() {
5 const [emblaRef, emblaApi] = useEmblaCarousel();
6
7 const slidePrev = useCallback((numSlides: number) => {
8 if (emblaApi) {
9 emblaApi.scrollTo(emblaApi.selectedScrollSnap() - numSlides);
10 }
11 }, [emblaApi],
12 );
13
14 const slideNext = useCallback((numSlides: number) => {
15 if (emblaApi) {
16 emblaApi.scrollTo(emblaApi.selectedScrollSnap() + numSlides);
17 }
18 }, [emblaApi],
19 );
20
21 return (
22 <div className="embla">
23 <div className="embla__viewport" ref={emblaRef}>
24 <div className="embla__container">{...Slides}</div>
25 </div>
26 <div>
27 <button onClick={() => slidePrev(2)}>이전</button>
28 <button onClick={() => slideNext(2)}>다음</button>
29 </div>
30 </div>
31 );
32}
Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
좌우로 더 이상 이동할 수 없을 때 버튼을 비활성화 해주세요!
이번에는 고생을 좀 하겠지만 정시 퇴근이 불가능한 것은 아니다.
좌우 슬라이드 이동 가능 여부를 상태로 관리하여 UI 업데이트가 발생할 수 있도록 만들어야 한다.
그리고 useEffect 훅을 통해서 다음의 경우에 이동 가능 여부를 체크하도록 한다.
- 캐러셀 인스턴스가 활성화 되었을 때
- 화면상에 보여지고 있는 캐러셀 슬라이드 인덱스가 변경되었을 때
- 브라우저 창의 크기 변화로 인해 캐러셀 인스턴스가 다시 활성화 되었을 때
1import { useCallback } from 'react';
2import useEmblaCarousel from 'embla-carousel-react';
3
4export default function EmblaCarousel() {
5 const [emblaRef, emblaApi] = useEmblaCarousel();
6 const [canScrollPrev, setCanScrollPrev] = useState<boolean | undefined>(
7 undefined,
8 );
9 const [canScrollNext, setCanScrollNext] = useState<boolean | undefined>(
10 undefined,
11 );
12
13 const checkNavigationAvailability = useCallback(() => {
14 if (emblaApi) {
15 setCanScrollPrev(emblaApi.canScrollPrev());
16 setCanScrollNext(emblaApi.canScrollNext());
17 }
18 }, [emblaApi]);
19
20useEffect(() => {
21 if (!emblaApi) return;
22 checkNavigationAvailability();
23 emblaApi.on('init', checkNavigationAvailability);
24 emblaApi.on('select', checkNavigationAvailability);
25 emblaApi.on('reInit', checkNavigationAvailability);
26 }, [emblaApi, checkNavigationAvailability]);
27
28 return (
29 <div className="embla">
30 <div className="embla__viewport" ref={emblaRef}>
31 <div className="embla__container">{...Slides}</div>
32 </div>
33 <div>
34 <button
35 className="font-bold mr-1"
36 disabled={canScrollPrev === false}
37 onClick={() => slidePrev(2)}
38 >
39 이전 {canScrollPrev === false && '(비활성화)'}
40 </button>
41 <button
42 className="font-bold"
43 disabled={canScrollNext === false}
44 onClick={() => slideNext(2)}
45 >
46 다음 {canScrollNext === false && '(비활성화)'}
47 </button>
48 </div>
49 </div>
50 );
51}
Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
슬라이드가 스와이프 한 만큼만 움직이게 해주세요!
다시 앞이 캄캄해진다. 난 그런거 할 줄 모른단 말이야!
라이브러리가 코드 한 줄이면 해결해준다.
퇴근하면 된다.
1import useEmblaCarousel from 'embla-carousel-react';
2
3export default function EmblaCarousel() {
4 const [emblaRef] = useEmblaCarousel({ dragFree: true });
5
6 return (
7 <div className="embla">
8 <div className="embla__viewport" ref={emblaRef}>
9 <div className="embla__container">{...Slides}</div>
10 </div>
11 </div>
12 );
13}
슬라이드가 자동으로 넘어가게 해주세요!
누군가의 요청으로 캐러셀 슬라이드가 마치 자동 진열장처럼 동작하게 해달라는 요청이 왔다.
플러그인을 활용해볼 차례다.
1$ pnpm add embla-carousel-autoplay
1import useEmblaCarousel from 'embla-carousel-react';
2import AutoPlay from 'embla-carousel-autoplay';
3
4export default function EmblaCarousel() {
5 const [emblaRef] = useEmblaCarousel({ loop: true }, [
6 AutoPlay({ delay: 2000, stopOnInteraction: false }),
7 ]);
8
9 return (
10 <div className="embla">
11 <div className="embla__viewport" ref={emblaRef}>
12 <div className="embla__container">{...Slides}</div>
13 </div>
14 </div>
15 );
16}
2초마다 맛있게 슬라이드가 넘어간다.
슬라이드가 마퀴(Marquee)처럼 동작하게 해주세요!
플러그인 한줄과 스타일링 작업이 필요하다.
마퀴처럼 동작시키기 위해 loop: true + AutoScroll 플러그인을 적용하고 있다.
특이사항은 슬라이드간 간격을 부여하고자 하는 경우이다.
embla__container 엘리먼트에 margin-left 스타일 속성을 부여하고자 하는 간격만큼 음의 값으로 부여하고 embla__slide 엘리먼트에 margin-left 스타일 속성을 부여하고자 하는 간격만큼 양의 값으로 부여하면 된다.
1$ pnpm add embla-carousel-auto-scroll
1import useEmblaCarousel from 'embla-carousel-react';
2import AutoScroll from 'embla-carousel-auto-scroll';
3
4const slideClassName =
5 'flex-[0_0_200px] min-w-0 !border-[1px] border-solid border-[#ccc] rounded-[8px] p-[20px] h-[300px] flex items-center justify-center ml-4';
6
7export default function EmblaCarousel() {
8 const [emblaRef] = useEmblaCarousel({ loop: true }, [
9 AutoScroll({ playOnInit: true, stopOnInteraction: false }),
10 ]);
11
12 return (
13 <div className="embla">
14 <div className="embla__viewport" ref={emblaRef}>
15 <div className="flex -ml-4">
16 <div className={slideClassName}>Slide 1</div>
17 <div className={slideClassName}>Slide 2</div>
18 <div className={slideClassName}>Slide 3</div>
19 <div className={slideClassName}>Slide 4</div>
20 <div className={slideClassName}>Slide 5</div>
21 <div className={slideClassName}>Slide 6</div>
22 </div>
23 </div>
24 </div>
25 );
26}
오픈소스 웹 페이지의 Sponsors 섹션에서 많이 보던 UI.
Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
오른쪽 끝에 접근할 수 있는 슬라이드 일부가 노출되게 해주세요!
캐러셀 슬라이드를 운영하는 과정에서 유저가 더 탐색할 수 있는 콘텐츠가 있다는 것을 인지하기 쉽도록 탐색 가능한 슬라이드가 있으면 일부를 노출해달라는 요청이 왔다.
이 경우는 슬라이드 정렬 옵션과 약간의 스타일링이 필요하다.
align: start 옵션을 주면 활성화된 슬라이드를 가시영역의 왼쪽 가장자리를 시작점으로 정렬하게 된다.
캐러셀 가시영역의 너비를 100% 기준으로 두고 활성화된 슬라이드의 너비를 95%, 슬라이드간 간격을 2%로 설정하였으니 다음 방문할 슬라이드의 3% 너비만 노출하게 된다.
퍼센트 값으로 설정한 이유는 어떤 해상도에서든 이 비율을 지켜서 표시하게 될 것이기 때문이다.
1import { useCallback } from 'react';
2import useEmblaCarousel from 'embla-carousel-react';
3
4export default function EmblaCarousel() {
5 const [emblaRef] = useEmblaCarousel({ align: 'start' });
6
7 return (
8 <div className="embla">
9 <div className="embla__viewport" ref={emblaRef}>
10 <div className="flex space-x-[2%]">
11 <div className="embla__slide !flex-[0_0_95%]">Slide 1</div>
12 <div className="embla__slide !flex-[0_0_95%]">Slide 2</div>
13 <div className="embla__slide !flex-[0_0_95%]">Slide 3</div>
14 </div>
15 </div>
16 </div>
17 );
18}
정리
Embla Carousel 라이브러리를 통해 다양한 유즈케이스를 구현해보았다.
FE 개발자로써 실무를 뛰다보면 캐러셀을 통해 메인 페이지를 작업할 일이 많이 있다.
한번 잘 익혀두면 많은 시간을 절약을 할 수 있으니 공식 문서를 자주 참고하면 좋다.