평소처럼 웹 서비스를 이용하는데 문득 '메인 페이지의 배너는 어떻게 개발하는 걸까' 궁금증이 들었어요.
이번 글에서는 제가 배너를 개발한 방식에 대해 얘기해볼게요.
배너를 개발하기 위해서 먼저 다음 두가지에 대해 생각해볼 필요가 있었습니다.
이를 위해서 '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'이 적용되도록 했습니다.
배너 이미지를 자동으로 변경하기 위해서 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;
}
}