(React) 6. ImageSlider

43882 단어 ReactReact

잠깐! 시작하기 전에

이 글은 wecode에서 실제 공부하고, 이해한 내용들을 적는 글입니다. 글의 표현과는 달리 어쩌면 실무와는 전혀 상관이 없는 글일 수 있습니다.

또한 해당 글은 다양한 자료들과 작성자 지식이 합성된 글입니다. 따라서 포스팅이 틀린 정보이거나, 해당 개념에 대한 작성자의 이해가 부족할 수 있습니다.

설명하듯 적는게 습관이라 권위자 발톱만큼의 향기가 날 수 있으나, 엄연히 학생입니다. 따라서 하나의 참고자료로 활용하시길 바랍니다.

글의 내용과 다른 정보나 견해의 차이가 있을 수 있습니다.
이럴 때, 해당 부분을 언급하셔서 제가 더 공부할 수 있는 기회를 제공해주시면 감사할 것 같습니다.


서론

지옥같았던 Carousel 구현입니다.

왜인지 모르겠는데, 처음으로 정말 마음처럼 되는게 아닌 코드였던 것 같습니다.

로직이 복잡하다기보다는 왜 이렇게 되는거야 하는 마음이 더 컸기에, 스트레스를 상당히 받지 않았나 싶습니다.

하마터면 다 포기할뻔 했지만, 시간을 두고 야금야금 고민하다보니 결국 완성은 했습니다.

그런데, Refactoring이 된 코드가 아닌지라 상당히 지저분하고, 아직 사실상 기능적으로 문제가 없는 코드가 아닙니다.

그럼에도 우선은 올려두고, 추후 변경된 로직과 조건들에 대해 확인하고자 1차 완료 코드를 첨부하겠습니다.

branch를 따로 만들까 했지만, 그건 그것대로 상당히 번거로워서 파일 분리의 개념으로 봐주시면 감사하겠습니다.

Carousel ImageSlider

무한히 회전하는 것 같이 보인다고 해서 carousel인가 봅니다.

저는 몰랐는데, 문실님이 그렇게 부른다고 해서 알았습니다.

아무튼, 잡설보다는 오늘은 코드로 말하겠습니다.

ImageSlider.js

import React from 'react';

import './ImageSlider.scss';

class ImageSlider extends React.Component {
  render() {
    const { imageList, imageCounter, imgSize, animation } = this.props;

    const _imageList = imageList.map((image, idx) => {
      const { name, url } = image;

      return (
        <div
          key={idx + 1}
          className={'imgWrap'}
          aria-hidden={idx + 1 === imageCounter ? 'false' : 'true'}
        >
          <img
            alt={name}
            src={url}
            width={imgSize.width}
            height={imgSize.height}
          />
          <div className="imgDescription">
            <span className="imgFlag">신메뉴 오픈</span>
            <span className="imgDescTitle">
              한 여름의 힐링캠핑 어쩌구 저저구 꾸깃 어쩌
            </span>
            <span className="imgDescText">꾸깃이 발행하는 웹 매거진</span>
          </div>
        </div>
      );
    });

    const {
      handleClick,
      buttonRender,
      buttonWrapClassName,
      buttonClassName,
      buttonText,
    } = this.props;

    const button = buttonRender => {
      if (buttonRender) {
        return (
          <div className={buttonWrapClassName}>
            <button className={buttonClassName} onClick={handleClick}>
              {buttonText.left}
            </button>
            <button className={buttonClassName} onClick={handleClick}>
              {buttonText.right}
            </button>
          </div>
        );
      }
    };
    console.log(imageCounter);

    return (
      <div className="imgSliderWrap">
        <div className="imgSlider" style={animation}>
          <div
            key={-1}
            className="imgWrap"
            aria-hidden={imageCounter === -1 ? 'false' : 'true'}
          >
            <img
              alt={imageList[imageList.length - 1].name}
              src={imageList[imageList.length - 1].url}
              width={imgSize.width}
              height={imgSize.height}
            />
          </div>
          {_imageList}
          <div
            key={imageList.length}
            className="imgWrap"
            aria-hidden={imageCounter === imageList.length ? 'false' : 'true'}
          >
            <img
              alt={imageList[0].name}
              src={imageList[0].url}
              width={imgSize.width}
              height={imgSize.height}
            />
          </div>
        </div>
        {button(buttonRender)}
      </div>
    );
  }
}

export default ImageSlider;

뭔가 이것저것 참 많지만,

  1. 버튼을 구현할것인가?

  2. style을 전달할것인가?

  3. animation을 변경할 수 있는가?

정도로 구성된 것 같습니다.

사실 위 코드는 좌, 우 무한 슬라이드의 코드이기 때문에 일부 수정되어야 할 것 같습니다.

이제, 해당 슬라이드를 적용하는 컴포넌트의 코드를 봅시다.

Main.js

import React from 'react';
import ImageSlider from './Asides/ImageSlider';

import './Main.scss';

class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      imageCounter: 0,
      animation: {
        transform: 'translateX(+4800px)',
        transition: '',
      },
      animationInfo: {
        slideX: 0,
        time: `2000ms`,
      },
    };
  }

  setImageCounter = (e, arr) => {
    this.setState(prevState => {
      const { imageCounter } = prevState;
      const newState = { ...prevState };

      let slideX = 0;
      let time = 2000 + `ms`;
      let transition = `all ${time} cubic-bezier(0, 0.71, 0.58, 1)`;

      if (e.target.innerText.includes('<') && imageCounter > -1) {
        newState.imageCounter = imageCounter - 1;
        slideX = -6720 + (arr.length - imageCounter + 1) * 1920;

        return {
          ...newState,
          animation: {
            transform: `translateX(${slideX + 'px'})`,
            transition,
          },
          animationInfo: {
            slideX,
            time,
          },
        };
      }

      if (e.target.innerText.includes('>') && imageCounter < arr.length) {
        newState.imageCounter = imageCounter + 1;
        slideX = 4800 - (imageCounter + 1) * 1920;

        return {
          ...newState,
          animation: {
            ...newState.animation,
            transform: `translateX(${slideX + 'px'})`,
            transition,
          },
          animationInfo: {
            slideX,
            time,
          },
        };
      }

      return newState;
    });
  };

  componentDidUpdate() {
    const { imageCounter, animation, animationInfo } = this.state;
    if (imageCounter === -1) {
      setTimeout(() => {
        this.setState(prevState => {
          let transition = '0ms';
          let slideX = -4800;
          let transform = `translateX(${slideX}px)`;
          let newImageCounter = imageCounter + IMAGE_LIST.length;
          console.log(slideX);

          return {
            ...prevState,
            imageCounter: newImageCounter,
            animation: { ...animation, transform, transition },
            animationInfo: { ...animationInfo, slideX },
          };
        });
      }, parseInt(animationInfo.time, 10));
    }
    if (imageCounter === 6) {
      setTimeout(() => {
        this.setState(prevState => {
          let transition = '0ms';
          let slideX = 4800;
          let transform = `translateX(${slideX}px)`;
          let newImageCounter = 0;
          console.log(slideX);

          return {
            ...prevState,
            imageCounter: newImageCounter,
            animation: { ...animation, transform, transition },
            animationInfo: { ...animationInfo, slideX },
          };
        });
      }, parseInt(animationInfo.time, 10));
    }
  }

  render() {
    const { setImageCounter } = this;
    const { imageCounter } = this.state;
    const { animation } = this.state;

    const buttonText = { left: '<', right: '>' };
    const imgSize = { width: '1920px', height: '640px' };

    console.log(this.state);
    return (
      <section className="mainContainer">
        <div className="mainWrap">
          <ImageSlider
            imageList={IMAGE_LIST}
            imageCounter={imageCounter}
            handleClick={e => {
              setImageCounter(e, IMAGE_LIST);
            }}
            buttonRender={true}
            buttonWrapClassName="moveButtonWrap"
            buttonClassName="moveButton"
            buttonText={buttonText}
            imgSize={imgSize}
            animation={animation}
          ></ImageSlider>
      </section>
    );
  }
}

export default Main;

const IMAGE_LIST = [
  { name: 'stake', url: '/images/Main/stake.jpg' },
  { name: 'stake', url: '/images/Main/food2.jpg' },
  { name: 'stake', url: '/images/Main/food3.jpg' },
  { name: 'stake', url: '/images/Main/stake.jpg' },
  { name: 'stake', url: '/images/Main/food2.jpg' },
  { name: 'stake', url: '/images/Main/food3.jpg' },
];

어유, 참 길기도 합니다.

보시다시피, 위 ImagSlider.js 의 경우, 공용 컴포넌트로 분리해야 할 것 같은 사항이 좀 있어 분리했습니다.

그러다보니 해당 컴포넌트에서 애니메이션을 직접 정의해서 보내고 있기에 좀 길어졌습니다.

애니메이션은 다음과 같이 구성됩니다.

(onClick event method)

  1. 좌, 우 조건

  2. imageCounter는 실제 배열 내 이미지들의 index를 나타냅니다.

  3. 좌, 우 버튼 클릭 시 imageSlider 전체가 좌, 우로 translateX 를 통해 움직입니다.

  4. 버튼클릭마다 해당 translateX의 좌표값과 animation 으로 전달되는 state가 수정됩니다. -> props 갱신에 따라 ImageSlider 컴포넌트도 다시 렌더

  5. CDU를 통해 업데이트가 완료되면, setTimeOut()을 활용한 비동기 setState를 진행합니다.

  6. animationInfo.time 에 해당하는 값의 시간 뒤, -1, 6에 해당하는 fake image는 애니메이션이 없는 상태로 각각 5, 0 번의 이미지의 좌표값으로 이동합니다.

  7. 다시 버튼을 클릭하면, 5번, 0번 이미지에 해당하는 이벤트 handle이 가능합니다.

이런 구조로 무한히 회전하는 slide를 구성했는데, 말씀드렸듯 공용 컴포넌트화 하기 위해 손봐야 할 곳이 많습니다.

state로 관리하지 않아도 될 부분은 확실히 없는지 체크해야겠네요.

마치며

해당 코드는 복습에서 사용할 예시입니다.

즉, 완성작이 아니므로, 언제든 바뀐 코드를 가지고 올 수 있습니다.

조금 부족한 코드지만, 사용하실 분들은 언제든 참고하셔도 좋습니다.

이상으로 글을 마치겠습니다. 읽어주셔서 감사합니다.

좋은 웹페이지 즐겨찾기