널리 쓰이는 공통 컴포넌트란 무엇일까요?
제가 생각하는 공통 컴포넌트란 특정 도메인에 종속되어있는 컴포넌트가 아닌 프로젝트 전반에 걸쳐 사용되는 컴포넌트를 말합니다.
예를 들면 Checkbox 또는 Radio , Switch 등등의 컴포넌트를 말합니다.
회사에 입사하고서 첫 배정받은 작업은 기존 레거시 컴포넌트의 개선 작업이였습니다. 이 내용을 공유 드리고자 합니다.
제어상태로만 동작하는 컴포넌트
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
}
function Checkbox({ ...props }: CheckboxProps) {
const { checked, onChange } = props;
return (
<input
type='checkbox'
checked={checked}
onChange={e => onChange(e.target.checked)}
/>
);
}
해당 Checkbox 컴포넌트의 문제점은 무엇일까요?
문제점은 부모 컴포넌트에서 checked 상태와 상태변경함수를 props로 반드시 내려주어야만 동작한다는점 이었습니다.
또한 반드시 제어 컴포넌트로써만 동작하기 때문에 부모 컴포넌트의 상태가 변경되면 하위 컴포넌트들 모두 리렌더링이 발생할 수 있다는 사이드이펙트 또한 존재합니다.
비제어 상태 컴포넌트로 리팩토링
import { useState } from 'react';
interface CheckboxProps {
checked?: boolean;
onChange?: (checked: boolean) => void;
}
function Checkbox({ ...props }: CheckboxProps) {
const { checked: propsChecked, onChange: propsOnChange } = props;
const [isChecked, setIsChecked] = useState(false);
const isControlled = propsChecked !== undefined;
const checked = isControlled ? propsChecked : isChecked;
const onChange = isControlled ? propsOnChange : setIsChecked;
return <input type='checkbox' checked={checked} onChange={(e)=>onChange(e.target.checked)} />;
}
무엇이 개선 되었을까요?
이제 이 체크박스는 부모로부터 checked 상태를 받지 않아도 체크박스를 눌렀을때 정상적으로 동작이 될 것입니다.
내부적으로 체크상태를 관리하는 코드가 있기 때문입니다. 그렇기 때문에 비제어 컴포넌트로써 사용할 수 있게 됩니다.
제어, 비제어 상태를 관리하는 공통 커스텀 훅 구현
커스텀훅에 사용될 타입을 정의합니다.
interface ControllableState<T> {
props?: T;
defaultProps?: T;
onChange?: (state: T) => void;
}
- props : 부모로부터 받게될 props ( ex: checkbox 라면 checked)
- defaultProps :부모로부터 받게될 props ( ex: checkbox 라면 defaultChecked)
- onChange : 부모로부터 받게될 상태변경 함수
function useControllableState<T>({
props,
defaultProps,
onChange = () => {},
}: ControllableState<T>) {
const [unControlledState, setUnControlledState] = useState<T | undefined>(
defaultProps,
);
const isControlled = props !== undefined;
const value = isControlled ? props : unControlledState;
const setValue: Dispatch<SetStateAction<T | undefined>> = useCallback(
(nextState) =>
isControlled
? onChange?.(nextState as T)
: setUnControlledState(nextState),
[isControlled, onChange, setUnControlledState],
);
return [value, setValue] as const;
}
export default useControllableState;
부모로부터 받은 prop이 있다면 이는 isControlled로 인해 부모로 받은 상태가 사용되어 제어 컴포넌트가 될것이고
부무로부터 받은 prop이 없다면 unControlledState가 사용되어 비제어 컴포넌트 처럼 동작하게 될것입니다.
해당 커스텀훅을 props를 제네릭하게 넘겨받을수 있도록 만들여져 있기 때문에 체크박스 뿐만 아니라 토글 스위치 또는 체크박스 그룹, 라디오 그룹 등에도 다양하게 사용이 가능합니다.
개선후
function Checkbox({ ...props }: CheckboxProps) {
const { checked, defaultChecked, onChange } = props;
const [isChecked, setIsChecked] = useControllableState({
props: checked,
defaultProps: defaultChecked,
onChange,
});
return (
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
);
}
export default Checkbox;
해당 코드에 대한 동작 확인은
https://stackblitz.com/edit/vitejs-vite-7vfweg?file=src%2FApp.tsx&view=editor 에서 확인할 수있습니다.
'React' 카테고리의 다른 글
React Developer Tools를 통한 웹 최적화 진행 해보기 (0) | 2023.11.08 |
---|---|
React createPortal을 사용하여 Modal 구현 (0) | 2023.10.10 |
React-Query - onClick 이벤트로 useQuery data fetching (0) | 2023.10.09 |
React- 재사용성과 확장성을 높이는 리액트 컴포넌트 설계 하기 (1) | 2023.09.25 |
React - 리액트 해시태그 구현 (0) | 2023.09.23 |