3월 인턴일지 (2) 📝 : [react] flickity를 사용한 이미지 슬라이더 (carousel)구현하기

Ko Seoyoung·2021년 3월 21일
0

carousel은 슬라이더 또는 슬라이더쇼라고도 불리며 광고 배너나 이미지 슬라이더 등 많은 곳에 활용되기 때문에 웹 페이지, 앱 스크린에서의 빠질 수 없는 컴포넌트이다.

flickity는 프로젝트에 매끄럽고 생동감 있는 carousel을 적용할 수 있게 해준다.

https://flickity.metafizzy.co/


react에서 flickity 사용

React에서 flickity를 사용하려면 이 블로그(Using Flickity with React)처럼
Slider 컴포넌트를 만들어서 사용할 수도 있는데, React Flickity Component를 활용하면 더 쉽게 리액트에서 flickity를 사용할 수 있다.

Swiper 컴포넌트 생성

import styled from 'styled-components';
import { ReactElement, useEffect, useState } from 'react';
import Flickity, { FlickityOptions } from 'react-flickity-component';

export type SwiperProps = {
  contents?: Array<ReactElement>;
  onChange?: (index: number) => void;
  current?: number;
  style?: React.CSSProperties;
} & FlickityOptions;

function Swiper({
  contents,
  onChange,
  current,
  style,
  ...flickityOptions
}: SwiperProps) {
  const [ref, setRef] = useState<Flickity>();

  useEffect(() => {
    if (!ref) {
      return;
    }

    const handleFlktyChange = (index: number) => {
      onChange(index);
    };

    ref.on('change', handleFlktyChange);
    return () => {
      ref.off('change', handleFlktyChange);
    };
  }, [ref]);

  useEffect(() => {
    if (!ref || current === undefined || current === null) {
      return;
    }

    if (current !== ref.selectedIndex) {
      ref.select(current);
    }
  }, [current]);

  return (
    <Wrapper style={style}>
      <Flickity
        options={{
          prevNextButtons: false,
          ...flickityOptions,
        }}
        flickityRef={(c) => setRef(c)}
      >
        {contents}
      </Flickity>
    </Wrapper>
  );
}

export default Swiper;

const Wrapper = styled.div`
  width: 100%;

  overflow: hidden;
  outline: none;
`;

좌우 이동 화살표는 커스텀 버튼을 사용할 것이므로 prevNextButtons: false를 해주었고, flickityRef에 useRef 타입이 할당될 수 없어 useState로 Flickity타입의 ref를 선언했다.

이벤트를 ref.on의 인자로는 flickity 문서에 명세 된 flickity의 이벤트가 들어간다. (https://flickity.metafizzy.co/events.html)


Indicator 생성

import React from 'react';
import styled from 'styled-components';

import { GREY } from '@src/component/atoms/colors';

export type IndicatorType = 'Default' | 'Bullet';

export type IndicatorProps = {
  current: number;
  size: number;
  onChange?: (index: number) => void;
  style?: React.CSSProperties;
  type?: IndicatorType;
};

function Indicator({
  current,
  onChange,
  size,
  style,
  type = 'Default',
}: IndicatorProps) {
  const handleBulletClick = (index: number) => () => {
    if (!onChange) {
      return null;
    }

    onChange(index);
  };

  if (size <= 1) {
    return null;
  }

  const Item = {
    Default: Default,
    Bullet: Bullet,
  }[type];

  return (
    <Wrapper style={{ ...style }}>
      {[...Array(size)].map((_, index) => (
        <Item
          key={index}
          selected={current === index}
          onClick={handleBulletClick(index)}
        />
      ))}
    </Wrapper>
  );
}

export default Indicator;

const Wrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;

  margin: 0.8rem auto 0;
`;

const Default = styled.div<{ selected: boolean }>`
  height: 0.2rem;
  border-radius: 9999px;
  &:not(:last-child) {
    margin-right: 0.6rem;
  }

  transition: all 0.5s;
  cursor: pointer;

  ${({ selected }) => `
    width: ${selected ? 1.6 : 1.2}rem;
    background-color: ${selected ? GREY[900] : GREY[300]};
  `}
`;

const Bullet = styled.div<{ selected: boolean }>`
  width: 0.8rem;
  height: 0.8rem;
  border-radius: 9999px;
  &:not(:last-child) {
    margin-right: 0.8rem;
  }

  transition: all 0.5s;
  cursor: pointer;

  ${({ selected }) => `
    background-color: ${selected ? GREY[900] : GREY[300]};
  `}
`;

[예제] Swiper와 Indicator로 만든 ImageSlider

import React, { useState } from 'react';
import styled from 'styled-components';

import { Img, Swiper } from '@src/component/atoms';
import ImageSliderControl from './control';
import Indicator from '../../indicator';

export type ImageSliderProps = {
  images: string[];
  style?: React.CSSProperties;
  imageStyle?: React.CSSProperties;
  hasIndicator?: boolean;
  hasControl?: boolean;
  onClick?: (index: number) => void;
};

function ImageSlider({
  images,
  style = {},
  imageStyle = {},
  hasIndicator = true,
  hasControl = true,
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onClick = () => {},
}: ImageSliderProps) {
  const [current, setCurrent] = useState(0);

  return (
    <Wrapper
      style={style}
      onClick={() => {
        onClick(current);
      }}
    >
      <Swiper
        current={current}
        onChange={setCurrent}
        contents={images.map((image) => (
          <Img
            src={image}
            alt={image}
            width="36rem"
            height="36rem"
            size={512}
            style={{ userSelect: 'none', ...imageStyle }}
          />
        ))}
        style={{ width: '36rem' }}
      />
      {hasControl && (
        <ImageSliderControl
          current={current}
          onChange={setCurrent}
          size={images.length}
        />
      )}
      {hasIndicator && (
        <Indicator
          current={current}
          onChange={setCurrent}
          size={images.length}
          type="Default"
        />
      )}
    </Wrapper>
  );
}

export default React.memo(ImageSlider);

const Wrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;

  position: relative;
  overflow-x: hidden;
`;

globally

.flickity-enabled {
	outline: none
}

슬라이더 클릭시 파란색 border가 생기는 것을 없애기 위해 css를 추가해주었다.

profile
Web Frontend Developer 👩🏻‍💻 #React #Nextjs #ApolloClient

0개의 댓글