Toss의 오픈소스 라이브러리인 slash에 DebounceClick 이라는 유틸 컴포넌트가 있습니다. 어떤 방식으로 Slash에서는 구현을 했을까 궁금하여 코드를 분석해보며 로직이나 타입에 대해 배울것이 많아 정리해보려고 합니다.
분석에 앞서 Debounce에 대해서 간단하게 알아 봅시다.
Debounce는 자바스크립트에서 특정 이벤트가 일정 시간 동안 반복해서 발생하는 것을 제어하는 기법입니다. 주로 사용자가 입력 필드에 타이핑을 할 때마다 발생하는 이벤트나 스크롤 이벤트와 같이 자주 발생하는 이벤트를 처리할 때 유용합니다. debounce를 사용하면 특정 시간 동안 이벤트가 발생하지 않을 때까지 이벤트 핸들러가 호출되지 않도록 할 수 있습니다.
대표적인 예시로 velog의 검색 필터링이 있습니다.
velog에서는 검색어를 기반으로 포스트를 graphql로 요청하는데요. 검색어를 치고 몇초뒤에 api가 호출되는것을 확인할 수 있습니다. 또한 검색어를 계속 치고있을경우 api 호출이 발생하지 않는것 또한 볼 수 있습니다.
이렇게 Debounce를 사용하여 리소스를 서버에 무리하게 요청하는 문제를 막을 수 있습니다.
그럼 본격적으로 slash에서는 어떤식으로 DebounceClick 컴포넌트를 구현하였는지 알아봅시다.
DebounceClick 컴포넌트는
click event에 debounce를 적용할 수 있는 유틸 컴포넌트입니다.
라고 설명을 하고 있습니다.
내부 코드는 다음과 같습니다.
import { Children, cloneElement, ReactElement } from 'react';
import { useDebounce } from '../../hooks/useDebounce';
/** @tossdocs-ignore */
interface Props {
/**
* @description 이벤트를 묶어서 한번에 보낼 시간으로 ms 단위
* e.g.) 200ms 일 때, 200ms 안에 발생한 이벤트를 무시하고 마지막에 한번만 방출합니다.
*/
wait: Parameters<typeof useDebounce>[1];
options?: Parameters<typeof useDebounce>[2];
children: ReactElement;
/**
* @default 'onClick'
* @description 이벤트 Prop 이름으로 'onClick' 이름 외로 받을 때 사용합니다.
* e.g. "onCTAClick", "onItemClick" ...
*/
capture?: string;
}
export function DebounceClick({ capture = 'onClick', options, children, wait }: Props) {
const child = Children.only(children);
const debouncedCallback = useDebounce(
(...args: any[]) => {
if (child.props && typeof child.props[capture] === 'function') {
return child.props[capture](...args);
}
},
wait,
options
);
return cloneElement(child, {
[capture]: debouncedCallback,
});
}
코드는 많이 복잡하지는 않네요.
DebounceClick 컴포넌트에 delay시간, 옵션 등을 props로 받고 child의 capture(onClick)에 해당하는 함수를 useDebounce에 할당하는 모습입니다. 그 뒤 child의 컴포넌트를 cloneElement 함수를 통해서 return 해주고 있네요. 여기서 React의 몇몇 함수와 타입이 낯선것들이 있었는데요. 하나하나 분석해 봅시다.
함수 타입의 매개변수 타입을 추출하는 TS 내장 유틸리티 타입
개발을 진행하며 특정 함수의 매개변수와 동일한 타입을 가져오고 싶을 경우가 있잖아요? 이럴때 사용하면 유용할 것 같습니다. 여기서는 wait과 options 에서 사용되었는데요. useDebounce
함수의 파라미터의 1번째 인자와 2번째 인자의 타입을 받아옴을 알 수 있습니다.
React 라이브러리에서 사용되는 기본적인 타입 중 하나로, React 컴포넌트가 반환하는 요소를 표현하는 타입
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
React에서 렌더링 하기위해서 사용되는 element를 표현하는 타입입니다. 위 예시에서는 children을 필수적으로 받으며 ReactElement 하나만 받도록 되어있네요.
유사하게 ReactNode 라는 타입도 존재하는데요. ReactNode는 React가 렌더링할 수 있는 모든 종류의 값을 나타내며 좀 더 포괄적인 타입입니다.
ReactNode에는 다음과 같은 타입을 허용합니다
여기까지 DebounceClick 의 Props 타입에 대해서 정리해 보았습니다. 다음으로는 사용한 React 유틸리티를 알아봅시다.
export function DebounceClick({ capture = 'onClick', options, children, wait }: Props) {
const child = Children.only(children);
const debouncedCallback = useDebounce(
(...args: any[]) => {
if (child.props && typeof child.props[capture] === 'function') {
return child.props[capture](...args);
}
},
wait,
options
);
return cloneElement(child, {
[capture]: debouncedCallback,
});
}
React의 Children는 React 컴포넌트의 자식 요소를 다루기 위한 유틸리티입니다.이를 통해 부모 컴포넌트에서 자식 컴포넌트를 효율적으로 다루고 관리할 수 있습니다. Children이 사용할 수 있는 메서드는 여러가지 존재하지만 이곳에서 사용된 only에 대해서만 알아봅시다.
자식 요소가 정확히 하나인지 확인하고, 그렇지 않으면 오류를 발생시킵니다. DebounceClick 에서는 Children.only(children)
를 통해 children이 항상 1개임을 명시하기 위해 사용한 것 같습니다. Props에서 children: ReactElement;
를 통해 2개 이상의 child가 있을 경우 타입에러가 발생하지만 js를 위해서 한번 더 사용한 것으로 보입니다.
cloneElement는 React에서 기존의 React 요소를 복제하고, 추가적으로 props를 변경하거나 덮어쓰는 기능을 제공하는 유틸리티 함수입니다. 이를 사용하면 기존 요소의 구조를 유지하면서 새로운 속성을 추가하거나 기존 속성을 수정할 수 있습니다.
기본적인 문법은 다음과 같습니다.
cloneElement(
element, // 복제할 요소
[props], // 새로 추가하거나 덮어쓸 props
[...children] // element의 새로운 자식 요소 (선택 사항)
)
DebounceClick
에서는 기존의 child를 복사하되, capture(onClick)에 debounce 함수만 변경하는 방식으로 구현을 하였습니다. 결국 DebounceClick 컴포넌트는 Wrapper의 껍대기 역할만 하고 실제 child인 button에 debounce된 함수를 넣어주는 역할이 되겠네요.
이렇게 Debounce에 대해서 간단하게 알아보며 toss/slash에서 사용하고 있는 DebounceClick가 어떤식으로 구현되어 있는지 보았습니다. 다양한 타입과 React 유틸리티 함수를 보며 적용해보고 싶은 생각이 드는데요. 읽어주셔서 감사합니다! 🙇🏻♂️