๐Ÿ’ป Dev/๐Ÿช„ ์›น (web)

[react] ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๋ฌดํ•œ ์Šฌ๋ผ์ด๋“œ ์‡ผ ๊ตฌํ˜„ํ•˜๊ธฐ

Gamddalki 2023. 3. 4. 01:21

 

์ž…๋ง›๋Œ€๋กœ ์Šฌ๋ผ์ด๋“œ ์‡ผ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋‹ค 3์ผ๊ฐ„ ์Œฉ์‡ผํ•œ ๋์— ์–ป์–ด๋‚ธ ์ฝ”๋“œ๋ฅผ ์ž๋ž‘ํ•˜๋Ÿฌ ์™”๋‹ค! ๋•๋ถ„์— ๋ฆฌ์•กํŠธ ๊ณต๋ถ€์— ์•„์ฃผ ํฐ ๋„์›€์ด ๋˜์—ˆ๋Š”๋ฐ, ์ฝ”๋“œ ๋˜ํ•œ ๋‚ด ์ž…๋ง›๋Œ€๋กœ์ด๋ฏ€๋กœ ํ˜น์—ฌ ์ง€์ €๋ถ„ํ•˜๋‹ค๊ณ  ๋Š๋ผ์‹ ๋‹ค๋ฉด ์ทจํ–ฅ ์ฐจ์ด๋‹ค!! ๋†๋‹ด์ž…๋‹ˆ๋‹ค ํ”ผ๋“œ๋ฐฑ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค๐Ÿค—๐Ÿค— + ์—ฌ๋Ÿฌ ๋ถ„๋“ค๊ป˜์„œ ์ž‘์„ฑํ•ด ์ฃผ์‹  ๋ฌดํ•œ ์Šฌ๋ผ์ด๋“œ ์‡ผ ๊ธ€์„ ์ฐธ๊ณ ํ•ด ๊ณต๋ถ€ํ•˜๊ณ  ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค

 

โ˜๐Ÿป ๋ฌดํ•œ ์Šฌ๋ผ์ด๋“œ ์‡ผ ์›๋ฆฌ

๋จผ์ € ์›๋ฆฌ๋ฅผ ์•Œ์•„๋ณด์ž. ์•„๋ž˜์™€ ๊ฐ™์ด ์Šฌ๋ผ์ด๋“œ ์‡ผ๋กœ ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€ 4์žฅ์„ ๊ฐ€๋กœ๋กœ ์ด์–ด ๋ถ™์ธ ๋ฐฐ์—ด์„ ๋งŒ๋“ค์–ด ์ค€๋‹ค.

import bg1 from "../img/1.jpg";
import bg2 from "../img/2.jpg";
import bg3 from "../img/3.jpg";
import bg4 from "../img/4.jpg";

/* bg img Array */
const bgArr = [
  { img: bg1, key: 1 },
  { img: bg2, key: 2 },
  { img: bg3, key: 3 },
  { img: bg4, key: 4 },
];

 

์ด์ œ ๊ทธ ์‹œ์ ˆ TV๋ณผํŽœ์ฒ˜๋Ÿผ ์œ„ ๋ฐฐ์—ด์„ ๋Œ๋ ค์ค„ ๊ฒƒ์ด๋‹ค. ๋ฌดํ•œ ์Šฌ๋ผ์ด๋“œ ์‡ผ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค๊ณ  ๋ฌดํ•œํ•œ ๋ฐฐ์—ด์„ ๋งŒ๋“ค ์ˆœ ์—†์œผ๋‹ˆ ๋ˆˆ์†์ž„์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค! TV๋ณผํŽœ์ฒ˜๋Ÿผ ์•ž์„œ ๋งŒ๋“  ์ด๋ฏธ์ง€ ๋ฐฐ์—ด์„ ๋Œ๋Œ ๋ง์•„์ค€๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์‰ฝ๋‹ค.

์™„๋ฒฝํ•œ ๋ˆˆ์†์ž„์„ ์œ„ํ•ด ์œ„์ฒ˜๋Ÿผ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด์˜ ์•ž, ๋’ค์— ๊ฐ๊ฐ ๋งˆ์ง€๋ง‰ ์ด๋ฏธ์ง€, ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ ๋ถ™์—ฌ์ค„ ๊ฒƒ์ด๋‹ค. ์Šฌ๋ผ์ด๋“œ ์ „ํ™˜ ํšจ๊ณผ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ์ด ๊ณผ์ •์„ ์ƒ๋žตํ•˜๋ฉด ์•ˆ ๋œ๋‹ค! ์ด ์นœ๊ตฌ๋ฅผ ์Šฌ๋ผ์ด๋“œ ๋ฐฐ์—ด๋กœ ๋ถ€๋ฅด์ž.

const BG_NUM = bgArr.length;
const beforeSlide = bgArr[BG_NUM - 1];
const afterSlide = bgArr[0];

let slideArr = [beforeSlide, ...bgArr, afterSlide]; // create slide array (last, origin(first,...,last) ,first) for infinite slide show
const SLIDE_NUM = slideArr.length;

TV๋ณผํŽœ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ์œ„ํ•œ html์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ–ˆ๋‹ค.

const Background = styled.div`
  width: 100%;
  height: 100vh;
  overflow: hidden;
  position: relative;
`;

/* bg img slider */
const SlideBtn = styled.div`
  z-index: 100;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const ImgContainer = styled.div`
  display: flex;
  overflow: hidden;
`;

const ImgBox = styled.div`
  width: 100%;
  height: 100vh;
  overflow: hidden;
  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
`;

return (
    <>
      <Background>
        <SlideBtn
          className="Left"
          onMouseEnter={stopAutoSlide}
          onMouseLeave={intervalHandler}
          onClick={() => slideHandler(-1)}
        >
          <FontAwesomeIcon icon={faChevronLeft} size="4x" />
        </SlideBtn>
        <ImgContainer
          ref={slideRef}
          style={{
            width: `${100 * SLIDE_NUM}vw`,
            transition: "all 500ms ease-in-out",
            transform: `translateX(${
              -1 * ((100 / slideArr.length) * slideIndex)
            }%)`,
          }}
        >
          {slideArr.map((item, index) => (
            <ImgBox key={index}>
              <img src={item.img} />
            </ImgBox>
          ))}
        </ImgContainer>
        <SlideBtn
          className="Right"
          onMouseEnter={stopAutoSlide}
          onMouseLeave={intervalHandler}
          onClick={() => slideHandler(+1)}
        >
          <FontAwesomeIcon icon={faChevronRight} size="4x" />
        </SlideBtn>
      </Background>
    </>);

 

๊ทธ๋Ÿผ ์ด์ œ ๋Œ๋Œ ๋ง์•„๋ณด์ž!

์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ ๋ฐฉํ–ฅ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ๋„˜์–ด๊ฐ€๊ณ  ์žˆ๋‹ค๋ฉด, ์ด๋ฏธ์ง€ 1-2-3-4 (์Šฌ๋ผ์ด๋“œ ๋ฐฐ์—ด[1]-[2]-[3]-[4]) ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‹ค์‹œ ์ด๋ฏธ์ง€ 1 (์Šฌ๋ผ์ด๋“œ ๋ฐฐ์—ด[5]) ์ด ํ™”๋ฉด์— ๋ณด์—ฌ์ง„๋‹ค. ์ด๋•Œ ์ „ํ™˜ ํšจ๊ณผ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ ์Šฌ๋ผ์ด๋“œ ๋ฐฐ์—ด[1]๋กœ ์ˆœ๊ฐ„์ด๋™ํ•œ๋‹ค. ์Šฌ๋ผ์ด๋“œ ๋ฐฐ์—ด[1] ๋˜ํ•œ ์ด๋ฏธ์ง€ 1 ์ด๋ฏ€๋กœ ํ™”๋ฉด์ƒ์œผ๋กœ๋Š” ์•„๋ฌด ๋ณ€ํ™”๊ฐ€ ์—†๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ผ ๊ฒƒ์ด๋‹ค. ์ด์ œ ๋‹ค์‹œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ ๋ฐฉํ–ฅ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์ง„ํ–‰๋  ์ˆ˜ ์žˆ๋‹ค. TV ๋ณผํŽœ์ด ๋œ ๊ฒƒ์ด๋‹ค!!

๋ฐ˜๋Œ€๋ฐฉํ–ฅ ๋˜ํ•œ ๋งˆ์ฐฌ๊ฐ€์ง€์ด๋‹ค.

 

ํŠธ๋žœ์ง€์…˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด 500ms๊ฐ„ ์ž‘๋™ํ•˜๋ฏ€๋กœ 500ms์˜ ๋”œ๋ ˆ์ด๋ฅผ ์ฃผ๊ณ  ์ˆœ๊ฐ„์ด๋™ ์‹œํ‚ค๋ฉฐ, ์ „ํ™˜ํšจ๊ณผ ์‚ญ์ œ ํ›„ ๋ณต๊ตฌ ์‹œ๊ธฐ๋ฅผ ์ˆœ๊ฐ„์ด๋™ ์ดํ›„๋กœ ๊ณ ์ •ํ•ด ์ฃผ๊ธฐ ์œ„ํ•ด ํšจ๊ณผ ๋ณต๊ตฌ๋Š” ์ˆœ๊ฐ„์ด๋™์œผ๋กœ๋ถ€ํ„ฐ 100ms์˜ ๋”œ๋ ˆ์ด๋ฅผ ์ค€๋‹ค.

  const [slideIndex, setSlideIndex] = useState(1);
  const [slideInterval, setSlideInterval] = useState(6000); // slideInterval 6 secs

  const slideRef = useRef<HTMLDivElement>(null);
  
  
  useInterval(() => setSlideIndex((prev) => prev + 1), slideInterval); // auto slide show with slideInterval
  
/* InfiniteSlideHandler attachs last/first imgs with origin last/first imgs to make slide seem infinite */
  const InfiniteSlideHandler = (flytoIndex: number) => {
    setTimeout(() => {
      if (slideRef.current) {
        slideRef.current.style.transition = "";
      }
      setSlideIndex(flytoIndex);
      setTimeout(() => {
        if (slideRef.current) {
          slideRef.current.style.transition = "all 500ms ease-in-out";
        }
      }, 100);
    }, 500);
  };

  if (slideIndex === SLIDE_NUM - 1) {
    // if first img (slide array's last item) -> go to origin first img
    InfiniteSlideHandler(1);
  }

  if (slideIndex === 0) {
    // if last img (slide array's first item) -> go to origin last img
    InfiniteSlideHandler(BG_NUM);
  }
  
  const intervalHandler = () => {
    // when InfiniteSlideHandler works for first img (slide array's last item), control slideInterval to show transition animation normally
    if (slideIndex === SLIDE_NUM - 1) {
      setSlideInterval(500);
    } else {
      setSlideInterval(6000);
    }
  };

 

 

โœŒ๐Ÿป ์Šฌ๋ผ์ด๋“œ ์ปจํŠธ๋กค ๋ฒ„ํŠผ

์œ„ ์ฝ”๋“œ์— ๋“ฑ์žฅํ•˜๋Š” useInterval ํ›…์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

/* useInterval Hook */
interface IUseInterval {
  (callback: () => void, interval: number): void;
}

const useInterval: IUseInterval = (callback, interval) => {
  const savedCallback = useRef<(() => void) | null>(null);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      if (savedCallback.current) {
        savedCallback.current();
      }
    }
    if (interval !== null && interval !== 10000000) {
      let id = setInterval(tick, interval);
      return () => clearInterval(id);
    }
  }, [interval]);
};

๊ธฐ์กด์˜ ํ›… ์ฝ”๋“œ์™€ ์กฐ๊ธˆ ๋‹ค๋ฅธ ์ ์€ interval !== 10000000๋ผ๋Š” ์กฐ๊ฑด๋ฌธ์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค๋Š” ์ ์ด๋‹ค!

๊ทธ ์ด์œ ๋Š” ๋ฐ”๋กœ ์Šฌ๋ผ์ด๋“œ ์ปจํŠธ๋กค๋Ÿฌ ๋ฒ„ํŠผ์„ ๋งŒ๋“ค์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์Šฌ๋ผ์ด๋“œ ์ปจํŠธ๋กค๋Ÿฌ ๋ฒ„ํŠผ์€ ์ž„์˜๋กœ ์ขŒ์šฐ๋ฐฉํ–ฅ ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•œ ๋ฒ„ํŠผ์ด๋‹ค. ํ•˜์–€์ƒ‰ ํ™”์‚ดํ‘œ์ฒ˜๋Ÿผ ๋ฐฐ์น˜ํ–ˆ๋‹ค.

๊ทธ๋Ÿผ ์—ฌ๊ธฐ์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ!!

๋‚ด๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด ์ž„์˜๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ๋„˜์–ด๊ฐ”์Œ์—๋„ ์‹œ๊ฐ„์€ ํ๋ฅธ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.. ์ฆ‰, ์ด๋ฏธ์ง€ 1๋กœ ์ž๋™ ์Šฌ๋ผ์ด๋“œ ๋œ ์‹œ์ ์œผ๋กœ๋ถ€ํ„ฐ 6์ดˆ๊ฐ€ ์ธก์ •๋˜๋ฏ€๋กœ, ํ™”์‚ดํ‘œ ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด 5.9์ดˆ ์‹œ๊ธฐ์— ๋‚ด๊ฐ€ ์ด๋ฏธ์ง€ 3์— ๋„๋‹ฌํ–ˆ๋‹ค๋ฉด 0.1์ดˆ ๋งŒ์— ์ด๋ฏธ์ง€ 4๋กœ ์Šฌ๋ผ์ด๋“œ ๋œ๋‹ค๋Š” ๊ฑฐ๋‹ค!

๊ทธ๋ž˜์„œ ์œ„ ์กฐ๊ฑด๋ฌธ์„ ์ถ”๊ฐ€ํ•ด, ์Šฌ๋ผ์ด๋“œ ์ปจํŠธ๋กค ๋ฒ„ํŠผ ์ƒ์— ๋งˆ์šฐ์Šค๊ฐ€ ์˜ฌ๋ผ๊ฐ€์žˆ๋Š” ๋™์•ˆ์€ setInterval์ด ๋™์ž‘ํ•˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค์–ด ์ž๋™ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋ฉˆ์ถ”์–ด์ฃผ์—ˆ๋‹ค. ๋งˆ์šฐ์Šค๊ฐ€ ๋ฒ„ํŠผ์„ ๋ฒ—์–ด๋‚˜๋ฉด ๋‹ค์‹œ 6์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์ž๋™ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์‹œ์ž‘๋œ๋‹ค.

  /* SlideHandler for buttons */
  const slideHandler = (direction: number) => {
    setSlideIndex((prev) => prev + direction);
  };

  /* stopAutoSlide when controlling slide with buttons */
  const stopAutoSlide = () => {
    setSlideInterval(10000000);
  };

 

 

์œ„์˜ ๋ชจ๋“  ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค!

๐Ÿค˜๐Ÿป ์ „์ฒด ์ฝ”๋“œ

import styled from "styled-components";
import bg1 from "../img/1.jpg";
import bg2 from "../img/2.jpg";
import bg3 from "../img/3.jpg";
import bg4 from "../img/4.jpg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faChevronLeft,
  faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { useEffect, useRef, useState } from "react";

const Background = styled.div`
  width: 100%;
  height: 100vh;
  overflow: hidden;
  position: relative;

  .Left {
    top: 50%;
    left: 3%;
    transform: translate(-50%, -50%);
    color: rgba(235, 235, 235, 0.3);
    &:hover {
      color: rgba(235, 235, 235, 0.5);
    }
  }
  .Right {
    top: 50%;
    left: 97%;
    transform: translate(-50%, -50%);
    color: rgba(235, 235, 235, 0.3);
    &:hover {
      color: rgba(235, 235, 235, 0.5);
    }
  }
`;

/* bg img slider */
const SlideBtn = styled.div`
  z-index: 100;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const ImgContainer = styled.div`
  display: flex;
  overflow: hidden;
`;

const ImgBox = styled.div`
  width: 100%;
  height: 100vh;
  overflow: hidden;
  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
`;

/* bg img Array */
const bgArr = [
  { img: bg1, key: 1 },
  { img: bg2, key: 2 },
  { img: bg3, key: 3 },
  { img: bg4, key: 4 },
];

/* useInterval Hook */
interface IUseInterval {
  (callback: () => void, interval: number): void;
}

const useInterval: IUseInterval = (callback, interval) => {
  const savedCallback = useRef<(() => void) | null>(null);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      if (savedCallback.current) {
        savedCallback.current();
      }
    }
    if (interval !== null && interval !== 10000000) {
      let id = setInterval(tick, interval);
      return () => clearInterval(id);
    }
  }, [interval]);
};

function Home() {
  const [slideIndex, setSlideIndex] = useState(1);
  const [slideInterval, setSlideInterval] = useState(6000); // slideInterval 6 secs

  const slideRef = useRef<HTMLDivElement>(null);

  const BG_NUM = bgArr.length;
  const beforeSlide = bgArr[BG_NUM - 1];
  const afterSlide = bgArr[0];

  let slideArr = [beforeSlide, ...bgArr, afterSlide]; // create slide array (last, origin(first,...,last) ,first) for infinite slide show
  const SLIDE_NUM = slideArr.length;

  useInterval(() => setSlideIndex((prev) => prev + 1), slideInterval); // auto slide show with slideInterval

  /* InfiniteSlideHandler attachs last/first imgs with origin last/first imgs to make slide seem infinite */
  const InfiniteSlideHandler = (flytoIndex: number) => {
    setTimeout(() => {
      if (slideRef.current) {
        slideRef.current.style.transition = "";
      }
      setSlideIndex(flytoIndex);
      setTimeout(() => {
        if (slideRef.current) {
          slideRef.current.style.transition = "all 500ms ease-in-out";
        }
      }, 100);
    }, 500);
  };

  if (slideIndex === SLIDE_NUM - 1) {
    // if first img (slide array's last item) -> go to origin first img
    InfiniteSlideHandler(1);
  }

  if (slideIndex === 0) {
    // if last img (slide array's first item) -> go to origin last img
    InfiniteSlideHandler(BG_NUM);
  }

  const intervalHandler = () => {
    // when InfiniteSlideHandler works for first img (slide array's last item), control slideInterval to show transition animation normally
    if (slideIndex === SLIDE_NUM - 1) {
      setSlideInterval(500);
    } else {
      setSlideInterval(6000);
    }
  };

  /* SlideHandler for buttons */
  const slideHandler = (direction: number) => {
    setSlideIndex((prev) => prev + direction);
  };

  /* stopAutoSlide when controlling slide with buttons */
  const stopAutoSlide = () => {
    setSlideInterval(10000000);
  };

  return (
    <>
      <Background>
        <SlideBtn
          className="Left"
          onMouseEnter={stopAutoSlide}
          onMouseLeave={intervalHandler}
          onClick={() => slideHandler(-1)}
        >
          <FontAwesomeIcon icon={faChevronLeft} size="4x" />
        </SlideBtn>
        <ImgContainer
          ref={slideRef}
          style={{
            width: `${100 * SLIDE_NUM}vw`,
            transition: "all 500ms ease-in-out",
            transform: `translateX(${
              -1 * ((100 / slideArr.length) * slideIndex)
            }%)`,
          }}
        >
          {slideArr.map((item, index) => (
            <ImgBox key={index}>
              <img src={item.img} />
            </ImgBox>
          ))}
        </ImgContainer>
        <SlideBtn
          className="Right"
          onMouseEnter={stopAutoSlide}
          onMouseLeave={intervalHandler}
          onClick={() => slideHandler(+1)}
        >
          <FontAwesomeIcon icon={faChevronRight} size="4x" />
        </SlideBtn>
      </Background>
    </>
  );
}

export default Home;