✚ Dr.Martens 간지철철 scroll event 결국 구현한 이야기

solmii·2020년 7월 3일
2

Project

목록 보기
9/13
post-thumbnail

   뭘 했다고요?   

본 글에서 말하려는 scroll event는 해당 영상 2:08초까지 나옵니다.

나는 style값을 scroll이 바뀔때마다 setState 했기때문에 위의 영상처럼 스크롤을 올렸을때도 자연스럽게 동작한다. (뿌ㅡ듯)


   왜 했다고요?   

사실, 처음 backlog 나눌때까지만 해도, 저 이거 하고싶어요! 하긴 했지만 결국 못끝내고 발표하게 될줄 알았다.
그런데 1주차의 나는 생각보다 대단했고, 제품 상세 페이지와 리스트 페이지를 빠르게 끝낸 덕에 선택권이 생겼다.

  1. 재미는 없지만 사이트에 필요한 기능을 담당할 것인가
  2. 더욱 재미는 없지만 꼭 필요한 코드 리팩토링을 담당할 것인가
  3. 닥터마틴 사이트를 투표한 이유인 scroll event에 도전할 것인가

감사하게도 팀원들 모두가 솔미님 하고싶은거 다해 라는 반응이었기에 3번 scroll event에 도전하게 되었다.
(이 땐 몰랐지, scroll 따위가 나를 이렇게 고통스럽게 할줄은.....)


   어떻게 했다고요?   

처절한 검색의 흔적..... (물론 이건 빙산의 일각임ㅎ)
근데도 모르겠다. 전혀 감도 못잡겠더라..... 결국 그날 뭔가 scroll이 될랑말랑 하는 코드를 낑낑대며 짜는 꿈까지 꿨다...😭
결국 두손 두발 다 들고 승현님께 달려감😱😱😱😱
승현님께서 거의 다 알려주셨고 저는 뭐 한게 없네요......ㅎ.....(프론트신 갓승현)

아무튼 결론은, 조절해야 하는 style 속성을 state로 관리하고(opacity, scale, transformX), 이벤트를 걸어야 하는 모든곳의 window.scrollY 축의 값을 구한다.
이 때, 이벤트 시작 위치와 끝나는 위치를 각각 구해야 한다.
(잘 보면 scroll이 내려가면 opacity 가 먼저 조절되고, 그 후에 scale이 조절되기 때문에 opacity 시작 위치, 끝 위치, scale 시작 위치, 끝 위치를 각각 구했다...힘들어따..😫)

우선, scroll 이벤트를 시작하기 위해 componentDidMountaddEventListener를,componentWillUnmountremoveEventListener 를 등록한다.
(등록만 하면 다른 페이지로 이동해도 scroll 이벤트가 그대로 남아 있기 때문에 componentWillUnmount 에서 이벤트를 삭제해줘야 한다.)

// 첫 render 후에 scroll 이벤트 등록
componentDidMount() {
 window.addEventListener("scroll", this.scrollHandler);
}

// scroll 이벤트 사용 후에는 다시 unmount
componentWillUnmount() {
 window.removeEventListener("scroll", this.scrollHandler);
}

그리고, scrollHandler 를 정의해준다.

// scroll을 내릴때 trancform, opacity 변경하는 이벤트
  scrollHandler = () => {

    // opacity style
    if (window.scrollY > 7000 && window.scrollY < 7600) {
      this.setState({
        opacity: 1 - (window.scrollY - 7000) / 1000,
      });
    }
    if (window.scrollY > 10500 && window.scrollY < 11100) {
      this.setState({
        opacity: 1 - (window.scrollY - 10500) / 1000,
      });
    }
    if (window.scrollY > 18400 && window.scrollY < 18900) {
      this.setState({
        opacity: 1 - (window.scrollY - 18400) / 1000,
      });
    }

    // scale style
    if (window.scrollY > 7800 && window.scrollY < 8500) {
      this.setState({
        scale: 1 + (window.scrollY - 7800) / 6000,
      });
    }
    if (window.scrollY > 10900 && window.scrollY < 11400) {
      this.setState({
        scale: 1 + (window.scrollY - 10900) / 6000,
      });
    }
    if (window.scrollY > 19200 && window.scrollY < 19700) {
      this.setState({
        scale: 1 + (window.scrollY - 19200) / 6000,
      });
    }

    // transformX style
    if (window.scrollY > 13600 && window.scrollY < 14800) {
      this.setState({
        transformX: (window.scrollY - 13600) / 10,
      });
    }

    // reset
    if (
      (window.scrollY > 9300 && window.scrollY < 9400) ||
      (window.scrollY > 12800 && window.scrollY < 12900) ||
      (window.scrollY > 15000 && window.scrollY < 15100)
    ) {
      this.setState({
        opacity: 1,
        scale: 1,
      });
    }
  };

그리고 이렇게 setState 된 값을 각 scroll component에 props로 넘겨준 후에 inline style 로 적용해준다.

const style_left = {
  opacity: opacity,
  transform: `scale(${scale}) translateX(${transformX}px)`,
};

const style_rigth = {
  opacity: opacity,
  transform: `scale(${scale}) translateX(${-transformX}px)`,
};

return (
  <div className="main_scroll_relative">
    <img alt="main_scroll_img" src={backImg} style={style_left} />
    <img alt="main_scroll_img" src={backImg2} style={style_rigth} />
  </div>

잘 동작한다!!


   어떻게 고쳤다고요?   

잘 동작하는건 좋은데.... 몹시도 부끄러운 코드가 완성되었다.
같은 코드가 if 밭 안에서 반복되고 있으며, Y축의 값이 바뀌기라도 하면 수정해야 할게 한둘이 아니다.
당장 구현에 급급해서 이렇게 어마어마한 코드를 짜고 말았지만, 잘 구현되는걸 확인했으니 이제 리팩토링을 해야 한다.

어떻게 할 수 있을까?
당장 생각나는 방법으로는 각 이벤트 시작점과 끝점에 일정한 숫자가 더해진다는 규칙이 있으므로, 시작점들을 배열안에 넣어서 변수로 지정해두고, 거기에 for문을 돌리면 될 것 같은데?

일단 바로 도전 gogogo

scrollHandler = () => {
    const scroll = window.scrollY;
    const eventOffset = [7000, 10500, 18400];

    // opacity, scale style
    for (let i in eventOffset) {
      if (scroll > eventOffset[i] && scroll < eventOffset[i] + 600) {
        this.setState({
          opacity: 1 - (scroll - eventOffset[i]) / 1000,
        });
      }
      if (scroll > eventOffset[i] + 800 && scroll < eventOffset[i] + 1500) {
        this.setState({
          scale: 1 + (scroll - (eventOffset[i] + 800)) / 6000,
        });
      }
      // style reset
      if (scroll > eventOffset[i] + 2300 && scroll < eventOffset[i] + 2400) {
        this.setState({
          opacity: 1,
          scale: 1,
        });
      }
    }

    // transformX style
    if (window.scrollY > 13600 && window.scrollY < 14800) {
      this.setState({
        transformX: (window.scrollY - 13600) / 10,
      });
    }
  };

오? 바로 해결~!😍😍😍😍
뭔가 여기서도 더 줄일 수 있을 것 같은데...! 이건 다음에 더 도전해 보기로!


   어떻게 더 고쳤다고요?   

음... scroll 이벤트에 대해 완전히 잘못 생각하고 있었다...!
scroll 위치가 바뀔 때 마다 실행되는 이벤트라, 함수 내에 setState 가 있다면 scroll이 바뀔때마다 매번 setState 되는데 제대로 이해하지 못하고 안에서 for 문을 사용해버렸다.

즉, scroll 이 바뀔때마다 매번 for 문이 돌고, 매번 setState 되는 엄청 비싼 이벤트 완성.......😱

일단 for 문 없애는거에 중점을 둬서 수정했는데 매우매우 길고 비효율적인 코드가 나와서 고민중이었는데, 종택님이 많이 도와주셔서 아래처럼 나름 많이 줄였다!!

리팩토링 하면서 계산된 속성명에 대해 감을 많이 잡을 수 있었다.

  setScrollConfig = (scroll) => {
    const section = {
      [scroll > 7000 && scroll < 10500]: [7000, "first"],
      [scroll > 10500 && scroll < 18400]: [10500, "second"],
      [scroll > 13600 && scroll < 14800]: [13600, "first"],
      [scroll > 18400]: [18400, "third"],
    };
    return (
      Object.keys(section).includes("true") && {
        sectionBrake: section["true"][0],
        currentOpacity: section["true"][1] + "Opacity",
        currentScale: section["true"][1] + "Scale",
      }
    );
  };

  scrollHandler = () => {
    const scroll = window.scrollY;
    const { sectionBrake, currentOpacity, currentScale } = this.setScrollConfig(
      scroll
    );

    let eventBrake = scroll - sectionBrake;
    let opacity = 1;
    let scale = 1;
    let transform = 0;

    const firstBrake = eventBrake > 0 && eventBrake < 600;
    const secondBrake = eventBrake >= 600 && eventBrake < 4000;
    const thirdBrake = eventBrake > 800 && eventBrake < 1500;
    const fourthBrake = eventBrake >= 1500 && eventBrake < 4000;
    const fifthBrake = scroll > 14600 && scroll < 18000;

    if (firstBrake) opacity = 1 - eventBrake / 1000;
    if (secondBrake) opacity = 0.4;
    if (thirdBrake) scale = 1 + (eventBrake - 800) / 4000;
    if (fourthBrake) scale = 1.17;
    if (fifthBrake) transform = 105;
    if (sectionBrake === 13600) transform = eventBrake / 10;

    this.setState({
      ...this.state,
      [currentOpacity]: opacity,
      [currentScale]: scale,
      transformX: transform,
    });
  };

더 줄이고 싶지만...^^....아무튼 돌아간다는거에 만족하고 오늘은 여기까지....


   throttle 적용했다고요?   

🤷🏻‍♀️ throttle
웹 페이지 성능의 향상을 위해 나타났는데, 이벤트를 일정한 주기마다 (설정한 Milliseconds) 발생하도록 제약을 거는 프로그래밍 기법이다!

export const throttle = (callback, milliseconds) => {
  let throttleCheck;

  return function () {
    if (!throttleCheck) {
      throttleCheck = setTimeout(() => {
        callback(...arguments);
        throttleCheck = false;
      }, milliseconds);
    }
  };
};

이렇게 setTimeout 메서드를 이용해 지정한 milliseconds 마다 함수가 실행될 수 있도록 하는 throttle 함수를 만들고,

 window.addEventListener(
      "scroll",
      throttle(() => this.scrollHandler(this.setScrollConfig), 10)
    );

Main.js component의 componentDidMount 에서 scrollHandler와 10 millisecond를 (0이랑 별반 다를바 없지만.....) 위에서 만든 throttle의 인자로 넘겨주었다.

출처 : https://pewww.tistory.com/9

그리고, 쬐끔이나마 성능 향상에 도움이 될까 싶어서 scrollHandler 함수 앞에 조건을 추가했다.

if (scroll < 7000) return;

대충 위에 있는 겁나 복잡한 함수

scroll7000 이하 라면, 아무런 event가 실행되지 않으므로, 빠르게 return 해버리기!


아무튼 구현했따~!!!!!!!!!!!!!!!!!

profile
하루하루는 성실하게 인생 전체는 되는대로

2개의 댓글

comment-user-thumbnail
2020년 7월 8일

대단대단

답글 달기
comment-user-thumbnail
4일 전

LGTM

답글 달기