<div id="root"></div>
$darkgray: #333;
$ivory: #fffef2;
$beige: #ebeade;

.CarouselItem {
  flex-shrink: 0;
  width: calc(100% / 3);
  text-align: center;
  font-size: 14px;

  .itemImg {
    display: flex;
    align-items: flex-end;
    height: 300px;

    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }

  .textWrapper {
    padding: 0 20px;
    line-height: 1.33;
    letter-spacing: -0.2px;

    h5 {
      margin: 10px 0;
      line-height: 1.7;
    }
  }
}

.Carousel {
  position: relative;
  margin-top: 10px;
  overflow: hidden;

  button.navigation {
    position: absolute;
    top: calc(50% - 80px);
    width: 80px;
    height: 80px;
    background-color: $darkgray;
    color: $ivory;
    text-align: center;
    font-size: 16px;
    z-index: 1;
    transition: transform cubic-bezier(0.22, 0.61, 0.36, 1) 0.5s;

    &.prev {
      left: 0;
      transform: translateX(-80px);
    }

    &.next {
      right: 0;
      transform: translateX(80px);
    }
  }

  &:hover {
    button.navigation {
      transform: translateX(0);
    }
  }

  .itemWrapper {
    display: flex;
    margin: 0 80px 48px;
    transition: transform ease 0.5s;
  }

  .pagination {
    height: 2px;
    margin: 0 80px 20px;
    background-color: rgba(0, 0, 0, 0.2);

    .current {
      height: 100%;
      background-color: $darkgray;
      transition: transform cubic-bezier(0.22, 0.61, 0.36, 1) 0.5s;
    }
  }
}
View Compiled
const { useState } = React;

const CarouselItem = ({ heading, description, alt, src }) => {
  return (
    <div className="CarouselItem">
      <div className="itemImg">
        <img alt={alt} src={src} />
      </div>
      <div className="textWrapper">
        <h5>{heading}</h5>
        <div className="description">{description}</div>
      </div>
    </div>
  );
};

const Carousel = ({ className, dataList }) => {
  const [slideIndex, setSlideIndex] = useState(0);

  const onClick = event => {
    const isPrev = event.target.className.includes('prev');
    const isNext = event.target.className.includes('next');
    if (isPrev) {
      setSlideIndex(curr => --curr);
    }
    if (isNext) {
      setSlideIndex(curr => ++curr);
    }
  };

  const firstSlide =
    slideIndex === 0
      ? {
          style: { transform: 'translateX(-80px)' },
        }
      : null;

  const lastSlide =
    slideIndex === dataList.length - 3
      ? {
          style: { transform: 'translateX(80px)' },
        }
      : null;

  return (
    <div className={`Carousel ${className}`}>
      <button
        className="navigation prev"
        onClick={onClick}
        style={firstSlide && firstSlide.style}
        disabled={firstSlide}
      >
        <i className="fas fa-chevron-left prev" />
      </button>
      <div
        className="itemWrapper"
        style={{ transform: `translateX(calc(-100% / 3 * ${slideIndex}))` }}
      >
        {dataList &&
          dataList.map(data => (
            <CarouselItem
              key={data.id}
              heading={data.name}
              description={data.description}
              alt={data.name}
              src={data.src}
            />
          ))}
      </div>
      <button
        className="navigation next"
        onClick={onClick}
        style={lastSlide && lastSlide.style}
        disabled={lastSlide}
      >
        <i className="fas fa-chevron-right next" />
      </button>
      <div className="pagination">
        <div
          className="current"
          style={{
            width: `calc(100% / ${dataList.length - 2})`,
            transform: `translateX(calc(100% * ${slideIndex}))`,
          }}
        />
      </div>
    </div>
  );
};

const DATA = [
  {
    id: 0,
    name: '요거트',
    description: '첫 번째 설명',
    src: 'https://images.unsplash.com/photo-1643121888146-9839fa08c8a2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80',
  },
  {
    id: 1,
    name: '빵',
    description: '두 번째 설명',
    src: 'https://images.unsplash.com/photo-1643114917776-78071774f6b8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80',
  },
  {
    id: 2,
    name: '샐러드',
    description: '세 번째 설명',
    src: 'https://images.unsplash.com/photo-1643127045144-7fe95e9888a2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80',
  },
  {
    id: 3,
    name: '과일',
    description: '네 번째 설명',
    src: 'https://images.unsplash.com/photo-1643131514219-1e480cce39aa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80',
  },
  {
    id: 4,
    name: '케이크',
    description: '다섯 번째 설명',
    src: 'https://images.unsplash.com/photo-1609050156152-5d064be292bb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80',
  },
];

ReactDOM.render(<Carousel dataList={DATA} />, document.getElementById('root'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js