[우아한테크코스 FE LEVEL1-4] 비동기 & 이벤트 처리 방식(throttle & debounce)

Gyuhan Park·2024년 4월 9일
1

[ 학습 목표 ]

  • 자바스크립트 언어의 비동기 개념 이해 및 활용
  • API 통신을 처리할 때 기술적으로 또는 UX적으로 고려해야 하는 케이스를 고민하고 개선
    • UX(사용자 경험) 개선
  • API 통신이 포함된 테스트 환경 경험 (cypress)

💭 TMI

레벨 1 마지막 미션이 마무리되었다. 이번 미션에서는 컴포넌트 생성 방법을 바꿔봤는데 만족스러웠다. 컴포넌트를 생성할 때 부모를 알고 있어야 한다는 점target이 가리키는 대상을 바꿔 일관성이 떨어진다는 점 등의 문제가 있었다. 컴포넌트를 생성할 때는 만들기만 하고, 붙이는 로직은 별도로 처리하였더니 좀더 컴포넌트처럼 동작한 것 같아서 좋았다.

레벨 1 끝난 기념으로 우리 조와 첫 회식을 했다 (바빠지면 처음이자 마지막일지도ㅠ) ㅋㅋㅋㅋㅋㅋㅋ 그 다음날 일정이 있어서 일찍 갈 생각이였는데 놀다보니 재밌어서 신나버렸다 다음에 또 고고 🔥

이번에는 미션이 끝나고 일주일 정도 지난 후에 회고를 작성하다보니 미션에 대한 기억이 희미해져간다. 첫 미션 때는 2차 PR을 제출한 당일에 작성한 것 같은데 역시 사람은 미룰수록 더 늘어져(?) 그래도 작업하면서 휘날리듯 남긴 기록들을 정리해보자.

📘 배운 점 & 느낀 점

약 2달 간의 레벨 1을 진행하면서 어떻게 하면 많은 것들을 얻어갈 수 있을까? 에 대한 고민이 많았다. 원래 모르고 있었던 사람과 조금 알고 있었던 사람이 같아지게 된다는 생각이 들었던 것 같다. 그런 부분에서 조급함을 느꼈던 것 같은데 좀더 마음을 편하게 갖기로 했다. 그렇게 한번에 같아진다면 모르던 사람이 빨리 이해한거고, 조금 알던 사람이 얕게 공부했던 게 아닐까? 나자신을 믿고 단단하게 나아가자 😁

✅ 이벤트 처리 방식

ScrollEvent, ResizeEvent 등은 사용자의 의도와 상관없이 과도하게 발생한다. 특히 사용자가 더많은 컨텐츠를 보기 위해 스크롤을 내리는 경우 수많은 스크롤 이벤트가 발생하는데, 이는 스크롤 이벤트에 등록된 이벤트 리스너가 동기적으로 실행되므로 메인 스레드에 악영향을 준다. 이렇게 연속적으로 발생하는 이벤트를 처리하는 방법으로 throttle, debounce 가 존재하며, 무한 스크롤을 구현할 때는 IntersectionObserver 도 사용할 수 있다.

🚨 throttle

이벤트에 의한 콜백을 특정 시간 뒤에 호출 하는 것

스크롤 이벤트에 사용하면서 주기적으로 실행되는 방식으로 오해할 수 있는데, 특정 시간 이내에 들어온 호출을 모두 무시하고, 특정 시간 뒤에 한 번만 실행되도록 하는 것이 핵심

  • timerId가 없을 경우 setTimeout을 실행시켜 timerId를 반환받고, setTimeout 내의 콜백함수에서 timerId를 null 로 초기화한다.
  • 스크롤을 했을 때 이벤트핸들러가 실행되는데 timerId가 없을 때만 setTimeout이 실행되므로 1초 딜레이를 걸었다면 1초 내의 호출된 스크롤이벤트는 모두 무시되고 맨 처음 이벤트 핸들러가 실행 된다.

🚨debounce

debounce : 연속해서 호출되는 콜백 중 마지막 것만 호출 하도록 하는 것

  • setTimeout으로 timerId를 반환받고, timer가 있으면 clearTimeout을 실행시켜 이전의 콜백함수들을 모두 취소한다.
  • 이벤트 발생이 멈추면 맨 마지막 실행된 이벤트 핸들러가 실행 된다.

🚨 IntersectionObserver

IntersectionObserver : 특정 요소가 사용자 화면에 보이는지 안보이는지 판단

  • 브라우저 뷰포트(Viewport)와 원하는 요소(Element)의 교차점을 관찰 하며, 요소가 뷰포트에 포함되는지 아닌지 구별하는 기능을 제공
  • IntersectionObserver WEB API로서 비동기적 으로 실행되기 때문에, 메인 스레드에 영향을 주지 않으면서 요소들의 변경사항들을 관찰
  • target 요소를 observe 대상으로 지정하면, IntersectionObserver의 인자로 들어가는 콜백함수가 target 요소가 관측될 때 콜백이 실행된다.

무한 스크롤을 구현할 때는 throttle과 debounce 중에서는 throttle이 많이 사용된다고 한다. 마지막 위치에 왔을 때 실행되도록 debounce를 사용하는 게 더 효율적이고 한번만 실행되는 게 적절하지 않나? 라고 생각했다. 하지만 사용자가 계속 스크롤을 내릴 경우 debounce가 적용된 함수는 실행되지 않기 때문에 throttle이 더 적합하다는 것을 알게 되었다.

✅ Promise

API 호출과 같은 시간이 오래 걸리는 작업이 있을 때 비동기 처리를 한다. 비동기는 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 방식 으로, JS에서 비동기 처리를 위한 방법으로는 콜백 함수, Promise, async/await 3가지가 존재한다. 콜백 함수는 가독성을 저해시켜 잘 사용되지 않고, Promise의 복잡함을 해결한 async/await 을 많이 사용한다. Promise 처리 방식과 이벤트 루프를 이해했다는 가정 하에 async/await 만 사용하게 되지 않을까? 생각을 했는데 Promise에 대해 다시 한번 공부하게 되었다.

Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체 이며, 비동기 작업이 끝날 때까지 결과를 기다리는 것이 아니라, 결과를 제공하겠다는 '약속'을 반환 한다. Promise는 pending, fulfilled, rejected 3가지 상태를 가지며, resolve 또는 reject 되면 executor 가 호출되어 비동기 처리 결과를 반환한다.

Promise를 이해했다면 다음 질문에 답해보자.

1초 뒤에 실행되는 비동기 함수가 있는데 비동기 함수의 결과값을 외부에서 반환받으려면 어떻게 할까?

먼저 1초 뒤에 실행되는 비동기 함수 라고하면 다음과 같은 코드를 생각할 수 있다.

setTimeout(async () => {
	await callback();
}, 1000)

해당 비동기 함수의 결과값을 반환 받으려면 리턴을 해야하므로 다음과 같은 코드를 생각할 수 있다.

setTimeout(async () => {
	const result = await callback();
	return result;
}, 1000)

하지만 다음 코드는 의도한대로 동작하지 않을 것이다. return은 가장 가까운 화살표 함수를 종료시키며 result를 반환하는데, 이는 setTimeout의 인자로 들어가게 되고 아무 동작도 하지 않는다. 나는 데이터를 fetch하기 시작하여 로딩 중일 때 다른 데이터를 fetch할 수 없도록 막는 처리를 하기 위해 위와 같은 고민을 하였다.

scroll, resize는 window 객체 1개에 붙는 이벤트이기 때문에 처리하기 수월했는데 각각의 아이템이 다른 클릭 이벤트를 처리하기 때문에 복잡했다. 어떻게 외부로 반환하는가 + fetch 중일 때 어떻게 다른 fetch 요청을 막을 수 있는가에 대한 문제를 Promise + throttle 로 해결하였다.

클로저로 throttle을 구현하여 timer을 재정의 하지 않도록 중첩함수를 반환하여 사용하였고, setTimeout 내에서 호출하는 비동기 함수 결과를 반환하기 위해 Promise의 resolve 를 사용하였다.

interface ThrottleProps {
  callback: (...args: any[]) => Promise<unknown>;
  delay: number;
}

const throttleAsync = ({ callback, delay }: ThrottleProps) => {
  let timer: NodeJS.Timeout | undefined;

  return (...args: any[]) =>
    new Promise(resolve => {
      if (!timer) {
        timer = setTimeout(async () => {
          const result = await callback(...args);
          timer = undefined;
          resolve(result);
        }, delay);
      }
    });
};

export default throttleAsync;

throttle을 사용하면 첫 번째 호출이 실행되는데, debounce로 마지막 호출을 실행시키면 그만큼의 delay가 생길 수 있다고 판단하였다. 글을 쓰고보니 debounce가 적합한 것 같긴 하지만 적용해보면서 상황에 맞게 적절히 응용할 수 있게 되었다.

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글