요청 대시보드 만들기 과제에서 사이드바와 토글 버튼의 경우 영역 밖을 누르면 드롭다운과 사이드바가 다시 사라지도록 만들어야 했다.
사이드바는 배경을 주어 해당 배경을 누르면 들어가도록 만들면 됐는데, 필터버튼의 경우 드롭다운에는 별도의 배경이 없었다.
두 군데에서 사용되었기 때문에 깔끔하게 커스텀 훅이나 utils함수로 빼서 공통으로 사용하고 싶었으나, 이 차이점 때문에 고민하다가 결국 따로 만들어 사용했다.
같은 과제를 수행했던 다른 팀 중 커스텀 훅을 정말 잘 사용한 팀이 있어, 어떻게 사용했는지 코드를 하나하나 뜯어보고 추후 리팩토링할 때 참고해보기로 했다.
마침 커스텀 훅으로 많이 사용되는 다른 기능들도 구현되어 있어 함께 살펴보았다.
커스텀 훅을 만들어 사용하는 것이 아직 익숙하지 않다. 하지만 좋은 코드를 작성하기 위해 꼭 필요한 부분이라고 생각했기 때문에, 프로젝트가 끝난 시점에서 다시한번 고민해보았다.
import { RefObject, useEffect, useRef } from 'react';
import { off, on } from '@utils/functions';
const defaultEvents = ['mousedown', 'touchstart'];
const useClickAway = <E extends Event = Event>(
ref: RefObject<HTMLElement | null>,
onClickAway: (event: E) => void,
events: string[] = defaultEvents
) => {
const savedCallback = useRef(onClickAway);
useEffect(() => {
const handler = (event: any) => {
const { current: el } = ref;
el && !el.contains(event.target) && savedCallback.current(event);
};
for (const eventName of events) {
on(document, eventName, handler);
}
return () => {
for (const eventName of events) {
off(document, eventName, handler);
}
};
}, [events, ref]);
};
export default useClickAway;
인자로 전달받은 요소에 원하는 event이름과 eventhandler가 될 함수를 추가하거나 제거하는 함수이다.
쉽게말해 El.addEventListener("click", onClick)
와 같이 이벤트를 등록하는 것을,
on(document, "click", onClick) 로 인자를 전달받으면
on 에서 document.addEventListener("click", onClick)으로 만들어주는 것이다.
export function on<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['addEventListener']> | [string | null, ...any]
): void {
if (obj && obj.addEventListener) {
obj.addEventListener(
...(args as Parameters<HTMLElement['addEventListener']>)
);
}
}
export function off<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['removeEventListener']> | [string | null, ...any]
): void {
if (obj && obj.removeEventListener) {
obj.removeEventListener(
...(args as Parameters<HTMLElement['removeEventListener']>)
);
}
}
const DropdownsRef = useRef(null);
const { isToggle, setState, onToggle } = useToggle({ initialState: false });
useClickAway(DropdownsRef, () => setState(false));
return (
<S.Form onClick={onToggle} checkedList={checkedList} ref={DropdownsRef}>
<span>
{filterTypeToKorean}
{checkedList > 0 && `(${checkedList})`}
</span>
<ArrowDropdown />
{isToggle && (
<Dropbox
filterType={filterType}
dataList={dataList}
setMethodList={setMethodList}
setMaterialList={setMaterialList}
/>
)}
</S.Form>
);
useToggle은 아래의 내용을 참고하자.
1. Dropbox 컴포넌트는 isToggle이 참일 때만 보인다.
2. 이 isToggle은 setState로 업데이트한다.
3. useClickAway
import { useCallback, useState } from 'react';
const useToggle = ({ initialState = false }) => {
const [isToggle, setState] = useState(initialState);
const onToggle = useCallback(() => setState(() => !isToggle), [isToggle]);
return { isToggle, onToggle, setState };
};
export default useToggle;
이렇게 사용했다.
const Toggle = forwardRef(
(
{
isToggle,
disabled,
onChange,
name,
children,
...props
}: Partial<ForwardProps>,
ref: ForwardedRef<HTMLInputElement>
) => {
const { isToggle: checked, onToggle } = useToggle({
initialState: isToggle,
});
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
onToggle();
onChange && onChange(e);
};
return (
<S.ToggleContainer {...props}>
<S.ToggleInput
type="checkbox"
checked={checked}
disabled={disabled}
onChange={handleChange}
ref={ref}
name={name}
/>
);
내부에 state안에 axios로 fetch해온 데이터를 넣은 뒤 리턴해주었다.
import { useEffect, useState } from 'react';
import axios from 'axios';
const useAxios = <T>(URL: string) => {
const [state, setState] = useState<T>();
useEffect(() => {
const request = async () => {
const { data } = await axios(URL);
setState(data);
};
request();
}, []);
return state;
};
export default useAxios;
사용은 이렇게 했다.
interface ICardData {
id: number;
title: string;
client: string;
due: string;
count: number;
amount: number;
method: string[];
material: string[];
status: statusType;
}
const data = useAxios<ICardData[]>(
'url'
);