bluegram - 11일차

박상은·2021년 12월 23일
0

🍃 blegram

목록 보기
15/20

게시글 생성을 구현할 때 나중에 적용하기로 하고 넘어갔던 image-carousel을 직접 구현했습니다.

처음에는 react-slick을 적용하려고 했지만, 사용법에 대해 공부하기보다는 원리를 먼저 이해하고 직접 구현해 본 뒤 라이브러리를 사용하는 게 더 좋다고 생각해서 구글링을 통해서 원리를 이해하고 직접 구현해 봤습니다.

  • 본인이 이해한 대로 정리
    1. 제일 처음에는 제일 마지막 이미지를 추가한다.
    2. 제일 끝에는 제일 처음 이미지를 추가한다.
    3. 이미지들을 줄바꿈 하지 않고 가로로 배치한다. ( overflow: hidden )
    4. 우측 버튼을 누르면 transform: translateX(100%)로 이동한다. ( transition 적용 ), ( 좌측은 반대로 )

  • 추가적으로 알아야 할 것
    1. 우측이든 좌측이든 끝에서 버튼을 누를 경우 최초에 추가했던 이미지를 보여준다. ( 위쪽 정리의 1, 2를 한 이유 )
    2. 이미지 이동이 끝나고 난 뒤 transition을 잠시 끄고 처음 이미지로 이동한 뒤 transition을 킨다.

  • 이해를 위한 설명
    이미지가 만약 1, 2, 3이라는 이름으로 있다면 [3-1, 1, 2, 3, 1-1]순으로 배치를 합니다.
    ( 설명을 위해 3-1, 1-1로 표현했습니다. )
    그러고 나서 3에서 우측으로 이동 시 1-1로 이동한다.
    1-1transition이 끝나고 나면 transition을 끄고 1-1에서 1로 이동하고 다시 transition을 켜줍니다.
    그렇게 되면 사용자에게는 변화가 없고 내부적으로 처음인 1로 이동했으므로 무한하게 이미지를 보여줄 수 있게 됩니다.
    반대로 이동하는 경우에도 같은 형식으로 만들면 됩니다.

이후에 추가적으로 이미지 이동을 위한 화살표나 현재 위치를 알려줄 수 있는 변수를 추가해줬습니다.

// 2021/12/23 - image-carousel ( 게시글 읽기 모달 and 게시글 생성 모달에 사용 ) - by 1-blue

import React, { useCallback, useEffect, useRef, useState } from "react";
import Proptypes from "prop-types";

// styled-components
import { Wrapper } from "./style";

const ImageCarousel = ({ children, speed, length, height }) => {
  const wrapperRef = useRef(null);
  const dotRef = useRef(null);
  const [imageNodes, setImageNodes] = useState(null);
  const [dotNodes, setDotNodes] = useState(null);
  const [currentIndex, setCurrentIndex] = useState(1);
  const [click, setClick] = useState(true);

  // 2021/12/23 - 이미지 노드들 배열로 모아서 state에 넣는 함수 - by 1-blue
  useEffect(() => {
    setImageNodes([...wrapperRef.current.childNodes]);
  }, [wrapperRef.current]);

  // 2021/12/23 - 첫 이미지 지정 - by 1-blue
  useEffect(() => {
    imageNodes?.forEach(imageNode => (imageNode.style.transform = `translateX(-${currentIndex * 100}%)`));
    setTimeout(() => {
      imageNodes?.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
    }, 100);
  }, [imageNodes]);

  // 2021/12/23 - 다음 이미지로 넘기는 함수 - by 1-blue
  const onClickNextButton = useCallback(() => {
    if (!click) return;

    // dot 모두 초기화 ( 이전에 이동이 앞인지 뒤인지 알 수 없으니 모두 초기화 )
    dotNodes.forEach(dotNode => (dotNode.style.color = "white"));

    // 이미지 변경
    imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(currentIndex + 1) * 100}%)`));
    setCurrentIndex(prev => (prev + 1 === imageNodes.length - 1 ? 1 : prev + 1));

    // 마지막 이미지에서 다음버튼을 누를 경우 실행
    if (currentIndex + 1 === imageNodes.length - 1) {
      setClick(false);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transition = `all 0s`));
      }, 900);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${1 * 100}%)`));
      }, 1000);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
        setClick(true);
      }, 1010);

      // 현재 이미지와 dot 동기화
      dotNodes[currentIndex - length].style.color = "black";
    } else {
      // 현재 이미지와 dot 동기화
      dotNodes[currentIndex].style.color = "black";
    }
  }, [imageNodes, currentIndex, click, dotNodes, length]);

  // 2021/12/23 - 이전 이미지로 넘기는 함수 - by 1-blue
  const onClickPrevButton = useCallback(() => {
    if (!click) return;

    // dot 모두 초기화 ( 이전에 이동이 앞인지 뒤인지 알 수 없으니 모두 초기화 )
    dotNodes.forEach(dotNode => (dotNode.style.color = "white"));

    imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(currentIndex - 1) * 100}%)`));
    setCurrentIndex(prev => (prev - 1 === 0 ? imageNodes.length - 2 : prev - 1));

    // 첫 이미지에서 이전버튼을 누를 경우 실행
    if (currentIndex - 1 === 0) {
      setClick(false);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transition = `all 0s`));
      }, 250);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(imageNodes.length - 2) * 100}%)`));
      }, 500);
      setTimeout(() => {
        imageNodes.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
        setClick(true);
      }, 510);
      // 현재 이미지와 dot 동기화
      dotNodes[length - 1].style.color = "black";
    } else {
      // 현재 이미지와 dot 동기화
      dotNodes[currentIndex - 2].style.color = "black";
    }
  }, [imageNodes, currentIndex, click, dotNodes, length]);

  // 2021/12/23 - dot 노드들 배열로 모아서 state에 넣는 함수들 - by 1-blue
  useEffect(() => {
    setDotNodes([...dotRef.current.childNodes]);
  }, [dotRef.current]);

  // 2021/12/23 - 첫 이미지와 dot 동기화 - by 1-blue
  useEffect(() => {
    if (!dotNodes) return;
    dotNodes[0].style.color = "black";
  }, [dotNodes]);

  return (
    <Wrapper height={height}>
      {/* 이미지들 */}
      <ul ref={wrapperRef} className="image-container">
        {children}
      </ul>

      {/* 이미지 이동 버튼 */}
      <button type="button" onClick={onClickNextButton} className="next-button">
        {">"}
      </button>
      <button type="button" onClick={onClickPrevButton} className="prev-button">
        {"<"}
      </button>

      {/* 이미지 현재 위치를 표시하는 노드들 */}
      <ul className="dots" ref={dotRef}>
        {Array(length)
          .fill()
          .map((v, i) => (
            <li key={i}></li>
          ))}
      </ul>

      <span className="image-number">{`${currentIndex} / ${length}`}</span>
    </Wrapper>
  );
};

ImageCarousel.propTypes = {
  children: Proptypes.node.isRequired,
  speed: Proptypes.number,
  height: Proptypes.number,
};

ImageCarousel.defaultProps = {
  speed: 1000,
  height: 100,
};

export default ImageCarousel;

마무리

1. 어려웠던 점 및 해결

구현할 때 기능을 구현하는데 어려움보다는 이미지를 배치하는 위치가 마음대로 되지 않아서 시간을 많이 허비했습니다.

또한 transition을 끄고 이미지를 이동시키는데도 자꾸 transition이 적용되는 문제가 발생해서 정확한 원인을 파악하지 못하고 해결을 위해 setTimeout을 이용해서 순차적으로 실행이 되도록 만들었습니다.

또한 currentIndex라는 state를 이용해서 현재 이미지가 무엇인지 판단하도록 만들었는데 currentIndex값에 +1인지 +2인지 어떤 계산을 적용해 줘야 정상적으로 작동할지 너무 헷갈려서 이해보다는 하나하나 적용해 보면서 문제를 고쳐나갔습니다.

0개의 댓글