-
Notifications
You must be signed in to change notification settings - Fork 3
Debouncing Throttling
이 문서는 Debouncing과 Throttling이 ‘Ask-It’ 프로젝트에서 프론트엔드의 효율적인 서버 요청 처리와 사용자 경험 개선에 어떻게 기여했는지 설명합니다.
회원가입 시 유효성 검사를 위한 Debouncing과 질문/답변에 대한 좋아요 기능의 서버 부하와 UX 문제를 해결하기 위한 Throttling을 중심으로 사례와 구현 방식을 다룹니다. 이를 통해 서버 리소스 관리와 사용자 경험의 최적화를 달성한 과정을 공유합니다.
‘Ask-It’ 프로젝트는 사용자 간의 실시간 질문과 답변을 지원하는 플랫폼으로, 사용자 인터랙션이 빈번하게 발생합니다. 이러한 특성으로 인해 프론트엔드에서 발생하는 서버 요청의 효율적인 관리와 사용자 경험의 최적화가 중요해졌습니다. 프로젝트 개발 중 아래 두 가지 문제가 있었습니다.
- 회원가입 시 유효성 검사 요청의 빈번한 발생
- 좋아요 기능 사용 시 서버 부하 증가와 UX 저하
이를 해결하기 위해 Debouncing과 Throttling 기술을 도입하여 서버 요청을 효율적으로 관리하고 사용자 경험을 개선하고자 이 기술을 다루게 되었습니다.
- 정의: Debouncing은 연속된 이벤트 중 마지막 이벤트가 발생하고 일정 시간이 지난 후에 한 번만 함수를 실행하도록 하는 기술입니다. 이는 빈번한 이벤트 발생을 제어하여 불필요한 함수 호출을 방지하고 성능을 최적화합니다.
- 예시: 사용자가 검색어를 입력할 때마다 서버에 요청을 보내지 않고, 입력이 멈춘 후 일정 시간(예: 500ms)이 지나면 한 번만 서버로 요청을 보냅니다.
- 정의: Throttling은 지정한 시간 간격 내에서 함수 호출 횟수를 제한하여, 이벤트가 아무리 많이 발생해도 일정 주기마다 최대 한 번만 함수가 실행되도록 하는 기술입니다.
- 예시: 사용자가 스크롤 이벤트를 빠르게 발생시켜도 지정한 시간(예: 100ms)마다 한 번씩만 함수가 실행되어 리소스 사용을 제한합니다.
사용자가 이메일과 닉네임을 입력할 때마다 서버로 유효성 검사를 요청하면, 키 입력마다 서버 요청이 발생하여 불필요한 서버 부하를 초래합니다. 실시간으로 검사를 수행하면 사용자 입력 도중에 오류 메시지가 표시될 수 있어 UX가 저하됩니다.
따라서 아래의 요구사항이 생기게 됩니다.
- 사용자의 입력이 완료된 후 일정 시간이 지난 시점에만 서버로 유효성 검사 요청을 보내고 싶습니다.
- 불필요한 서버 요청을 줄여 서버 자원을 효율적으로 사용하고 사용자에게 정확한 피드백을 제공하고자 합니다.
질문과 답변에 대해 사용자가 좋아요를 빠르게 여러 번 누를 경우, 데이터베이스에 과도한 부하가 생깁니다. 좋아요를 누를 때마다 토스트 메시지가 발생하여 짧은 시간에 많은 메시지가 표시되면 사용자 경험이 저하됩니다.
따라서 아래의 요구사항이 생기게 됩니다.
- 일정 시간 내에 좋아요 요청 횟수를 제한하여 서버 부하를 줄이고 불필요한 토스트 메시지 발생을 방지하고 싶습니다.
- 사용자에게 좋아요가 정상적으로 처리되고 있음을 알리면서도 과도한 인터랙션으로 인한 불편함을 최소화하고자 합니다.
- 디바운싱을 적용하면 사용자가 좋아요가 정상적으로 처리되지 않는다고 느낄 수 있습니다.
사용자가 이메일과 닉네임을 입력할 때, 입력이 완료된 후에 서버로 유효성 검사 요청을 보내도록 구현해야 합니다.
아래 코드에서는 Debouncing을 직접 구현하지 않고 라이브러리를 통해 사용하고 있습니다. 또한 원본 파일의 모든 내용을 담지 않고 중요한 부분만 포함하고 있습니다.
import { debounce } from 'es-toolkit';
import { useCallback, useEffect, useState } from 'react';
import { getVerifyEmail } from '@/features/user';
function useSignUpForm() {
const [email, setEmail] = useState('');
const [emailValidationStatus, setEmailValidationStatus] = useState({ status: 'INITIAL' });
const checkEmailToVerify = useCallback(
debounce(async (emailToVerify: string) => {
try {
const response = await getVerifyEmail(emailToVerify);
setEmailValidationStatus(
response.exists
? { status: 'INVALID', message: '이미 사용 중인 이메일입니다.' }
: { status: 'VALID', message: '사용 가능한 이메일입니다.' },
);
} catch (error) {
setEmailValidationStatus({
status: 'INVALID',
message: '알 수 없는 오류가 발생했습니다.',
});
}
}, 1500),
[],
);
useEffect(() => {
if (email) {
checkEmailToVerify(email);
} else {
checkEmailToVerify.cancel();
}
}, [email, checkEmailToVerify]);
return {
email,
setEmail,
emailValidationStatus,
};
}
-
checkEmailToVerify
함수는debounce
로 감싸져 있어, 지정된 지연 시간(1500ms) 내에 연속된 호출이 발생하면 이전 호출을 취소하고 마지막 호출만 실행합니다. - 사용자가 이메일 입력란에 값을 입력하면
email
상태가 업데이트되고,useEffect
훅이 실행됩니다. -
checkEmailToVerify
함수는 입력이 완료된 후 지연 시간 후에 실행되어 서버로 유효성 검사 요청을 보냅니다. - 사용자가 입력을 지워 이메일이 빈 문자열이 될 경우, 예약된 함수 호출을 취소하여 불필요한 서버 요청을 방지합니다.
이를 통해 아래의 효과를 얻을 수 있습니다.
- 입력 중에는 서버에 요청을 보내지 않고, 사용자가 입력을 멈춘 후에만 요청을 보내므로 서버 부하를 줄입니다.
- 입력 도중에 발생할 수 있는 불필요한 오류 메시지를 방지하고, 정확한 시점에 유효성 검사를 수행하여 사용자에게 올바른 피드백을 제공합니다.
- Debouncing을 통해 입력 완료 후 자동으로 검증이 이루어져 사용자 편의성을 높입니다.
사용자가 좋아요 버튼을 빠르게 여러 번 클릭하더라도 일정 시간 내에 하나의 요청만 서버로 보내도록 합니다.
아래 코드에서는 Throttling을 직접 구현하지 않고 라이브러리를 통해 사용하고 있습니다. 또한 원본 파일의 모든 내용을 담지 않고 중요한 부분만 포함하고 있습니다.
import { useMutation } from '@tanstack/react-query';
import { throttle } from 'es-toolkit';
import { useCallback } from 'react';
function QuestionItem({ question }) {
const { addToast } = useToastStore();
const { sessionToken, sessionId, expired } = useSessionStore();
const { mutate: likeQuestionMutation, isPending: isLikeInProgress } = useMutation({
mutationFn: (params) =>
postQuestionLike(params.questionId, {
token: params.token,
sessionId: params.sessionId,
}),
onSuccess: (res) => {
addToast({
type: 'SUCCESS',
message: question.liked ? '좋아요를 취소했습니다.' : '질문에 좋아요를 눌렀습니다.',
duration: 3000,
});
updateQuestion({
...question,
...res,
});
},
onError: console.error,
});
const handleLike = useCallback(
throttle(
() => {
if (expired || !sessionToken || !sessionId || isLikeInProgress) return;
likeQuestionMutation({
questionId: question.questionId,
token: sessionToken,
sessionId,
});
},
1000,
{ edges: ['leading'] },
),
[expired, sessionToken, sessionId, isLikeInProgress, likeQuestionMutation],
);
return (
<button onClick={handleLike} disabled={isLikeInProgress}>
{question.liked ? '좋아요 취소' : '좋아요'} ({question.likesCount})
</button>
);
}
-
handleLike
함수는throttle
로 감싸져 있어, 지정된 시간 간격(1000ms) 내에 여러 번 호출되더라도 첫 번째 호출만 실행됩니다. - Throttling을 통해 연속된 클릭으로 인한 불필요한 서버 요청과 토스트 메시지의 중복 발생을 방지합니다.
이를 통해 아래의 효과를 얻을 수 있습니다.
- 빠른 연속 클릭으로 인한 다수의 요청을 제한하여 서버와 데이터베이스에 가해지는 부하를 줄입니다.
- 불필요한 토스트 메시지의 중복 표시를 방지하여 사용자에게 명확한 피드백을 제공합니다.
- Debouncing과 Throttling을 적절한 곳에서 사용하여 서버의 부하를 줄이고 사용자 경험을 개선하고자 했습니다.
- 다른 사용자 입력이나 이벤트 처리에서도 적용하여 전체적인 성능과 UX를 향상시킬 수 있다는 것을 경험할 수 있었습니다.
- 사파리 브라우저에서는 스크롤 종료 이벤트가 발생하지 않는데, 그런 부분에서 Debouncing을 활용할 수 있을 것 같습니다.
- 프론트엔드뿐만 아니라 서버 측에서도 서버 자원을 보호하기 위해 이러한 방식을 이용하는 등 추가적인 노력이 필요할 것으로 보입니다.
Debouncing과 Throttling은 ‘Ask-It’ 프로젝트에서 프론트엔드의 효율적인 서버 요청 관리와 사용자 경험 개선에 도움을 주었습니다. 회원가입 시 유효성 검사 요청의 빈도를 줄여 서버 부하를 낮추고, 사용자에게는 정확한 피드백을 제공할 수 있었습니다. 또한 좋아요 기능에서의 서버 부하와 UX 문제를 해결할 수 있었습니다.
이러한 기술들은 간단하지만 효과적이었으며, 앞으로의 개발에서도 적극 활용한다면 도움이 될 것으로 생각합니다.