프로젝트에 적용된 프론트엔드 기술

Throttling and Recoil-프로젝트에 적용된 프론트엔드 기술

안나경
6 min readJan 29, 2024

Throttling

초기 스크롤이 없는 메인 페이지를 만들었다가, 사용자에게 우리 사이트를 어필하고 기억에 각인시키고자 스크롤을 활용한 동적 UI를 적용하게 되었다.

사용자의 스크롤 위치와 Framer Motion 라이브러리를 활용한 애니메이션을 연동하면서 애니메이션이 우리가 원하는 대로 동작하지 않는 현상(애니메이션이 느려지거나 버벅이는 현상)을 확인했고, 이를 해결하기 위해 Throttling 기술을 메인페이지에 적용하는 것을 고려하게 되었다.

Throttling은 짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지하는 기법이다.

우리는 스크롤 UI를 적용하기 위해 Framer Motion에서 제공하는 useMotionValueEvent 훅을 사용했는데, 이는 모션 값을 이벤트에 바인딩할 때 사용된다.(여기서 모션값은 스크롤의 위치에 해당한다) 기존 코드는 Throttling을 적용하지 않아 스크롤 이벤트가 발생할 때 불필요하게 많은 이벤트 함수가 호출되어 애니메이션이 우리의 의도대로 동작하지 않았던 것이다.

const { scrollY } = useScroll();
const [isUp, setIsUp] = useState(false);

useMotionValueEvent(scrollY, "change", (latest) => {
if (latest >= 400 && !isUp) {
setIsUp(true);
const box = document.getElementById("page2container");
animate(box as HTMLElement, { top: "20%" }, { duration: 0.8 });
} else if (latest < 370 && isUp) {
setIsUp(false);
const box = document.getElementById("page2container");
animate(box as HTMLElement, { top: "100%" }, { duration: 0.5 });
}
});

Throttling을 적용하여 0.1초마다 함수를 실행하도록 제한을 두니 애니메이션이 우리가 원하는 대로 동작하는 걸 확인할 수 있었다.

const [isDone, setIsDone] = useState([false, false, false]);
const [throttler, setThrottler] = useState(false);
const control1 = useAnimationControls();
const control2 = useAnimationControls();
const control3 = useAnimationControls();
useMotionValueEvent(scrollY, "change", (latest) => {
if (latest < 400) {
const scroll = document.getElementById("scrollContent") as HTMLDivElement;
scroll.style.top = `-${latest * 2}px`;
}
//쓰로틀링으로 0.1초마다 함수 실행하도록 제어
if (throttler) return;
setThrottler(true);
setTimeout(() => {
//함수가 한 번만 실행될 수 있도록 상태변수 추가
if (latest >= 400 && !isDone[0]) {
let _isDone = [...isDone];
_isDone[0] = true;
setIsDone(_isDone);
control1.start({ opacity: 1, y: 0 });
}
if (latest >= 760 && !isDone[1]) {
let _isDone = [...isDone];
_isDone[1] = true;
setIsDone(_isDone);
control2.start({ opacity: 1, y: 0 });
}
if (
(latest >= 2100 && !isDone[2] && window.innerWidth >= 1024) ||
(latest >= 3570 &&
!isDone[2] &&
window.innerWidth >= 769 &&
window.innerWidth <= 1023) ||
(latest >= 2900 && !isDone[2] && window.innerWidth <= 768)
) {
let _isDone = [...isDone];
_isDone[2] = true;
setIsDone(_isDone);
control3.start({ opacity: 1 });
}
setThrottler(false);
}, 100);
});

Global State Management Recoil

useState만으로 상태 관리를 하는 데에는 존재하는 컴포넌트의 수가 많아 상태 값을 컴포넌트 간에 전달하기가 어려웠다. 그래서 상태 관리 라이브러리인 Recoil을 통해 전역으로 상태를 관리하는 것을 고려하게 되었다.

왜 Recoil인가?

상태 관리 툴로 Redux, Recoil, Zustand 등 다양한 선택지가 있었으나, Redux는 러닝커브가 많이 높아 단기간에 프로젝트를 완성하는 과정에서 사용하기 어렵다는 결론이 나왔다. 그래서 Recoil과 Zustand 중 하나를 선택해야 했다.

Zustand는 무게가 가볍고 사용하기가 편리하다는 평이 있었다. 하지만 사용법을 아는 사람이 없어서 배우는 데 일정한 시간이 들어갈 것으로 보였다. Recoil은 팀원 중 사용할 줄 아는 사람이 있기도 하고 사용법이 useState와 비슷해서 배우는 시간도 얼마 들지 않을 것이라는 판단 하에 전역 상태 관리 라이브러리로 채택하게 되었다.

전역 상태는 프로젝트 폴더에 Recoil.ts를 만들어 따로 관리를 해주었다. GitHub 로그인 정보, 프로필 정보, 리포지토리 목록에 대한 상태를 저장하여 면접 생성 페이지, 면접 결과 페이지에 정보를 표시하는 등 여러 페이지에서 사용자가 자신과 관련된 내용을 확인함으로써 사용자의 UX를 개선시키려는 노력을 하였다.

Opinion

프론트엔드 개발자로서 같은 코드도 효율적으로 최적화할 수 있는 방안에 대해 많이 고민했던 시간이었으며, 과도한 이벤트 핸들러 호출을 방지하고, 러닝커브 줄이기, 상태 관리 등 더 나은 웹을 만들기 위해 노력해야겠다는 생각이 들었다.

또한, 백엔드와 협업하는 과정에서 대화를 많이 나누는 게 중요하다는 것을 배웠다. 지금 API가 어떤 상태까지 개발되었는지, 변경사항이 생겼는지 등등 먼저 대화를 시도하지 않았다면 놓쳤을 내용들이 몇몇 있었는데, 대화를 함으로써 빠르게 피드백을 받을 수 있었고, 프론트엔드에 적용해서 개발 진행속도를 높게 유지할 수 있었다.

--

--