[FIFAPulse] 개발기록 - 로직을 진행하는데 의존성을 줄여야 하는 이유( react-slick 라이브러리 상태 업데이트 오류 해결하기 )

조민호·2023년 7월 5일
0


문제 상황

각 매치의 세부 통계를 보여주는 페이지가 있고,

여기에 패스 , 슛 , 수비 등등의 세부 통계를 보여주는 컴포넌트들이 존재하는데

여기서 슛 정보를 보여주는 컴포넌트에 슈팅 정보도 있지만 나 혹은 상대방이 득점했었던

골 정보를 보여주는 부분이 있다


피파온라인4에는 각 매치별로 전체 슈팅에 대한 데이터만 제공해 주는데

여기서 내가 득점이 된 슈팅만 따로 필터링을 해서

아래와 같이 데이터를 따로 추출을 했고

const myGoalData =
[	
	// 첫번째 골 데이터
	{
    assist: true,
    assistSpId: 272037576,
    assistX: 0.553772509098053,
    assistY: 0.3725910186767578,
    goalTime: 496,
    hitPost: false,
    inPenalty: true,
    result: 3,
    spGrade: 6,
    spId: 246189505,
    spIdType: true,
    spLevel: 4,
    type: 1,
    x: 0.92109727859497,
    y: 0.4674257934093475,
  },
	{...}, 	// 두번째 골 데이터
	{...}, 	// 세번째 골 데이터
]

위의 데이터를 바탕으로

  • 슈팅 좌표
  • 도움 좌표
  • 비거리
  • 슈팅 종류 등에 대해서

각 득점에 대한 데이터를 시각화해서 보여주려고 했다



원래 내 생각은 , 모든 골들을 한번에 보여주는 것 보다

1개의 골들을 차례로 선택하면 각 골에 대한 정보를 하나씩 보여주는 것이 좋다고 생각했다

  1. 그렇다면 골이 들어간 시간들을 나열해 주고

  2. 이 시간들을 클릭하면 각 골에 대한 정보를 보여줄 수 있게 하려고 했다


(각 시간대를 클릭하면 해당 골 정보를 보여줌)

나                               상대
32:43 43:03 54:51 93:49          27:58 44:23

여기서 기능 구현 자체는 별 무리가 없었지만 css가 문제였다

누구는 골을 1골만 넣을 수 있지만 상대방은 10골도 넣을 수 있는 것이 축구이므로
단순히 li태그로 모든 시간들을 한번에 나열 했다간 불균형으로 인해 UI가 어긋날 수 있었다

물론 나름대로 수정을 거듭하면 해결책 자체는 매우 많겠지만 그래도 더 나은 방법이 없나 생각하다가 캐러셀 형태가 생각났다

캐러셀을 이용해서 하나씩 넘겨가면서 각 골에 대한 정보들을 보여주면 훨씩 UI/UX면에서

단순 li로 나열하는 것보다 좋을 것이라고 생각했다

그래서 캐러셀을 사용하기로 했고 보다 빨리 구현하기 위해 react-slick을 사용하기로 했다

react-slick을 위한 세팅은 [FIFAPulse] 개발기록 - react-slick install하기 , TS환경에 @types 적용하기 를 보면 된다



내가 사용한 react-slick 코드이다

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
  };
<Slider {...settings}>
                {myGoalData
                  .sort((a, b) => a.goalTime - b.goalTime)
                  .map((i, index) => {
                    return (
                      <div key={index}>
                        <b>{calculateGoalTime(i.goalTime)}</b>
                      </div>
                    );
                  })}
</Slider>
  • myGoalData는 위에서 언급했던 골 정보 데이터를 담은 배열이고

  • 이걸 시간순대로 오름차순해서 시간을 보여주는 것이다

이제 캐러셀 자체는 구현이 됐으므로 , 각 화살표를 클릭할 때마다
상태가 업데이트 되는 기능만 구현하면 된다



클릭할 때마다 상태 업데이트 구현하기

현재 내 캐러셀은 각 골 정보를 map 객체로 순회하며 골 시간을 보여주고 있다

아래의 사진처럼 보여지는 것인데 저기서 양쪽의 화살표를 클릭하면

이제 해당 시간대의 골 정보를 보여지게 구현해야 했었다

구현 방법 자체는 매우 간단하다

  1. 현재 골 정보 데이터를 가진 myGoalData 는 객체 배열 형태이다

    const myGoalData =
    [	
    	// 첫번째 골 데이터
    	{
        assist: true,
        assistSpId: 272037576,
        assistX: 0.553772509098053,
        assistY: 0.3725910186767578,
        goalTime: 496,
        hitPost: false,
        inPenalty: true,
        result: 3,
        spGrade: 6,
        spId: 246189505,
        spIdType: true,
        spLevel: 4,
        type: 1,
        x: 0.92109727859497,
        y: 0.4674257934093475,
      },
    	{...}, 	// 두번째 골 데이터
    	{...}, 	// 세번째 골 데이터
    ]
    
  2. 그러므로 인덱스를 가지는 상태를 하나 생성하고,

    const [myGoalIndex, setMyGoalIndex] = useState(0);
    • 오른쪽 화살표를 클릭하면 상태값을 +1
    • 왼쪽 화살표를 클릭하면 상태값을 -1 해서

    myGoalData[myGoalIndex] 에 해당하는 골 정보만 보여주면 됐었다



react-slick에서 각 화살표에 대해 이벤트나 스타일링 같이 추가적인 설정을 하려면

  • react-slick에 사용되는 settings에다가 nextArrow, prevArrow 속성을 추가하고

  • 여기에 직접 화살표에 해당하는 컴포넌트를 지정해 줘야 한다

  • 그리고 만약에 추가적인 이벤트 핸들러 로직을 적용하고 싶다면 해당 컴포넌트의

    props로 지정해 주면 된다

const myNextArrowClick = () => {
    // goalIndex 상태값 +1
    setMyGoalIndex((prev) => prev + 1);
  };

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
  
  	// nextArrow, prevArrow 속성 추가
    nextArrow: <NextArrow myNextArrowClick={myNextArrowClick} />,
    prevArrow: <PrevArrow myPrevArrowClick={myPrevArrowClick} />,
  };

import React from 'react';

			   //타입은 임시로 any로 지정
const NextArrow = (props: any) => {
	
	// myNextArrowClick 같이 추가적으로 진행할 이벤트 로직을 제외한
	// className, style, onClick은 기본적으로 props에 할당이 된다
  const { className, style, onClick, myNextArrowClick } = props;

  return (
    <div
      className={className}
      style={{ ...style, display: 'block' }}
      onClick={() => {
        onClick();
        myNextArrowClick (); // 따로 props로 전달받은 이벤트 로직 진행
      }}
    />
  );
};

export default NextArrow;

이렇게 진행했을 때 내가 의도한 대로 제대로 동작이 된다

그렇지만 큰 문제가 발생한다

화살표를 눌러서 다름 캐러셀로 넘어가는 그 애니메이션이 동작하는 그 찰나의 동안에
버튼을 한번 더 누르게 되면 캐러셀은 하나만 넘어가는데 상태 업데이트는 2번이 되어 버리는 것이다

즉, 캐러셀이 한번 넘어갈 동안 상태 업데이트가 1번 이상 진행되는 문제가 발생하는 것이다

이렇게 되면 만약 골이 7개가 들어갔을 경우 , 7개까지 캐러셀을 넘길 수 있어야 하는데

  • 마지막 7번째 골의 시간이 38:39일 경우 38:39 캐러셀이

    맨 마지막에 위치하게 된다

  • 그러나 광클을 하게 됐을 경우 아래처럼 분명 마지막 골이어야 하는데 캐러셀은

    여전히 5번째를 가리키고 있게 된다







해결

첫번째 시도

구글링을 해 본 결과, react-slick 의 settings 옵션에서

애니메이션이 시작될 때 와 애니메이션이 끝날 때 각각 진행하고자 하는

로직을 추가로 지정해 줄 수 있다고 한다

  • 그래서 다음 캐러셀로 넘어가는 애니메이션이 진행중인지 알려주는 상태값을 하나 추가하고
    const [isAnimating, setIsAnimating] = useState(false); // true면 애니메이션이 진행 중
  • 이 상태값을 기반으로 캐러셀이 넘어가는 애니메이션이 진행이 될 동안은 추가적인 상태 업데이트 로직을 막고자 했다
const myNextArrowClick = () => {
    if (isAnimating) return; // 애니메이션이 진행 중일 때는 상태 업데이트 x
    setMyGoalIndex((prev) => prev + 1);
  };

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
	beforeChange: () => setIsAnimating(true), // 애니메이션이 시작될 때 true로 변경
    afterChange: () => setIsAnimating(false), // 애니메이션이 끝날 때 false로 변경
    nextArrow: <NextArrow myNextArrowClick={myNextArrowClick} />,
    prevArrow: <PrevArrow myPrevArrowClick={myPrevArrowClick} />,
  };

근데 제대로 동작하지 않았다

아무래도 상태가 afterChange나 beforeChange의 로직이 트리거되는 그 타이밍과

상태가 업데이트 되는 그 찰나의 순간에 빈틈이 존재하는 것 같다



두번째 시도

그렇다면 혹시 , setTimeout을 이용해서 캐러셀 애니메이션이 진행되는 그 시간 만큼

아예 해당 이벤트 로직을 막으면 어떻게 될까?

const [isButtonDisabled, setIsButtonDisabled] = useState(false); 
// true면 버튼의 이벤트 로직이 진행되지 않음
// 버튼을 일정 시간 동안 비활성화
const disableButtonForSeconds = (seconds: number) => {
    setIsButtonDisabled(true);
    setTimeout(() => {// 1초 있다가 isButtonDisabled를 false로 변경
      setIsButtonDisabled(false);
    }, seconds);
  };

const myNextArrowClick = () => {
		// 애니메이션 중일 때 or setTimeout으로 지정된 1초동안 클릭이 무시
    if (isAnimating || isButtonDisabled) return; 
    setMyGoalIndex((prev) => prev + 1);
    disableButtonForSeconds(1000); // 1초(=캐러셀 애니메이션 동작 시간)동안 버튼 비활성화
  };

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
	beforeChange: () => setIsAnimating(true), 
    afterChange: () => setIsAnimating(false), 
    nextArrow: <NextArrow myNextArrowClick={myNextArrowClick} />,
    prevArrow: <PrevArrow myPrevArrowClick={myPrevArrowClick} />,
  };

지금까지 진행한 코드를 보면 화살표를 한번 클릭했을 때 다음 캐러셀로 넘어가는

애니메이션이 진행이 될때,

  • beforeChange로 인해 isAnimating이 true가 되고
  • disableButtonForSeconds() 로 인해 isButtonDisabled가 1초동안은 true가 된다

결국 이 2가지로 인해서 둘 중 하나라도 true가 된다면 다음 클릭 이벤트를 아예 진행하지

않도록 한 것이다



근데 , 이렇게 해도 여전히 문제가 해결되지 않았다.


정확히 말하면 광클을 하게 되면

이제는 캐러셀 애니메이션이 넘어간 횟수에 비해 , 상태 값이 더 늦게 바뀌게 된다

캐러셀이 3번 넘어가는데 상태값은 1번만 업데이트가 되어 버리는 상황인 것이다



사실, react-slick라이브러리에서 제공하는

beforeChange 나 afterChange가 트리거되는 시점이나 , nextArrow나 prevArrow에 지정된 컴포넌트의 이벤트 로직이 정확히 언제 진행되는지에 대해서 알 수 가 없었기 때문에

이 문제의 원인이 무엇인지 정확히 파악하지 못했고
여기에 setTimeout까지 적용을 해 버리기 완전히 뒤죽박죽 섞여버린 것 같았다



세번째 시도 (드디어 해결!)

생각을 해 보니 , 애니메이션이 진행 중인지에 대한 상태값을 이벤트 로직에 조건부로 사용하는 것은

이벤트 로직이 트리거 되는 시점에 의존 할 수 밖에 없게 된다

const myNextArrowClick = () => {
    if (isAnimating) return; 
    setMyGoalIndex((prev) => prev + 1);
  };

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
	beforeChange: () => setIsAnimating(true), 
    afterChange: () => setIsAnimating(false), 
    nextArrow: <NextArrow myNextArrowClick={myNextArrowClick} />,
    prevArrow: <PrevArrow myPrevArrowClick={myPrevArrowClick} />,
  };

무슨 말이냐 하면, isAnimating 상태가 beforeChange, afterChange에 의해 아무리 애니메이션의
진행 시점에 딱 맞춰서 제대로 바뀐다 한들,
결국 nextArrow로 지정한 컴포넌트의 myNextArrowClick 이벤트 로직이 진행되는
그 시점에 isAnimating 상태가 제대로 안 바뀌어 있다면
의미가 없다는 것이다

그래서 아무래도 내가 의도한대로 동작하지 않았던 것 같다

그래서 이를 해결 하려면

beforeChange나 afterChange에 의해 isAnimating의 상태가 변경이 되면 더 이상 추가적인 것(지금은 myNextArrowClick가 진행되는 타이밍)에 의존하지 않고
오로지 isAnimating의 상태에 의해서만 동작을 진행하도록 하는 것이 필요했다



그러므로 , 애니메이션이 시작될 때와 끝날때에 맞춰서 진행하는 로직은

어쩔 수 없이 react-slick에서 제공하는 beforeChange , afterChange를 사용할 수 밖에 없으니까

  • beforeChange , afterChange에 따라 isAnimating 상태를 바꾸는 것은 그대로 유지하고

  • isAnimating에 따라 이벤트 로직을 진행하는게 아니라

  • 아예 isAnimating에 따라 nextArrow , prevArrow 자체를 활성화 , 비활성화 하게 되면 어떻게 될까?

    그러면 이제 화살표 클릭으로 인한 myNextArrowClick()은 오직 isAnimating상태에 의해서만 동작하게 되는 것이다!

// 혹시 모르니, 1초 동안 버튼 비활성화 기능은 유지
 const disableButtonForSeconds = (seconds: number) => {
    setIsButtonDisabled(true);
    setTimeout(() => {
      setIsButtonDisabled(false);
    }, seconds);
  };


const myNextArrowClick = () => {
    setMyGoalIndex((prev) => prev + 1);
    disableButtonForSeconds(1000); 
  };

const settings = {
    dots: true,
    infinite: false,
    speed: 400,
    slidesToShow: 1,
    slidesToScroll: 1,
    beforeChange: () => setIsAnimating(true), 
    afterChange: () => setIsAnimating(false), 
	
  	//isAnimating 의 값이 따라 컴포넌트 할당
    nextArrow: isAnimating ? undefined : <NextArrow myNextArrowClick={myNextArrowClick} />,
    prevArrow: isAnimating ? undefined : <PrevArrow myPrevArrowClick={myPrevArrowClick} />,
  };

( 💡 TS에서 nextArrow의 값은 ReactNode 아니면 undefined만 가질 수 있으므로

nextArrow: !isAnimating && <NextArrow myNextArrowClick={myNextArrowClick} />

이렇게 사용하는 것 대신 undefined에 대한 분기점을 따로 만들어 줘야 했다)



이렇게 해 보았더니 드디어 제대로 동작한다…!

이번 문제는 단순히 react-slick을 사용하는 방법에 대해 깨달은 것을 넘어서
하나의 로직을 진행하는데 있어서 최대한 의존성을 줄여야 하는 이유에 대해서도 깨달은 것 같다

profile
웰시코기발바닥

1개의 댓글

comment-user-thumbnail
2023년 8월 1일

오호 정말 유익하군요!

답글 달기