본문 바로가기

TIL

TIL - 회원가입 이메일 중복 확인 로직 개선 - 실시간 검사 디바운싱 적용 (트러블 슈팅)

배경

  • 기존: 중복 확인 버튼을 통한 이메일 중복 확인
  • 변경 필요성: UI/UX 개선을 위해 버튼이 제거되고 실시간 중복 확인 필요

 

문제 해결 과정

1. 초기 접근: 실시간 중복 확인 구현

  • 구현: 입력 값 변경 시마다 서버에 중복 확인 요청
  • 문제점: 매 입력마다 서버 요청 발생, 성능 저하 우려


2. 디바운싱 적용 시도

  • 목적: 불필요한 서버 요청 감소
  • 구현: lodash의 debounce 함수 사용
const debouncedCheckEmail = debounce((value) => {
  // 이메일 중복 확인 로직
}, 1000);
  • 결과:  매 입력마다 서버 요청 발생

변경 마다 중복 확인 요청 중

 


3. 메모이제이션 시도 (useCallback, useMemo)

  • 목적: 불필요한 함수 재생성 방지, (리렌더링마다 디바운싱 함수가 생성되는 것을 원인으로 생각 함)
const debouncedCheckEmail = useCallback(
  debounce((value) => {
    // 이메일 중복 확인 로직
  }, 1000),
  []
);
  • 결과: 여전히 실패


4. 배포 환경 테스트

  • 가설: 개발 환경과 배포 환경의 차이로 인한 문제일 수 있음
  • 결과: 동일한 문제 발생

5. 커스텀 훅 (useDebounce) 생성

import { debounce, DebouncedFunc } from 'lodash';
import { useCallback, useEffect, useRef } from 'react';

export function useDebounce<T extends (...args: any[]) => any>(callback: T, delay: number): DebouncedFunc<T> {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  const debouncedFn = useRef<DebouncedFunc<T>>();

  useEffect(() => {
    debouncedFn.current = debounce((...args: Parameters<T>) => {
      callbackRef.current(...args);
    }, delay);

    return () => {
      debouncedFn.current?.cancel();
    };
  }, [delay]);

  return useCallback(
    (...args: Parameters<T>) => {
      debouncedFn.current?.(...args);
    },
    [debouncedFn],
  ) as DebouncedFunc<T>;
}
  • 코드 설명:
    • 타입 설명:
      • T extends (...args: any[]) => any: 제네릭 타입 T는 임의의 인자를 받고 임의의 값을 반환하는 함수 타입
      • DebouncedFunc<T>: lodash의 debounce 함수로 래핑된 함수 타입, 원본 함수의 시그니처를 유지하면서 cancel, flush 등의 메서드를 추가로 가짐
      • Parameters<T>: T 함수 타입의 매개변수 타입을 추출
    • 주요 개선 사항:
      1. callbackRef 사용: callback 함수의 최신 참조를 유지
      2. 제네릭 타입 활용: 다양한 함수 시그니처에 대응하도록 타입 설정
      3. useCallback 사용: 디바운스된 함수를 메모이제이션하여 불필요한 리렌더링을 방지하고 성능을 최적화
  • 사용:
const debouncedCheckEmail = useDebounce((value: string) => {
  // 이메일 중복 확인 로직
  checkDuplicate('email', value)
    .then((isAvailable) => {
      // 상태 업데이트 로직
    })
    .catch((error) => {
      console.error('이메일 중복 확인 중 오류 발생:', error);
      // 에러 처리 로직
    });
}, 1000);
  • 결과:

디바운싱이 적용된 터미널

 

해결의 핵심 !

  1. useRef를 통한 안정적인 참조 유지
    • 컴포넌트 리렌더링과 무관하게 디바운스 함수의 참조 유지
  2. useEffect를 이용한 디바운스 함수 생성 및 관리
    • callback과 delay 변경 시에만 새로운 디바운스 함수 생성
    • 컴포넌트 언마운트 시 디바운스 함수 정리
  3. useCallback을 통한 안정적인 함수 반환
    • 항상 최신의 디바운스 함수를 참조하는 메모이제이션된 함수 제공
  4. 클로저와 리액트 렌더링 사이클의 상호작용
    • useEffect 내부의 클로저가 최신 callback과 delay 값 캡처
    • 리렌더링 발생 시에도 이전 값 유지

학습 포인트

  1. 리액트의 렌더링 사이클과 클로저의 관계
    • 컴포넌트 리렌더링 시 함수와 변수의 재생성 이해
    • 클로저를 통한 이전 렌더링 값 유지 메커니즘
  2. useRef, useEffect 고급 활용
    • useRef: 리렌더링과 독립적인 값 저장
    • useEffect: 사이드 이펙트 관리 및 클로저 형성

 

결론

이번 경험을 통해 리액트의 내부 동작 원리와 자바스크립트의 클로저 개념이 실제 문제 해결에 어떻게 적용되는지 경험하고 학습하게 되었습니다. 단순히 기능 구현을 넘어, 성능과 사용자 경험을 고려한 최적화의 중요성을 배웠습니다. 또한, 커스텀 훅을 통해 재사용 가능한 로직을 분리함으로써 코드의 구조화와 유지보수성을 향상시키는 방법을 익혔습니다!