Baehyunki
이미지 슬라이더를 직접 구현해보자

이미지 슬라이더를 직접 구현해보자

이미지 슬라이더는 한 번쯤 만들어보는게 좋습니다.

일반적으로 이미지 슬라이더는 Swiper를 많이 사용합니다.

실무에서 Swiper가 많이 사용되는 이유는 높은 완성도와 함께 다양한 커스터마이징이 가능하기 때문입니다. Swiper는 Vanilla JS에서도 활용 가능하지만, 리액트를 비롯한 프론트엔드 라이브러리/프레임워크와도 잘 어우러지는 훌륭한 라이브러리입니다.

하지만 직접 구현해 봄으로서 CSS 트랜지션과 Javascript를 이용한 애니메이션 효과를 어떤 식으로 활용할 수 있는지 더 깊은 이해를 얻을 수 있습니다.

실제로 구현된 사이트는 아래 링크에서 확인할 수 있습니다.

https://stranger-things-alpha.vercel.app

소스는 아래에서 확인할 수 있습니다.


index.html


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <link rel="icon" type="image/svg+xml" href="favicon.svg"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Stranger Things fanpage</title>
    <meta property="og:type" content="website">
    <meta property="og:image" content="https://stranger-things-alpha.vercel.app/assets/screenshot.png">
</head>
<body>
<main class="container" id="root">
    <header>
        <h1 class="brand">stranger things</h1>
        <button id="info"><i data-feather="info"></i></button>
    </header>
    <article class="card">
        <div class="card-image"></div>
        <div class="carousel">
            <div class="carousel-container"></div>
            <div class="buttons">
                <button id="prev">
                    <svg width="26" height="46" viewBox="0 0 26 46" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path class="button-arrow" d="M24 43.5L4 23L24 2.5" stroke="#E00000" stroke-width="5"/>
                    </svg>
                </button>
                <button id="next">
                    <svg width="26" height="46" viewBox="0 0 26 46" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path class="button-arrow" d="M2 2.5L22 23L2 43.5" stroke="#E00000" stroke-width="5"/>
                    </svg>
                </button>
            </div>
        </div>
    </article>
    <article id="site-info">
        <p>I'm fan of "Stranger things" on Netflix.</p>
        <p>all images and contents are property of Netflix.</p>
        <p>넷플릭스 오리지널 시리즈 "기묘한 이야기" 팬페이지입니다.</p>
        <p>모든 이미지와 콘텐츠는 넷플릭스에 귀속되어있습니다.</p>
        <!--a href="mailto:gloomysight@naver.com">gloomysight@naver.com</a-->
        <button id="close">
            <i data-feather="x"></i>
            close
        </button>
    </article>
</main>
<script type="module" src="/main.js"></script>
</body>
</html>

style.scss

@import "./animation";

:root {
  --width: 1024px;
  --height: 650px;
  --timing: 750ms;
  --radius: 1rem;
}

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

html, body {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background: linear-gradient(180deg, #250909 67.5%, #4F193F 100%) no-repeat;
  height: 100vh;
}


.container {
  width: 100%;
  height: 100%;
  padding-bottom: 4rem;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  overflow: hidden;
  gap: 2rem;
  perspective: 1000px;
  transform-style: preserve-3d;

  header {
    width: 100%;
    max-width: var(--width);
    display: flex;
    justify-content: space-between;
    align-items: center;

    #info {
      outline: 0;
      border: 0;
      background-color: transparent;
      cursor: pointer;
    }

    .feather {
      color: #ff2929;

    }
  }
}

.brand {
  background-image: url('public/assets/Stranger-Things-Logo.webp');
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  text-indent: 100%;
  white-space: nowrap;
  overflow: hidden;
  width: 120px;
  height: 50px;
}

.card {
  width: 100%;
  min-width: 800px;
  max-width: var(--width);
  height: 100%;
  min-height: 600px;
  max-height: var(--height);
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  position: relative;
  z-index: 0;
  border-radius: var(--radius);
  background-color: transparent;
  transform-style: preserve-3d;
  transform: translateZ(-30px);
  transition: transform 0.5s ease-in-out;
  animation: cardOpen 0.25s forwards ease-in-out;
  outline: 1px solid transparent; // aliasing technique - https://stackoverflow.com/questions/6492027/css-transform-jagged-edges-in-chrome

  &:hover {
    transform: translateZ(0px);

    &::before, &::after {
      transform: rotateX(-3deg) translateZ(50px);
    }

    &::before {
      transform: translateX(-50px);
    }

    &::after {
      transform: translateX(50px);
    }
  }

  &::before, &::after {
    position: absolute;
    content: '';
    background-position: center;
    background-repeat: no-repeat;
    background-size: 100%;
    width: 50%;
    height: 50%;
    z-index: 2;
    transition: transform 0.5s ease-in-out;

  }

  &::before {
    left: -250px;
    bottom: -150px;
    background-image: url('public/assets/cloud1.webp');
    animation: cloudAnimation 5s infinite ease-in-out;
  }

  &::after {
    right: -250px;
    bottom: -150px;
    background-image: url('public/assets/cloud2.webp');
    animation: cloudAnimation 7s infinite 1s ease-in-out;
  }


  &-image {
    z-index: 1;
    border-radius: var(--radius);
    width: 100%;
    height: 100%;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center;
    transform-origin: center;
    display: flex;
    align-items: center;


    &.animate {
      animation-name: tweenImage;
      animation-fill-mode: forwards;
      animation-duration: var(--timing);
      animation-timing-function: ease-in-out;
      animation-play-state: running;
    }

  }
}


.character {
  display: flex;
  color: white;
  flex-direction: column;
  width: 300px;
  padding: 1rem;
  margin-left: 2rem;
  z-index: 10;

  &.animate {
    .character-name, .character-desc {
      animation-name: fadeUp;
      animation-fill-mode: forwards;
      animation-duration: calc(var(--timing) / 2);
      animation-timing-function: ease-in-out;
      animation-play-state: running;
      animation-delay: calc(var(--timing) / 2);
    }
  }

  &-name {
    font-size: 3rem;
    text-transform: capitalize;
    margin-bottom: 1rem;

  }

  &-desc {

    line-height: 1.5;
  }
}


.carousel {
  width: 320px;
  position: absolute;
  top: 50%;
  right: -50px;
  transform: translateY(-50%);
  height: 320px;
  overflow: hidden;
  padding-left: 1rem;
  display: flex;
  flex-direction: column;
  z-index: 10;

  &::before {
    position: absolute;
    top: 0;
    right: -1px;
    width: 30px;
    height: 100%;
    background: linear-gradient(90deg, transparent 30%, #250909 70%);
    content: '';
    z-index: 1;
  }

  &-container {
    display: flex;
    width: 100%;
    height: 100%;
    align-items: center;
    padding-bottom: 1rem;
    gap: 1rem;
    transition: all 0.2s ease-in-out;
    transition-delay: 0.1s;
  }

  .buttons {
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-right: 70px;


    button {
      border: 0;
      background: transparent;
      color: white;
      cursor: pointer;
      user-select: none;
      display: flex;
      align-items: center;
      justify-content: center;

      &:focus {
        outline: 0;
      }

      &:hover {
        .button-arrow {
          transform: scale(0.6);
        }
      }

      &:active {
        .button-arrow {
          transform: scale(0.45);
        }
      }

      .button-arrow {
        width: 100%;
        height: 100%;
        transform: scale(0.5);
        transform-origin: center;
        transition: all 0.15s ease-in-out;

        &:active {
          transform: scale(0.45);
        }
      }

      &:hover {

      }
    }

  }

  &-item {
    width: 94px;
    height: 168px;
    display: flex;
    flex: 1 0 auto;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    user-select: none;
    transition: all 0.25s ease-in-out;
    transition-delay: 0.2s;
    perspective: 1000px;
    filter: grayscale(0.8);

    &.active {
      width: 178px;
      height: 256px;
      filter: grayscale(0) drop-shadow(0 3px 10px hsla(0, 30%, 15%, 0.8));
      overflow: hidden;
      outline: 1px solid transparent;
    }

    img {
      transform-style: preserve-3d;
      width: 100%;
      max-width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 0.5rem;
      margin-top: -3px;
    }
  }
}


.dimmed {
  position: absolute;
  z-index: 0;
  width: 100%;
  height: 100%;
  content: '';
  background-color: hsla(255, 40%, 50%, 0.4);
  filter: brightness(10%) contrast(70%);
  top: 0;
  left: 0;
  border-radius: var(--radius);
  transition: all 0.25s ease-in-out;

  &:hover {
    background-color: hsla(255, 40%, 50%, 0);
    filter: brightness(100%) contrast(100%);
  }
}

.flame {
  position: absolute;
  bottom: -30px;
  display: block;
  width: 6px;
  height: 15px;
  background: linear-gradient(180deg, #ff2929 0%, transparent 100%);
  border-radius: 6px;
  filter: blur(1px);
  transform: rotateX(180deg) translateX(-50px) scale(0.5);
  animation: flame;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

#site-info {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  color: white;
  background-color: hsl(0, 100%, 5%);
  padding: 1rem;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  z-index: 1;

  &.active {
    display: flex;
  }

  p {
    line-height: 1.5;
    margin-bottom: 0.5rem;

  }

  a {
    color: white;
    text-decoration: none;
    margin-bottom: 2rem;
    transition: color 0.25s ease-in-out;

    &:hover {
      color: #ff2929;;
    }
  }
}

#close {
  display: flex;
  align-items: center;
  color: white;
  background-color: transparent;
  border: 0;
  outline: 0;
  text-transform: uppercase;
  font-size: 1.5rem;
  cursor: pointer;
  transition: color 0.25s ease-in-out;

  &:hover {
    color: #ff2929;;
  }

}

animation.scss

@keyframes cloudAnimation {
  0% {
    transform: translate3d(0, 0, 0px);
  }
  50% {
    transform: translate3d(0, 15px, 10px);
  }
  100% {
    transform: translate3d(0, 0, 0px);
  }
}

@keyframes cardOpen {
  from {
    transform: scale(0);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }

}

@keyframes tweenImage {
  0% {
    opacity: 0;
    transform: scale(1.125);
    clip-path: inset(280px 90px 300px 1200px round 0.5rem 0.5rem 0.5rem 0.5rem);
  }
  100% {
    opacity: 1;
    transform: scale(1);
    clip-path: inset(0 0 0 0);
  }
}

@keyframes fadeUp {
  0% {
    opacity: 0;
    transform: translateY(50%);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes flame {
  0% {
    transform: rotateX(180deg) translateX(-50px) scale(0.5);
    bottom: -30px;
    opacity: 0;
  }
  50% {
    transform: rotateX(-45deg) translateX(50px) scale(1);
    bottom: 20%;
  }
  100% {
    transform: rotateX(180deg) translateX(-50px) scale(0);
    bottom: 40%;
  }
}

main.js

import './style.scss'
import feather from 'feather-icons'

const cast = [
    {
        name: 'eleven',
        description: "Eleven (Millie Bobby Brown) has never let anything stand in the way of protecting her friends. She’s used to overcoming challenges, but this new chapter finds her navigating the complexities of high school in California — along with the social world that comes with it."
    },
    {
        name: 'mike',
        description: "When we last saw Mike (Finn Wolfhard) in Season 3, he was saying goodbye to Eleven as she moved across the country with the Byers family. The two keep in touch by writing letters, with plans to see each other over spring break... and maybe fight a new evil along the way."
    },
    {
        name: 'lucas',
        description: "Most recently, Lucas (Caleb McLaughlin) and his friends helped save the day by defeating the Mind Flayer at the Starcourt Mall. With exceptional scouting and sleuthing skills, Lucas tries to stay one step ahead of danger in order to protect his buddies. When a darker and more ominous threat lands in Hawkins, he’ll have to be as ready as ever to jump into action."
    },
    {
        name: 'max',
        description: "Max (Sadie Sink) has been through a lot of changes in the past few years, beginning with her move to Hawkins and, most recently, enduring the loss of her stepbrother, Billy (Dacre Montgomery). The fourth season finds her grieving his passing... and uncovering plenty of darkness while trying to avenge his death."
    },
    {
        name: 'dustin',
        description: "While Dustin (Gaten Matarazzo) may be a high school freshman when Season 4 begins, he still plays D&D with his friends... and just might find himself wrapped up in a new adventure inside the mysterious and abandoned Creel House."
    },
    {
        name: 'will',
        description: "There’s no doubt that Will (Noah Schnapp) has been through a lot, what with being lost in the Upside Down and all. He finally made it out of Hawkins with his family, but it’s clear that plenty of horrors await him in sunny California, too."
    },
    {
        name: 'steve',
        description: "After cracking the case of what was lurking beneath the Starcourt Mall, Steve (Joe Keery) is ready for some time away from supernatural horrors while working at the video store alongside Robin. But nothing ever stays calm in Hawkins for long, and we can expect Steve — and his hair — to be pulled into the never-ending battle with the Upside Down once again."
    },
    {
        name: 'robin',
        description: "We first met Robin (Maya Hawke) in Season 3 when she was working at Scoops Ahoy with Steve. Now a fixture of the Demogorgon-slaying crew, Robin teams up with Nancy (finally!) in Season 4 while they investigate the dark secrets of the Creel House."
    },
    {
        name: 'eddie',
        description: "Stranger Things Season 4 introduces fans to Eddie (Joseph Quinn), the leader of the Hellfire Club, the D&D club within Hawkins High School. When our heroes sign up to join, Eddie eventually gets pulled into the supernatural dangers of Hawkins, too."
    },
    {
        name: 'erica',
        description: "You can’t spell America without Erica! Lucas’ little sister (Priah Ferguson) was introduced in Season 2 and quickly rose to fan-favorite status. With her quick wit, snappy comebacks and stealth skills, Erica was a central player in unlocking the secrets of the Starcourt Mall. At the end of Season 3, Dustin and Lucas gave her Will’s old Dungeons & Dragons set, encouraging her to embrace her inner nerd."
    },
    {
        name: 'hopper',
        description: "The end of Season 3 left some major questions about Hopper’s (David Harbour) fate unanswered, but the iconic police chief won’t give up that easily. Season 4 finds him far from home, battling an evil just as deadly as the ones he fought in Hawkins."
    },
    {
        name: 'joyce',
        description: "For as long as we’ve known her, Joyce (Winona Ryder) has stopped at nothing to protect her family. While she might be looking for a fresh start, there are plenty of mysteries — and dangers — that lie ahead."
    },
    {
        name: 'murray',
        description: "Murray (Brett Gelman) was introduced in Season 3 and quickly rose to fan-favorite status. With new threats of evil coming, he’ll have to work alongside Joyce to put a stop to the Upside Down once and for all."
    },
    {
        name: 'jonathan',
        description: "After helping to kill the Mind Flayer at Starcourt Mall, Jonathan (Charlie Heaton) has relocated to the West Coast with his mom, his brother and Eleven. While he might be hoping for a relaxing change of pace from the supernatural horrors of Hawkins, an entirely new danger soon finds him amid the sunshine and palm trees."
    },
    {
        name: 'nancy',
        description: "When we last caught up with Nancy (Natalia Dyer), she was exchanging a tearful goodbye with Jonathan as he moved to California with his family. In the fourth season, she joins forces with Robin to form a truly dynamic duo as they dig up horrific secrets about Hawkins and the Creel House."
    },
    {
        name: 'argyle',
        description: "Argyle (Eduardo Franco) joins the Stranger Things crew in Season 4 as Jonathan’s new best friend. He happily delivers pizzas and enjoys his laid-back Cali lifestyle — but getting mixed up with the Byers family will undoubtedly lead to trouble."
    },
    {
        name: 'karen',
        description: "Nancy and Mike’s mother, Karen (Cara Buono), has been relatively oblivious to the supernatural goings-on of her town — and children. But as darkness closes in on Hawkins, the Wheeler matriarch might be forced to reckon with the nightmarish truth that’s been surrounding her this whole time."
    },
]

const init = (cast) => {
    const carousel = document.querySelector('.carousel-container')
    const cardBg = document.querySelector('.card')
    const dimmedLayer = document.createElement('div')

    dimmedLayer.classList.add('dimmed')
    cardBg.appendChild(dimmedLayer)

    cast.forEach((character, index) => {
        const figure = document.createElement('figure')
        const img = document.createElement('img')

        img.src = `./assets/${character.name}.webp`
        figure.classList.add('carousel-item')
        figure.appendChild(img)
        figure.dataset.id = index
        carousel.appendChild(figure)
        document.querySelector(`[data-id="0"]`).classList.add('active')
    })


}


const actionInit = (cast) => {
    const wrapper = document.getElementById('root')
    const card = document.querySelector('.card-image')
    const cardBg = document.querySelector('.card')
    const carousel = document.querySelector('.carousel-container')
    const images = [...document.querySelectorAll('.carousel-item')]
    const prevButton = document.getElementById('prev')
    const nextButton = document.getElementById('next')
    const infoWrapper = document.createElement('div')
    const name = document.createElement('h2')
    const description = document.createElement('p')
    const infoButton = document.getElementById("info")
    const siteInfo = document.getElementById('site-info')
    const closeButton = document.getElementById('close')
    let imageWidth = 94
    let gap = 16
    let currentPosX = 0
    let currentIndex = 0
    let prevIndex
    let nextIndex
    let length = images.length - 1
    let interval
    let timer = 3500
    let elem
    let count = 0


    const infoLayer = () => {
        if (!siteInfo.classList.contains('active')) {
            return siteInfo.classList.add('active')
        }
        siteInfo.classList.remove('active')
    }
    infoButton.addEventListener("click", () => {
        infoLayer()
    })
    closeButton.addEventListener("click", () => {
        infoLayer()
    })

    const addFlame = () => {
        const flame = document.createElement('span');
        flame.classList.add('flame');
        flame.style.left = `${Math.random() * (window.innerWidth - 1) + 1}px`;
        flame.style.animationDuration = `${Math.random() * (20 - 8) + 8}s`;
        flame.style.animationDelay = `${Math.random() * (10 - 1) + 1}s`;
        flame.style.opacity = `${Math.random()}`;
        wrapper.append(flame);
        if (count < 100) {
            window.requestAnimationFrame(addFlame);
            count++;
        }
    };
    // 목록 이미지 설정
    images.forEach((image, index) => {
        // 각 이미지를 클릭했을 경우
        image.addEventListener('click', (e) => {
            if (length >= index) {
                if (!e.currentTarget.classList.contains('active')) {
                    moveHandler(currentIndex)
                }
            }
        })
    })
    const addIndex = (index) => {
        // console.log(`addIndex에 들어온 index값 = ${index}`)
        currentIndex = +index
        if (currentIndex >= length) {
            currentIndex = 0
            prevIndex = length
            nextIndex = 0
            return

        }
        if (currentIndex === length) {
            return prevIndex = length - 1;
        }
        prevIndex = +currentIndex
        nextIndex = +currentIndex + 1
        currentIndex++
    }
    const minusIndex = (index) => {
        currentIndex = +index
        if (+currentIndex === 0) {
            prevIndex = length
            currentIndex = length
            nextIndex = 0
            return
        }
        if (+currentIndex < 0) {
            currentIndex = 0
            prevIndex = length
            nextIndex = 1
            currentIndex++
            return
        }

        prevIndex = +currentIndex - 1
        nextIndex = +currentIndex;
        currentIndex--
    }

    // 정보 삽입
    const addInfo = (target) => {
        name.innerText = cast[+target].name
        description.innerText = cast[+target].description
    }

    // 노드 삽입
    const insertInfoElement = () => {
        infoWrapper.classList.add('character')
        name.classList.add('character-name')
        description.classList.add('character-desc')
        infoWrapper.appendChild(name)
        infoWrapper.appendChild(description)
        card.appendChild(infoWrapper)
    }

    // 정보 삭제
    const removeInfo = () => {
        name.innerText = ''
        description.innerText = ''
    }


    // 마우스 올렸을 경우 자동 재생 멈춤
    cardBg.addEventListener('mouseover', () => {
        clearInterval(interval)
        interval = null
    })

    // 마우스 hover 해제시 자동 재생 재시작
    cardBg.addEventListener('mouseout', () => {
        autoPlay()
    })

    // 일시정지
    const pauseAutoPlay = () => {
        clearInterval(interval)
        interval = null
        setTimeout(() => autoPlay(), timer)
    }

    // 이전버튼
    prevButton.addEventListener('click', () => {
        minusIndex(currentIndex)
        pauseAutoPlay()
        cardImageHandler(currentIndex)
        backgroundHandler(cardBg, nextIndex)
        animationHandler()
        movePrev(currentIndex)

    })

    // 다음버튼
    nextButton.addEventListener('click', () => {
        addIndex(currentIndex)
        pauseAutoPlay()
        cardImageHandler(currentIndex)
        backgroundHandler(cardBg, prevIndex)
        animationHandler()
        moveNext(nextIndex)
    })

    // 이동 핸들러
    const moveHandler = () => {
        addIndex(currentIndex)
        if (currentIndex >= 0 && length > currentIndex) {
            cardImageHandler(nextIndex)
            backgroundHandler(cardBg, prevIndex)
            animationHandler()
            return moveNext(currentIndex)
        }
        cardImageHandler(currentIndex)
        backgroundHandler(cardBg, prevIndex)
        animationHandler()
        return moveNext(currentIndex)
    }

    // 다음이동
    const moveNext = (currentIndex) => {
        currentPosX = Math.abs(+currentPosX + (imageWidth + gap))
        if (currentIndex === 0) {
            carousel.style.transform = `translateX(0)`
            removeInfo()
            addInfo(nextIndex)
        }
        if (currentPosX > 0) {
            carousel.style.transform = `translateX(-${
                (imageWidth + gap) * currentIndex
            }px)`
            removeInfo()
            addInfo(nextIndex)

        }
    }

    // 이전이동
    const movePrev = (currentIndex) => {
        currentPosX = Math.abs(+currentPosX + (imageWidth + gap))
        if (currentIndex === 0) {
            carousel.style.transform = `translateX(0)`
            removeInfo()
            addInfo(prevIndex)
        }

        if (currentPosX >= 0) {
            carousel.style.transform = `translateX(-${
                (imageWidth + gap) * prevIndex
            }px)`
        }
        removeInfo()
        addInfo(prevIndex)
    }

    // 클래스 리셋
    const resetActiveHandler = (item) => {
        images.forEach((image) => image.classList.remove('active'))
        item.classList.add('active')
    }

    // 현재 배경화면
    const backgroundHandler = (elem, target) => {
        if (target < 0 || target === undefined || target === null) {
            target = 0
        }
        elem.style.backgroundImage = `url('./assets/${
            cast[+target].name
        }@2x.webp')`
    }

    // 카드 이미지 삽입
    const cardImageHandler = (index) => {
        elem = document.querySelector(`[data-id="${+index}"]`)
        resetActiveHandler(elem)
        card.style.backgroundImage = `url('./assets/${
            cast[+index].name
        }@2x.webp')`
    }

    // 애니메이션 핸들러
    const animationHandler = () => {
        card.classList.add('animate')
        infoWrapper.classList.add('animate')
        window.addEventListener('animationend', () => {
            card.classList.remove('animate')
            infoWrapper.classList.remove('animate')
        })
    }


    // 자동재생
    const autoPlay = () => {
        if (!interval) {
            interval = setInterval(moveHandler, timer)
        }
    };

    autoPlay()
    // 첫 배경이미지 설정
    backgroundHandler(card, currentIndex)
    animationHandler()
    insertInfoElement()
    addInfo(currentIndex)
    addFlame();
}
window.addEventListener('DOMContentLoaded', () => init(cast))
window.addEventListener('load', () => {
    feather.replace()
    actionInit(cast)
})

인기 OTT 넷플릭스의 오리지널 컨텐츠에 공개된 이미지를 사용하였으며, 해당 이미지에 대한 저작권은 저에게 없습니다.

0 0 votes
Article Rating
Subscribe
Notify of
guest

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

services

어떤 도움이 필요하신가요?

합리적인 비용으로 최선을 다해 업무를 도와드리겠습니다.
HTML,CSS,Javascript와 연관된 업무들을 도와드립니다.

  • 디자인 소스

  • 웹퍼블리싱