메인페이지 애니메이션 구현

seul_velog·2022년 5월 24일
2
post-thumbnail

✍️ 1차 프로젝트에서 내가 맡은 Main Page의 애니메이션 기능을 구현하면서 기록한 메모들

메인 상단 캐러셀

📌 구현목표
1. 하단 버튼 클릭시 인덱스에 따라서 해당하는 배경이미지로 보여주기
2. 마지막 인덱스에 도달하고 다시 처음 인덱스로 돌아와서 무한 자동재생 구현하기
3. 바뀌기 전 후 이미지가 자연스럽게 대체되도록 구현하기
+) 메인페이지 캐러셀 클릭시 해당 아이템 상세 페이지로 이동



하단 버튼 클릭시 배경이미지 변경하기

✍️ 접근 방법 : onClick 했을때 그 인자값(아이디) 를 전달해서 그 아이디에 맞는 이미지를 보여주기


const [imgList, setImgList] = useState([]);
const [curImg, setCurImg] = useState(0);

const moveToImg = targetNum => {
    setCurImg(targetNum);
  };

  return (
    <section className="carousel">
      <div className="carouselWrapper">
        {imgList.map(imgData => {
          return (
            <CarouselImg key={imgData.id} imgData={imgData} curImg={curImg} />
          );
        })}
      </div>
      <div className="carouselBtns">
        <CarouselImgBtn curImg={curImg} moveToImg={moveToImg} />
      </div>
    </section>
  );
};


const CarouselImgBtn = ({ curImg, moveToImg }) => {
  return (
    <>
      <span
        className={`btn ${curImg === 0 ? 'active' : null}`}
        onClick={() => moveToImg(0)}
      />
      <span
        className={`btn ${curImg === 1 ? 'active' : null}`}
        onClick={() => moveToImg(1)}
      />
      <span
        className={`btn ${curImg === 2 ? 'active' : null}`}
        onClick={() => moveToImg(2)}
      />
      <span
        className={`btn ${curImg === 3 ? 'active' : null}`}
        onClick={() => moveToImg(3)}
      />
    </>
  );
};
const CarouselImg = ({ imgData, curImg }) => {
  return (
    <div className="carouselImg">
      {imgData.id === curImg ? (
        <>
          <img src={`${imgData.image}`} alt="mainToyImg" className="active" />
          <div className="itemDesc">
            <h3>BEST</h3>
            <span className="title">제품이름</span>
            <span className="titleEn">item name</span>
          </div>
        </>
      ) : null}
    </div>
  );
};
  • imgList를 json 목데이터를 통해 불러온다.
  • map을 통해서 각각의 배경 이미지를 보여주도록 한다.

✍️ 여기서 모든 이미지가 한꺼번에 보여지면 세로로 사진 개수 만큼 쌓이게 될 것이다.
기본값으로 0번째 사진을 보여주고, 하단 이미지버튼을 클릭함에 따라서 인덱스에 맞는 사진을 보여주도록 하자.

1) curImg 변수를 선언한다.

2) 해당 버튼을 클릭 했을 때의 값을 이벤트로 전달해서 moveToImg 함수에 인자로 전달, setCurImg로 현재 이미지 넘버를 변경한다.

3) 해당 넘버를 <CarouselImg> 컴포넌트로 전달한다.

4) 컴포넌트에서는 json으로 받아온 전체 이미지 데이터중 데이터의 id값과 부모로부터 받은 props의 curImg 즉, onClick 을 통해서 받아온 넘버와 일치하는지를 삼항연산자를 통해 확인한다.

5) 맞다면 UI에 맞게 렌더링 하도록 한다. 이때 이미지 src는 imgData의 image를 참조하면 원하는 이미지만 맞게 불러올 수 있다.



✍️ 기억하고 싶은 코드리팩토링

캐러셀 배경이미지 전환을 위한 하단 버튼 클릭 파트 (리팩토링 전)
→ 이부분은 이미지가 많아지면 그만큼 다시 수정을 해야해서, 유지보수 측면에서 봤을 때는 비효율적일 것 같았다.

const CarouselImgBtn = ({ curImg, moveToImg }) => {
  return (
    <>
      <span
        className={`btn ${curImg === 0 ? 'active' : null}`}
        onClick={() => moveToImg(0)}
      />
      <span
        className={`btn ${curImg === 1 ? 'active' : null}`}
        onClick={() => moveToImg(1)}
      />
      <span
        className={`btn ${curImg === 2 ? 'active' : null}`}
        onClick={() => moveToImg(2)}
      />
      <span
        className={`btn ${curImg === 3 ? 'active' : null}`}
        onClick={() => moveToImg(3)}
      />
    </>
  );
};

📌 Key!

new Array(4).fill().map()

참고한 코드 ▼

console.log(new Array(4).map((_, idx) => idx + 1));

혹은 Array.from 을 활용할 수도 있다. Array.from 의 경우, 콘솔로 찍어보면 array.form 은 0,1,2,3 식으로 나왔는데, 다음엔 이 방법으로도 해보자! 😀


✍️ 리팩토링 후

const CarouselImgBtn = ({ currentImage, moveToImage }) => {
  return (
    <>
      {new Array(4).fill().map((_, i) => (
        <span
          key={i}
          className={`btn ${currentImage === i ? 'active' : null}`}
          onClick={() => moveToImage(i)}
        />
      ))}
    </>
  );
};





transition 효과 부여하기

✍️ 변경 전 코드

  const [curImg, setCurImg] = useState(0);
  const [fadeIn, setFadeIn] = useState(true);

  const moveToImg = targetNum => {
    setFadeIn(prev => !prev);
    setTimeout(() => {
      setFadeIn(prev => !prev);
      setCurImg(targetNum);
    }, 400);
  };
&.hidden {
  opacity: 0.3;
  transition: opacity 800ms ease-out;
}

&.active {
  opacity: 1;
  transition: opacity 1000ms ease-in;
}
const CarouselImg = ({ imgData, curImg, fadeIn }) => {
  return (
    <div className={`carouselImg ${fadeIn === true ? 'active' : 'hidden'}`}>
      {imgData.id === curImg ? (
        <>
          <img src={`${imgData.image}`} alt="mainToyImg" className="active" />
          <div className="itemDesc">
            <h3>BEST</h3>
            <span className="title">{imgData.name}</span>
            <span className="titleEn">{imgData.price}</span>
          </div>
        </>
      ) : null}
    </div>
  );
};
  • 버튼 클릭시 hiddenactive 를 부여한다.
  • scss에 속성값을 설정한 뒤 클릭된 버튼에만 fadeIn 효과를 주거나 제거한다.
  • 처음 생각했을 때는 hidden 효과와 active 효과를 setTimeout 으로 시간차를 둬서 같이 발생되면 트랜지션 효과가 나올 거라고 생각했다. 🤔
  • 위 영상처럼 작동이 되는 것 처럼 보이지만 사진이 튀어나오는 것 같은 (끊김)현상이 발생❗️

문제점
1. hidden 상태의 경우 opacity: 0, active 상태의 경우 opacity:1 으로 state 변화에 따라 해당 사진이 바로바로 교체되기 때문에 사진이 끊기는 듯한 현상이 생기는 것 같았다.
2. setInterval 을 하면 아예 transition 이 작동 되지 않았다.

Elements 탭에서 검사를 해보니 모든 그림에 hidden 혹은 active 가 되어 있어서 그런것 같다.


✍️ 해결 방안 모색
배경 사진들을 같은 위치에 놓고 active 상태의 사진만 오퍼시티를 1로 설정, 나머지 사진은 같은 위치에서 오퍼시티를 0으로 설정한다.
이때 수정 전 fadein 으로 true false 값을 따로 줄 필요 없다. Map을 돌리면 전체가 true 혹은 전체가 false가 되므로 이렇게 구현하려면 CarouselImg 컴포넌트 안에서 로직을 구현하는 것이 맞다. 우선 이 로직에서는 필요하지 않은 부분이므로 제거하자.
활성화 된 것만 active, 그외 나머지는 hidden 으로 되도록 다시 수정하자.
( 아래처럼 클래스네임만 위치를 잘 설정하면 해결! )

 <div
      className={`carouselImg ${imgData.id === curImg ? 'active' : 'hidden'}`}
    >

수정 후 ▼



setInterval로 무한 자동재생 시키기

문제점
1. 렌더링 초과 메세지
curImg의 값을 현재 useEffect 외부에서 작성했기 때문에 setState 를 수정하면 (if 문으로 curImg < imgList.length - 1 일 때 setCurImg를 설정... 동일하면 0으로 setState를 설정...) 계속 렌더링되면서 이 함수가 수행되므로 콘솔에 렌더링 초과 메세지가 뜬다.


✍️ 해결 방안 모색

▶ 멘토님 조언으로 해당 로직을 setInterval 안에 넣는 것이 좋을 것 같다고 생각했다! 그런데 이 안에 어떻게 if 문을 작성하지? 삼항연산자로? 🤔
여러번 시도끝에 아래와 같이 setState 함수에서 if 문을 작성할 수 있다는 것, 그것을 return값으로 할당할 수 있다는 것을 알았다.😀

const [curImg, setCurImg] = useState(0);

...

useEffect(() => {
    const timer = setInterval(() => {
      if (curImg < imgList.length - 1) {
        return setCurImg(curImg => curImg + 1);
      } else if (curImg === imgList.length - 1) {
        return setCurImg(0);
      }
    }, 3000);
    return () => clearInterval(timer);
  }, []); 



문제점
2. 페이지를 새로고침했을때 슬라이드가 자동재생 되지 않는다.


✍️ 해결 방안 모색

  useEffect(() => {
    const timer = setInterval(() => {
      if (curImg < imgList.length - 1) {
        return setCurImg(curImg => curImg + 1);
      } else if (curImg === imgList.length - 1) {
        return setCurImg(0);
      }
    }, 1000);
    return () => clearInterval(timer);
  }, [curImg, imgList.length]); // 값을 넣어줌 

useEffect() 에서 deps 에 값을 넣어 주었다.
curImg 로 넘어갈때마다 렌더링, 그리고 imgList 로 처음 렌더링을 감지한다.


아래는 위의 코드를 한번 더 리팩토링한 코드이다.✨

 useEffect(() => {
    const timer = setInterval(() => {
      setCurrentImage(currentImage =>
        currentImage < imgList.length - 1 ? currentImage + 1 : 0
      );
    }, 3000);
    return () => clearInterval(timer);
  }, [currentImage, imgList]);



+) 메인페이지 캐러셀 클릭시 해당 아이템 상세 페이지로 이동

  const navigate = useNavigate();
  const goToDetail = () => {
    navigate(`/itemdetail/${imgData.id}`);
  };

문제점
클릭하면 항상 마지막 사진의 값만 url로 전달된다.

✍️ 해결 방안 모색
어떻게 해결하지? 아마 모든 사진이 position: absolute 이므로 마지막 사진이 가장 위에 있고, opacity:0 이라서 선택이 되는 것 같다고 생각했다. (display: none 이 아니기 때문. 그런데 이경우 애니메이션을 주지 못한다고 알고있다.)

❗️ 해결해보자

  • useRef 사용하기
  useEffect(() => {
    carousellRef.current.style['z-index'] = '1';
  }, []);

  const carousellRef = useRef('');
  
  ...
   <img
        src={`${imgData.url}`}
        alt="mainToyImg"
        className="active"
        onClick={goToDetail}
        ref={carousellRef}
      />

클릭하면 해당 아이템에 맞는 상세 페이지로 이동된다 😀 !!!

새로운 이슈사항 : transition 500ms로 이동할 동안의 잠깐 사이에 클릭하면 역시 마지막 인덱스만 클릭된다. 이 부분은 더 생각해보자! 🔨





메인 페이지 하단 무한슬라이드 구현

📌 구현목표
1. 좌우 버튼 클릭시 인덱스에 따라서 좌우로 이동하기
2. 무한 슬라이더 구현하기
3. 자연스러운 전환효과 구현하기



✍️ 첫 시도

const Instagram = () => {
const [imgList, setImgList] = useState([]);
const [curIndex, setCurIndex] = useState(4);

const TATAL_SLIDES = 3;
const transitionTime = 500;
const transitionStyle = `transform ${transitionTime}ms ease-in-out`;
const [transition, setTransition] = useState(transitionStyle);

const slideRef = useRef('');
const replaceSlide = () => { 
    setCurIndex(8);
    setTimeout(() => {
      setTransition('');
      setCurIndex(4);
    }, transitionTime);
  };

  useEffect(() => { // useRef를 통해서 transition 값을 할당
    slideRef.current.style.transition = transition;
    slideRef.current.style.transform = `translateX(-${curIndex * 25}%)`;
  }, [curIndex]);

// 정방향 이동함수
const moveToNextSlide = () => {
    if (curIndex > TATAL_SLIDES * 2) {
      replaceSlide();
    } else {
      setCurIndex(curIndex => curIndex + 1);
      setTransition(transitionStyle); // 1루프 후 다시 애니메이션 효과를 주도록
    }
  };

  return (
    <div className="instagram">
      <h2 className="title">INSTAGRAM</h2>
      <div className="description">#토위 #TOWE STORY #TOWE #TOY SHOP #REGO</div>
      <div className="slider">
        <div className="slide" ref={slideRef}> // useRef사용
         ...
  );
};

export default Instagram;

정방향으로 슬라이드가 진행되지만 반대방향으로 진행하면 애니메이션이 이어지지는 않는다.
반대방향도 구현해보자 🤔


 const TOTAL_SLIDES = 4; 
  const transitionTime = 500;
  const transitionStyle = `transform ${transitionTime}ms ease-in-out`;
  const [transition, setTransition] = useState(transitionStyle);
  
   useEffect(() => {
    slideRef.current.style.transition = transition;
    slideRef.current.style.transform = `translateX(-${curIndex * 25}%)`;
  }, [curIndex]);
  

  const slideRef = useRef('');

  const replaceSlide = () => {
    setCurIndex(TOTAL_SLIDES * 2);
    setTimeout(() => {
      setTransition('');
      setCurIndex(TOTAL_SLIDES);
    }, transitionTime);
  };

  const replaceReverseSlide = () => {
    setCurIndex(0);
    setTimeout(() => {
      setTransition('');
      setCurIndex(TOTAL_SLIDES);
    }, transitionTime);
  };


  const moveToNextSlide = () => {
    if (curIndex > TOTAL_SLIDES * 2 - 2) {
      replaceSlide();
    } else {
      setCurIndex(curIndex => curIndex + 1);
      setTransition(transitionStyle);
    }
  };

  const moveToPrevSlide = () => {
    if (curIndex === 1) {
      replaceReverseSlide();
    } else {
      setCurIndex(curIndex => curIndex - 1);
      setTransition(transitionStyle);
    }
  };

📌 slider (하단) 슬라이드 작동방식

1) 필요한 이미지리스트 *3 개수만큼 준비한다.

2) 화살표 아이콘을 선택했을 때 moveToPrevSlide 함수 혹은 moveToNextSlide가 호출된다.

3) curIndex 라는 state를 만들고 현재 위치한 이미지의 기준을 잡는다.

4) 정방향 이동의 경우 curIndex가 전체 리스트 *2 보다 클 경우 처음으로 되돌아가도록 replaceSlide() 함수를 호출한다.

5) replaceSlide 함수가 하는일

  • 다음(혹은 이전) 이미지를 보여주고 나서 비동기적으로 setTimeout()의 함수도 같이 동작할 수 있도록 한다.
  • setTimeout()를 통해 transition의 ms만큼 대기한 뒤 트랜지션 효과 없이 다시 처음 기준 위치로 돌아갈 수 있도록 한다.

6) 위와같은 동작이후 다시 transition 효과를 넣는다.

정말 많은 시행착오 끝에 종류가 다른 무한 캐러셀들을 구현해보았다. 👏👏
( 하드 코딩된 부분을 물어물어 리팩토링하려고 노력했지만 .. 우선은 더 중요하고 급한 기능 구현부터 마무리 하기로 ..! )





스크롤 이벤트 애니메이션 구현하기

📌 시도
window.addEventListener를 사용해보자.

📌 문제점 예측
1) 스크롤 길이가 달라지면?
2) 계속해서 이벤트스크롤 이벤트가 발생되는 점 ? 여기는 cleanup으로 해결이 안될까? 🤔 (되더라도 엄청 잦을 것 같다.)
→ 이때 IntersectionObserver 를 사용해보면 어떨까?

우선 window.addEventListener 를 통해서 시도해보자. (잘 작동한다!! 😙 )

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
const Main = () => {
  const [scrollFadeIn, setScrollFadeIn] = useState(false);
  const [scrollFadeIn2, setScrollFadeIn2] = useState(false);
  const [scrollFadeIn3, setScrollFadeIn3] = useState(false);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
  const handleScroll = () => {
    window.scrollY >= 1300 ? setScrollFadeIn(true) : setScrollFadeIn(false);
    window.scrollY >= 2800 ? setScrollFadeIn2(true) : setScrollFadeIn2(false);
    window.scrollY >= 4000 ? setScrollFadeIn3(true) : setScrollFadeIn3(false);
  };
  
  return (
    <div className="main">
      <Carousel />
      <BestProducts />
      <OurStory scrollFadeIn={scrollFadeIn} />
      <WallPaper scrollFadeIn2={scrollFadeIn2} />
      <Instagram scrollFadeIn3={scrollFadeIn3} />
    </div>
  );

✍️ 스크롤 이벤트 개선사항 memo !
: useRef로 각 컴포넌트의 위치를 먼저 파악하고 그 높이에 도달했을 때 fadeIn 효과를 주는 방향으로 리팩토링을 해보면 어떨까? + 스테이트 값 하나로도 리팩토링(진행중) 해볼 수 있겠다. :)




reference
peppermint100
jujusnake
https://nohack.tistory.com/126
intrepidgeeks
JavaScript에서 배열을 채우는 4가지 방법
Array.prototype.fill()
Array() constructor

profile
기억보단 기록을 ✨

0개의 댓글