배너는 어떻게 개발하나요?

37336 단어 CSSReactCSS

평소처럼 웹 서비스를 이용하는데 문득 '메인 페이지의 배너는 어떻게 개발하는 걸까' 궁금증이 들었어요.

이번 글에서는 제가 배너를 개발한 방식에 대해 얘기해볼게요.


배너를 개발하기 위해서 먼저 다음 두가지에 대해 생각해볼 필요가 있었습니다.

  • 여러개의 배너 이미지 중 어떻게 하나의 배너 이미지만 노출시킬 것인가?
  • 배너 이미지를 자동으로 변경하려면 어떻게 해야 하는가?

1. 여러개의 배너 이미지 중 어떻게 하나의 배너 이미지만 노출시킬 것인가?

이를 위해서 'position: absolute'를 사용해서 배너 이미지들을 같은 지점에 위치시켰습니다. 그리고 'display: none', 'display: block'을 사용해서 하나의 배너 이미지만 노출시키고 다른 배너 이미지들은 노출되지 않도록 했습니다.

Banner.tsx

...

const bannerImage = ['https://.../1.jpg', 'https://.../2.jpg', 'https://.../3.jpg']; // 이미지 url을 갖고있는 배열

const [bannerIndex, setBannerIndex] = useState<number>(0); //화면에 보여지는 배너 이미지의 인덱스를 갖는 state

...

{bannerImage.map((image, index) => {
	return (
    <div
    	className={`banner__image${
        	bannerIndex === index ? " active" : ""
        }`}
        key={index}
    >
    	<a href="" style={{ background: `url(${image})` }}></a>
    </div>	  
    );
})}

...

}

Banner.scss

...

 .banner__image {
    position: absolute;
    width: 100%;
    height: 100%;
    display: none;
    animation: bannerOpacity 0.7s;

    a {
      display: block;
      height: 100%;
    }
    
    .active {
      display: block;
  	}
  }
  
  @keyframes bannerOpacity {
    0% {
      opacity: 0;
    }

    100% {
      opacity: 1;
    }
  }
...

사용자에게 노출시킬 배너 이미지를 정하기 위해서 bannerIndex라는 state를 사용했습니다. 배너 이미지의 index가 bannerIndex와 동일할 경우 className에 active를 추가해서 'display: block'이 적용되도록 했습니다.

2. 배너 이미지를 자동으로 변경하려면 어떻게 해야 하는가?

배너 이미지를 자동으로 변경하기 위해서 setInterval을 사용했습니다.

주의할 점은 react에서는 setInterval을 다음과 같은 코드만으로 사용할 경우

setInterval(() => setBannerIndex((bannerIndex + 1) % bannerImage.length), 3000);

랜더링이 될 때 마다 새로운 interval이 생성되기 때문에 문제가 발생합니다. 그래서 unmount가 될 때 clearInterval을 호출해서 현재 실행 중인 interval을 제거해야 합니다. 이를 위해서 useEffect를 사용했습니다.

useEffect(() => {
	const id = setInterval(() => setBannerIndex((bannerIndex + 1) % bannerImage.length), 3000)

    return () => clearInterval(id);
}, [bannerIndex]);

브라우저에서는 다음과 같이 보여집니다.

전체 코드는 다음과 같습니다.
Banner.tsx

import * as React from "react";
import { useState, useEffect } from "react";
import "../styles/Banner.scss";

function Banner() {
  const bannerImage = [
    "https://edit-edition.com/images/m-1.jpg",
    "https://edit-edition.com/web/upload/NNEditor/20211110/03_shop1_201805.jpg",
    "https://edit-edition.com/web/upload/NNEditor/20211110/12_shop1_201806.jpg",
  ];

  const [bannerIndex, setBannerIndex] = useState<number>(0);
  const [bannerChangeFlag, setBannerChangeFlag] = useState<boolean>(true);

  const handleLeftBtnClick = (e: React.MouseEvent) => {
    setBannerIndex(
      bannerIndex === 0
        ? bannerImage.length - 1
        : (bannerIndex - 1) % bannerImage.length,
    );
  };

  const handleRightBtnClick = (e: React.MouseEvent) => {
    setBannerIndex((bannerIndex + 1) % bannerImage.length);
  };

  const handleDotClick = (e: React.MouseEvent) => {
    const index = e.currentTarget.getAttribute("data-index");

    setBannerIndex(index ? Number(index) : 0);
  };

  const handleMouseEnter = (e: React.MouseEvent) => {
    setBannerChangeFlag(false);
  };

  const handleMouseLeave = (e: React.MouseEvent) => {
    setBannerChangeFlag(true);
  };

  useEffect(() => {
    let id: NodeJS.Timer;
    if (bannerChangeFlag) {
      id = setInterval(
        () => setBannerIndex((bannerIndex + 1) % bannerImage.length),
        3000,
      );
  	}

    return () => clearInterval(id);
  }, [bannerIndex, bannerChangeFlag]);

  return (
    <div className="banner">
      {bannerImage.map((image, index) => {
        return (
          <div
            className={`banner__image${
              bannerIndex === index ? " active" : ""
            }`}
            key={index}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
          >
            <a href="" style={{ background: `url(${image})` }}></a>
          </div>
        );
      })}
      <div className="banner__leftBtn" onClick={handleLeftBtnClick}></div>
      <div className="banner__rightBtn" onClick={handleRightBtnClick}></div>
      <ul className="banner__dots">
        {bannerImage.map((image, index) => {
          return (
            <li key={index}>
              <button
                className={bannerIndex === index ? "active" : undefined}
                data-index={index}
                onClick={handleDotClick}
              ></button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

export default Banner;

Banner.scss

.banner {
  position: relative;
  height: 300px;
  min-width: 1200px;
  padding-left: 70px;

  .banner__image {
    position: absolute;
    width: 100%;
    height: 100%;
    display: none;
    animation: bannerOpacity 0.7s;

    a {
      display: block;
      height: 100%;
    }
  }

  .active {
    display: block;
  }

  .banner__leftBtn {
    position: absolute;
    top: 135px;
    left: 150px;
    cursor: pointer;
  }

  .banner__leftBtn:after {
    content: "";
    width: 20px;
    height: 20px;
    border-top: 5px solid #d7d7d7;
    border-right: 5px solid #d7d7d7;
    display: inline-block;
    transform: rotate(225deg);
    transition: 0.2s;
  }

  .banner__leftBtn:hover:after {
    border-top: 5px solid #000;
    border-right: 5px solid #000;
  }

  .banner__rightBtn {
    position: absolute;
    top: 135px;
    right: 80px;
    cursor: pointer;
  }

  .banner__rightBtn:after {
    content: "";
    width: 20px;
    height: 20px;
    border-top: 5px solid #d7d7d7;
    border-right: 5px solid #d7d7d7;
    display: inline-block;
    transform: rotate(45deg);
    transition: 0.2s;
  }

  .banner__rightBtn:hover:after {
    border-top: 5px solid #000;
    border-right: 5px solid #000;
  }

  .banner__dots {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 10px;
    text-align: center;
    padding-left: 70px;

    li {
      display: inline-block;
      margin: 0px 3px;

      button {
        height: 16px;
        border-radius: 8px;
        cursor: pointer;
        background-color: #d7d7d7;
        border-color: #d7d7d7;
      }

      .active {
        background-color: #000;
        border-color: #000;
      }
    }
  }
}

@keyframes bannerOpacity {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

좋은 웹페이지 즐겨찾기