let과 디바운스 활용하기

Imnottired·2023년 5월 10일
1
post-thumbnail

프로젝트를 진행하던 도중 반응형웹을 구현하기 위해서 window.innerWidth를 사용하려 하였다.
next.js는 서버에서 먼저 실행되는데 이때 window같은 브라우저가 없기때문에 이를 찾지 못하고 오류가 발생한다.

그래서 클라이언트와 서버를 명확히 구분하여 오류를 방지하고, innerWidth를 빠르게 변경하면 많은 이벤트가 발생하기 때문에 디바운스를 걸어서 이벤트를 늦춰보겠다.




함수 작성하기

먼저 window.innerWidth는 재사용성이 높아서 함수로 만들어주는 것이 좋다.

const useWindowSize = () => {
  const isClient = typeof window === "object";
  const [windowSize, setWindowSize] = useState(
    isClient ? window.innerWidth : undefined
  );
  }

먼저 서버와 클라이언트를 구분할 수 있는 변수를 선언하고,
그것을 기반으로 값을 넣어준다

(추후에 초기값으로는 조건상 undefined가 들어갈 것이기 때문에 조건문을 삭제하였다)


  useEffect(() => {
    if (isClient) {
      const handleResize = () => {
          setWindowSize(window.innerWidth);
      };

      window.addEventListener("resize", handleResize);
      return () => {
        window.removeEventListener("resize", handleResize);
      };
    }
  }, [isClient]);
 

그리고 useEffect를 사용하여서 clean up 클라이언트가 로드 되었을 때 윈도우에 이벤트를 추가하고 setState를 업데이트 해준다.

그러면 업데이트 시에 shouldComponentUpdate가 트루가 되면서 렌더링이 된다.

값이 변화할 때마다 업데이트를 해준다.

width값이 변화할때마다 레이아웃을 다시잡고 페인트하기때문에 렌더링이 되겠지만,

그 사이에서 이벤트를 줄여주어서 콜스택에 부담을 줄여주어야한다.

그래서 디바운스를 사용하기로 하였다.


디바운스

  • 디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

디바운스를 사용하여서 연이어 업데이트 되는 것을 막고 일정 간격을 주는 방법을 사용하겠다.


     const handleResize = () => {
        const debounce = setTimeout(() => {
          console.log("windowSize");
          setWindowSize(window.innerWidth);
        }, 500);

        return () => {
          clearTimeout(debounce);
          console.log("디바운스");
        };
      };
      

setTimeout을 사용하여서 일정시간 이후에 실행 되도록 하였고, 다른 이벤트가 들어오면 취소하고 새로운 이벤트가 실행이 되도록 하였다.

해치웠나? 라고 생각했지만 setTimout 이벤트로 해결한 것이 아니었다.
return문에 console.log 디바운스를 적었지만 실행되지 않았다.
그래서 다른 이유로 해결되었다는 것을 알 수 있었고,

찾아본 결과, Auto batching이 적용되면서 문제가 해결 된 것이었다.
의도치 않았지만 운이 좋았던 것이었다.

Auto Batching

React에서 state가 변경될 때, React는 변경 사항을 즉시 적용하는 것이 아니라, 변경 사항을 비동기적으로 처리하는데 이것은 성능 향상을 위해 React가 state 변경을 일괄 처리하기 때문이다.

여러 개의 setState 호출이 일어날 때, React는 내부적으로 변경 사항을 큐에 넣고, 큐에서 일괄 처리합니다. 따라서 마지막으로 호출된 setState의 값이 최종적으로 적용된다. 이렇게 함으로써, React는 불필요한 렌더링을 방지하고 성능을 최적화할 수 있습니다.

18 버전 이하에서는 오직 React 의 이벤트 핸들러 내부의 state update 작업에 대해서만 가능했지만 이후 업데이트 되면서 setTimeout에도 적용이 되었다고 한다.

출처
https://velog.io/@rookieand/React-18%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80%EB%90%9C-Auto-Batching-%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80#%EF%B8%8F-react-18%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80%EB%90%9C-automatic-batching-%EC%9D%B4%EB%9E%80

디바운싱 재시도

원하는 결과가 나왔지만 내가 의도한 결과가 아니었기에 이 방향으로 다시 한번 시도해보았다.

     const handleResize = () => {
       const debounce = setTimeout(() => {
         console.log("windowSize");
         setWindowSize(window.innerWidth);
       }, 500);

       return () => {
         clearTimeout(debounce);
         console.log("디바운스");
       };
     };
     

위 함수의 문제점은 debounce에 timeout 변수를 저장하여도 return문이 돌아가지 않고, 다시 const debounce를 계속해서 저장만 하여서 문제였다.
이유는 한번 실행되고나서 그 다음이 return이 실행되는 것이 아니라 새롭게 위에서부터 시작 되기 때문에 계속해서 debounce에 값만 할당하는 것이 문제였다.

디바운스 설계

나의 계획은
1번. 먼저 함수가 실행이 되면 setWindowSize가 실행될 준비를 한다 (0.5초)

2번. 1번에 setWindowSize가 아직 실행되지 않았고, debounce에 Nodejs.timeout 값이 저장된다.

3번 이 상태에서 handleResize을 호출하면, clearTimeout이 실행되면서
그전 실행은 취소하고 마지막 값을 기준으로 setWindowSize를 준비한다.

3번까지 과정을 반복하여 시간 간격 내에 값이 들어오지 않았을 때 실행이 된다. 라는 가정이었다.

하지만 실행 후 취소 방식으로 로직을 작성하였고,
들어오면 실행이 계속 쌓여서 취소가 작동하지 않았다

그래서 방식을 취소 후 실행으로 바꾸었다.

하지만 여기서 문제가

        clearTimeout(timeoutId);
        console.log("일반 실행");
        timeoutId = setTimeout(() => {
          console.log("디바운스");
          setWindowSize(window.innerWidth);
        }, 200);

timeoutid를 알 수가 없다.
그래서 그전 실행을 취소할 수가 없고 오류를 낸다.

React를 사용하다보면 const를 지향하고 다른 변수 선언은 지양했는데,
이런 경우에 let을 사용해주어야한다 생각했다.(var는 스코프가 리스크가 크다)

let을 사용한 디바운스


let timeoutId: NodeJS.Timeout;

     const handleResize = () => {
       clearTimeout(timeoutId);
       console.log("일반 실행");
       timeoutId = setTimeout(() => {
         console.log("디바운스");
         setWindowSize(window.innerWidth);
       }, 200);
     };
     

위처럼 작성하였고, 함수가 실행이 되면 바깥 값에 nodejs.Timeout 값을 저장해서
다음 실행 떄 이 값을 불러와 clearTimeout을 실행하여 그 전 실행을 취소할 수 있다.


일반 실행은 함수가 실행할 때마다 넣어주었고, setTimeout 안에는 디바운스라는 콘솔로그를 달아서 실행 횟수를 확인하였다.

내가 의도한 대로 디바운스가 잘 작동되었고,
오토 배칭이랑 비교했을 때, 오토배칭은 함수를 연속으로 실행해도 특정 간격에서 실행되었고,
디바운스는 내가 함수를 끊지 않는 이상 계속해서 함수 실행을 미루어주었고,
더욱더 부드럽게 작동하였다.


let에 대한 리스크

남들이 쓰지 않는 이유가 있으니 무작정 사용하기 보다는 리스크를 충분히 고려해보아야하기때문에 공부를 더 해보았다.
const를 지향하는 이유는 불변성을 보장하기때문에 불필요한 렌더링을 제거할 수 있고, 사이드 이펙트를 막기 위함이다.
let은 재할당이 가능하여 빈번하게 렌더링이 일어날 수 있고, 사이드 이펙트 발생할 수 있어서 지양하지만, 상황에 맞추어서는 괜찮다고 한다.
특히 React 컴포넌트 밖에서 let을 사용하면 전역변수처럼 공유가 되니 그런 상황을 조심해야한다.

(let 문제에 관련 출처
https://www.youtube.com/results?search_query=react+let)

위와 같은 리스크를 알았고, let이 계속해서 업데이트 되니 컴포넌트가 계속해서 렌더링이 되는지 확인할 필요가 있다고 느꼈다.

컴포넌트 안에 렌더링이라는 콘솔로그를 작성하였고, 확인해본 결과

let에 의한 렌더링은 발생하지 않았다.
if문, useEffect에 감싸져있기때문에 블록스코프여서 발생하지 않는 것이었다.

해치웠다..




마무리

처음에는 window.innerWidth를 사용하기 위해 로직을 작성하였고, 빈번하게 렌더링 되는 것이 아쉬워서 디바운스까지 추가하였다.
디바운스로 함수가 컨트롤 되는 것을 보니 상쾌하게 돌아가는 듯한 느낌이어서 좋았다.

앞으로 디바운스를 활용할 방도가 보인다면 적극적으로 사용할 것 같고, React에서 let 사용에 대한 거부감이 심했고, const로 해결하려고 하다보니 문제가 생겨서 Auto Bacthing이 일어났는데 나의 편견이었던 것 같다.
블록체인이라는 점을 잘이용해서 사용한다면 괜찮을 것 같다.
(그래도 조심하자)

좋았던 점은 내가 무언가 사용할 때, 리스크를 고려하고 사용했다는 점과 의도치 않게 성공했지만 그 이유를 분석하고 내가 원하는 방향대로 성공했다는 점이 만족스러웠다.

끗..

profile
새로운 것을 배우는 것보다 정리하는 것이 중요하다.

4개의 댓글

comment-user-thumbnail
2023년 5월 14일

이번에 과제하면서 느꼈지만 진짜 state 관리하기 넘 까다롭습니다 ㅋ큐

답글 달기
comment-user-thumbnail
2023년 5월 14일

useState 초기값에 조건문을 넣을 수 있다는 사실.. 처음 알았네요..!!

답글 달기
comment-user-thumbnail
2023년 5월 14일

고생하셨습니다 !!

답글 달기
comment-user-thumbnail
2023년 5월 14일

localStorage.getItem("accessToken")쓰면서 (typeof window !== "undefined")로 조건문을 써서 오류를 없앴는데 이렇게 디바운스 개념을 접목해서 할 수도 있군요,, 이런 건 대체 어디서 아시는지!!! 저도 나중에 접목시켜야겠어요.
리스크를 짊어지며 원하는 방향으로 성공시키는 점이 너무 좋았습니다 잘보고가용

답글 달기