[react] 무한 자동 슬라이드 만들기 carousel

ony·2022년 11월 28일
1

React

목록 보기
2/13


참고 블로그 원
참고 블로그 투

UI는 굳이 이쁘게 만들 생각이 없었고
원하던 기능만 말해보자면..

구현하고 싶었던 기능 설명

  1. 사진 갯수에 제한 안두고 사진을 보여주고 싶음 -> 사실 몇개가 될지 모름

  2. 사진이동
    1.1. 기본은 사진이 자동으로 넘어가게 할건데
    1.2. 원하면 사진을 직접 넘길수도 있어야 한다

  3. 탭 이동하면 지정해둔 사진 바뀌어야 한다

별거 아니라고 생각했는데 마지막께 처음을 바라보고 있어야 하며
내가 구현한거는 양 옆에 사진 두장도 짜잔하고 보여주고 있어야 하므로

구현해두고도 이게 이렇게 복잡한 게 맞나 싶은 느낌이다.

구현 기술과 관련된 설명은 참고 블로그 원 에서 정말 상세하게 잘 다뤄주고 있기 때문에
나는 생략하도록 하겠다. 😇

Code

사진은 /public/store_image 에 폴더를 나누어서 저장해두었고.

네이밍이 이것밖에 안되는 이유는 ...더보기 우끼끼 🐵

대충 src 내의 경로는 이런 식으로 되어 있다.

레츠고

assets

css 몇개랑 slide 에서 사용할 아이콘 좀 저장해두려고 한다.

1. main.js

/*[main.css]*/

@import "./reset.css";

html,body{
    width: 100%;
    height: 100%;
}

body{
    font-size: 14px;
    color: #333333;
}
*{
    box-sizing: border-box;
    padding: 0;
    margin: 0;
}

*::before,*::after{
    box-sizing: border-box;
}
a{
    text-decoration: none;
    color: inherit;
}

button{
    background-color: transparent;
    border: none;
}

:root{
    --color_main: #3366FF;
}

2. reset.css

/*[reset.css]*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, 
footer, header, hgroup, menu, nav, section {
	display: block;
}
body {
	line-height: 1;
}
ol, ul {
	list-style: none;
}
blockquote, q {
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: '';
	content: none;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}

img{
	max-width: 100%;
	max-height: 100%;
}


button,a{
	cursor: pointer;
}

3. icons 폴더

그냥 이 폴더는 코드 쭉쭉쭉 나열할게요..
링크로 아이콘 가져오는 부분일 뿐이니까...

ic_alert.svg


<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18"><defs><path id="bpnpn3yn0a" d="M7.554 14.813h3.183a1.689 1.689 0 01-3.183 0zm1.592 2.25a2.813 2.813 0 002.812-2.813.563.563 0 00-.562-.563h-7.5c-.31 0-.541-.014-.699-.04.018-.036.04-.077.066-.123.036-.065.354-.605.46-.8.477-.875.735-1.676.735-2.599V6.75c0-2.656 2.057-4.688 4.688-4.688 2.63 0 4.687 2.032 4.687 4.688v3.375c0 .923.258 1.724.736 2.6.106.194.424.734.46.799.026.046.047.087.065.123-.157.026-.389.04-.698.04a.564.564 0 000 1.126c1.263 0 1.896-.221 1.896-1.002 0-.26-.092-.494-.28-.833-.045-.083-.361-.619-.456-.792-.395-.724-.598-1.355-.598-2.061V6.75c0-3.28-2.563-5.813-5.812-5.813S3.333 3.47 3.333 6.75v3.375c0 .706-.203 1.337-.598 2.06-.094.174-.41.71-.456.793-.188.339-.279.572-.279.833 0 .78.632 1.002 1.896 1.002H6.39a2.813 2.813 0 002.756 2.25z"></path></defs><g fill="none" fill-rule="evenodd"><g transform="translate(-1079 -16) translate(224 7) translate(855 9)"><mask id="1dencd96ob" fill="#fff"><use xlink:href="#bpnpn3yn0a"></use></mask><use fill-rule="nonzero" stroke="currentColor" stroke-width=".3" xlink:href="#bpnpn3yn0a"></use><g fill="currentColor" mask="url(#1dencd96ob)"><path d="M0 0H18V18H0z"></path></g></g></g></svg>

ic_arrow.svg

<svg class="SvgIcon_SvgIcon__root__svg__DKYBi" viewBox="0 0 18 18"><path d="m11.955 9-5.978 5.977a.563.563 0 0 0 .796.796l6.375-6.375a.563.563 0 0 0 0-.796L6.773 2.227a.562.562 0 1 0-.796.796L11.955 9z"></path></svg>

ic_new.svg

<svg class="" width="5" height="5" viewBox="0 0 6 6" style=""><g fill="#fff" fill-rule="nonzero"><path d="M6.647 11L6.647 7.259 6.688 7.259 9.158 11 11 11 11 5 9.353 5 9.353 8.357 9.322 8.357 7.089 5 5 5 5 11z" transform="translate(-123 -375) translate(20 365) translate(98 5)" style=""></path></g></svg>

ic_search.svg

<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18"><defs><path id="qt2dnsql4a" d="M15.727 17.273a.563.563 0 10.796-.796l-4.875-4.875-.19-.165a.563.563 0 00-.764.028 5.063 5.063 0 111.261-2.068.562.562 0 101.073.338 6.188 6.188 0 10-1.943 2.894l4.642 4.644z"></path></defs><g fill="current" fill-rule="evenodd"><use fill="#333" fill-rule="nonzero" stroke="#333" stroke-width=".3" xlink:href="#qt2dnsql4a"></use></g></svg>

components/Slider

1. SlideButton.js
슬라이드 버튼 만들어주는 부분

import { ReactComponent as ArrowIcon } from '../../assets/icons/ic_arrow.svg'

export default function SlideButton({ direction, onClick }) {
    return (
        <button onClick={onClick} className={`btn-slide-control btn-${direction}`}>
            <ArrowIcon width="16" height="16" fill="#333" />
        </button>
    );
}

2. Slider.css
슬라이드 관련 css 입혀주는 부분

.slider-area{
    position: relative;
    overflow: hidden;
    height: auto;
}
.slider{
    position: relative;
    display: block;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -ms-touch-action: pan-y;
    touch-action: pan-y;
    -webkit-tap-highlight-color: transparent;
}
.slider-list{
    position: relative;
    overflow: hidden;
    display: block;
    margin: 0;
}

.slider-track{
    position: relative;
    position: relative;
    left: 50%;
    top: 0;
    display: flex;
    flex-direction: row;
    text-align: left;
    width: fit-content;
    /* transition: -webkit-transform 500ms ease 0s; */
    /* transition: transform 500ms ease 0s; */
}

.slider-auto{
}

.slider-item{
    position: relative;
    height: 100%;
    padding: 0 12px;
    float: left;
    -webkit-filter: brightness(50%);
    filter: brightness(50%);
}

.btn-slide-control{
    position: absolute;
    top: calc(50% - 30px);
    padding: 20px 4px;
    z-index: 1;
    background-color: white;
    width: 30px;
    height: 60px;
    opacity: .5;
    border-radius: 15px;
}

.btn-prev{
    transform: rotate(180deg);
    left : calc((100% - 1200px) / 2)
}

.btn-next{
    right: calc((100% - 1200px) / 2);
}

.slider-item div{
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 300px;
    color: white;
    justify-content: center;
    font-size: 60px;
    font-weight: bold;
}
.slider-item span{
    font-size: 18px;
    margin-bottom: 1rem ;
}
.current-slide{
    -webkit-filter: none;
    filter: none;
}

3. Slider.js

내 코드에서는 왜 Slider1 Slider2 로 나뉘어져 있냐면..
사진을 다르게 뿌려주는 방법을 아직 고민 안하고
해당 탭 클릭하면 바로 해당 slider가 바라보는 쪽으로 이동하도록 만들었기 때문

그냥 Slider 기능만 이해해 주세요

import './Slider.css';
import './SliderItem.css';
import React, { useLayoutEffect, useRef, useEffect, useState } from "react";
import SlideButton from './SlideButton'

function useWindowSize() {
    const [size, setSize] = useState([0, 0]);
    useLayoutEffect(() => {
        function updateSize() {
            setSize([window.innerWidth, window.innerHeight]);
        }
        window.addEventListener('resize', updateSize);
        updateSize();
        return () => window.removeEventListener('resize', updateSize);
    }, []);
    return size;
}

function useInterval(callback, delay) {
    const savedCallback = useRef();
    useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

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

function Slider() {
    const [windowWidth, windowHeight] = useWindowSize();
    const items = ['/store_image/WC01/작업지시서1.jpg', '/store_image/WC01/작업지시서2.jpg', '/store_image/WC01/작업지시서3.jpg',]
    const itemSize = items.length;
    const sliderPadding = 40;
    const sliderPaddingStyle = `0 ${sliderPadding}px`;
    const newItemWidth = getNewItemWidth();
    const transitionTime = 500;
    const transitionStyle = `transform ${transitionTime}ms ease 0s`;
    const 양끝에_추가될_데이터수 = 2;
    const [currentIndex, setCurrentIndex] = useState(양끝에_추가될_데이터수)
    const [slideTransition, setTransition] = useState(transitionStyle);
    const [isSwiping, setIsSwiping] = useState(false);
    const [slideX, setSlideX] = useState(null);
    const [prevSlideX, setPrevSlideX] = useState(false);
    let isResizing = useRef(false);

    let slides = setSlides();
    function setSlides() {
        let addedFront = [];
        let addedLast = [];
        var index = 0;
        while (index < 양끝에_추가될_데이터수) {
            addedLast.push(items[index % items.length])
            addedFront.unshift(items[items.length - 1 - index % items.length])
            index++;
        }
        return [...addedFront, ...items, ...addedLast];
    }

    function getNewItemWidth() {
        let itemWidth = windowWidth * 0.9 - (sliderPadding * 2)
        itemWidth = itemWidth > 1060 ? 1060 : itemWidth;
        return itemWidth;
    }

    useEffect(() => {
        isResizing.current = true;
        setIsSwiping(true);
        setTransition('')
        setTimeout(() => {
            isResizing.current = false;
            if (!isResizing.current)
                setIsSwiping(false)
        }, 1000);
    }, [windowWidth])

    useInterval(() => {
        handleSlide(currentIndex + 1)
    }, !isSwiping && !prevSlideX ? 2000 : null)

    function replaceSlide(index) {
        setTimeout(() => {
            setTransition('');
            setCurrentIndex(index);
        }, transitionTime)
    }

    function handleSlide(index) {
        setCurrentIndex(index);
        if (index - 양끝에_추가될_데이터수 < 0) {
            index += itemSize;
            replaceSlide(index)
        }
        else if (index - 양끝에_추가될_데이터수 >= itemSize) {
            index -= itemSize;
            replaceSlide(index)
        }
        setTransition(transitionStyle);
    }

    function handleSwipe(direction) {
        setIsSwiping(true);
        handleSlide(currentIndex + direction)
    }

    function getItemIndex(index) {
        index -= 양끝에_추가될_데이터수;
        if (index < 0) {
            index += itemSize;
        }
        else if (index >= itemSize) {
            index -= itemSize;
        }
        return index;
    }

    function getClientX(event) {
        return event._reactName == "onTouchStart" ? event.touches[0].clientX :
            event._reactName == "onTouchMove" || event._reactName == "onTouchEnd" ? event.changedTouches[0].clientX : event.clientX;
    }

    function handleTouchStart(e) {
        setPrevSlideX(prevSlideX => getClientX(e))
    }

    function handleTouchMove(e) {
        if (prevSlideX) {
            setSlideX(slideX => getClientX(e) - prevSlideX);
        }
    }

    function handleMouseSwipe(e) {
        if (slideX) {
            const currentTouchX = getClientX(e);
            if (prevSlideX > currentTouchX + 100) {
                handleSlide(currentIndex + 1)
            }
            else if (prevSlideX < currentTouchX - 100) {
                handleSlide(currentIndex - 1)
            }
            setSlideX(slideX => null)
        }
        setPrevSlideX(prevSlideX => null)
    }

    return (
        <div className="slider-area">
            <div className="slider">
                <SlideButton direction="prev" onClick={() => handleSwipe(-1)} />
                <SlideButton direction="next" onClick={() => handleSwipe(1)} />
                <div className="slider-list" style={{ padding: sliderPaddingStyle }}>
                    <div className="slider-track"
                        onMouseOver={() => setIsSwiping(true)}
                        onMouseOut={() => setIsSwiping(false)}
                        style={{
                            transform: `translateX(calc(${(-100 / slides.length) * (0.5 + currentIndex)}% + ${slideX || 0}px))`,
                            transition: slideTransition
                        }}>
                        {
                            slides.map((slide, slideIndex) => {
                                const itemIndex = getItemIndex(slideIndex);
                                return (
                                    <div key={slideIndex} className={`slider-item ${currentIndex === slideIndex ? 'current-slide' : ''}`}
                                        style={{ width: newItemWidth || 'auto', height:'auto' }}
                                        onMouseDown={handleTouchStart}
                                        onTouchStart={handleTouchStart}
                                        onTouchMove={handleTouchMove}
                                        onMouseMove={handleTouchMove}
                                        onMouseUp={handleMouseSwipe}
                                        onTouchEnd={handleMouseSwipe}
                                        onMouseLeave={handleMouseSwipe}
                                    >
                                        <a >
                                            <img src={items[itemIndex]} alt={`banner${itemIndex}`} />
                                        </a>
                                    </div>
                                )
                            })
                        }
                    </div>
                </div>
            </div >
        </div >
    );


}

export default Slider;

4. SliderItem.css

.slider-item{
    display: inline-block;
}

.slider-item img{
    width: 100%;
    height: 100%;
    border-radius: 4px;
    object-fit: cover;
    max-height: 300px;
    -webkit-user-drag: none;
    -khtml-user-drag: none;
    -moz-user-drag: none;
    -o-user-drag: none;
    user-select: none;
}

이렇게 하고서

내가 사진 carousel 시키고 싶은 곳에서 임포트 받아서
써주면 됨...

그러면 워크센터1 을 선택했을 때는
썸네일 사진처럼 등록한 작업지시서 사진들이 주루룩 뜨게 되고

워크센터2를 선택하면

이렇게 동그리 사진이 뜨게 된다 !
🐱

여기서 보완해야할 점은 분명 더 있지만
일단 여기서 잠시 스탑하고...

정확한 요구사항이 들어오면 기능 추가하면서 포스팅을 이어나가보도록 하겠다.

예상치도 못했는데
빡세네 이거... 🐵

profile
파이(π)형 개발자 🎐🌿🤍

0개의 댓글