오늘은 프로젝트에서 여러 개의 인자를 타입가드하다가 만난 오류들과 그것들을 어떻게 해결했는지 이야기해보려고한다!
🚨 문제 사항
1) 목적 : Dropdown 컴포넌트에서 제어/비제어 컴포넌트 패턴을 모두 지원하면서, 반복되는 타입 체크 로직을 타입가드 함수로 분리하고자 함
2) 최초 구현 방법 : typeof isOpen === 'boolean' && typeof setIsOpen === 'function' 조건을 직접 사용
3) 문제 내용 : 타입가드 함수로 분리할 때 변수의 타입 정보가 제대로 좁혀지지 않음
4) 문제 원인 : 객체 리터럴 전달 시 참조 불투명성 문제와 다중 인자 타입가드 미지원 이슈
5) 해결 방안 : 객체를 먼저 변수로 정의하고, 해당 객체에 대해 타입 검증 후 사용
⚠️ 문제 상세 내용
Dropdown Menu 컴포넌트를 만드는 중이었다. 컴포넌트 사용자가 드롭다운의 상태를 직접 제어하고 싶을 때는(제어 컴포넌트 패턴) 외부에서 상태를 주입할 수 있고, 그렇지 않을 때는 내부 상태로 자동 관리되는 방식(비제어 컴포넌트 패턴)으로 동작하게 만들면 두 가지 사용 케이스를 모두 지원하려고 한다. 사용자가 isOpen
과 setIsOpen
값을 넘겨주면 해당 값을 사용하고, 그렇지 않으면 컴포넌트 내부에서 정의한 isOpenInternal
상태를 사용해 드롭다운 메뉴를 펼치고 닫는 방식으로 구현한다.
처음에는 아래와 같이 isOpen
값이 있고 setIsOpen
값이 있는지 간단하게 확인하였다:
export type DropdownStateProps = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>> | ((isOpen: boolean) => void);
};
export type DropdownContextProviderProps = Partial<DropdownStateProps> & {
children: ReactNode;
onClose?: () => void;
};
const DropdownContextProvider = ({ children, isOpen, setIsOpen, onClose }: DropdownContextProviderProps) => {
const [isOpenInternal, setIsOpenInternal] = useState(false);
const openMenu = () => {
if (typeof isOpen === 'boolean' && typeof setIsOpen === 'function') setIsOpen(true);
else setIsOpenInternal(true);
};
const closeMenu = () => {
if (typeof isOpen === 'boolean' && typeof setIsOpen === 'function') setIsOpen(false);
else setIsOpenInternal(false);
if (typeof onClose === 'function') onClose();
};
return (
<dropdownStateContext.Provider value={{ isOpen: isOpen ?? isOpenInternal }}>
<dropdownActionContext.Provider value={{ openMenu, closeMenu }}>{children}</dropdownActionContext.Provider>
</dropdownStateContext.Provider>
);
};
export default DropdownContextProvider;
코드를 보다 보니 typeof isOpen === 'boolean' && typeof setIsOpen === 'function'
이라는 조건이 반복되고 있었다.
겨우 2번이라서 타입 가드를 해주는 유틸함수로 만들지 않아도 괜찮지만 나는 만들고 싶었다!
그럼 해야지.
🔍 시도 1: 객체로 타입 가드 - 객체로 인한 참조 불투명성 문제
처음에는 다음과 같이 타입 가드 함수를 만들었다:
function isValidDropdownState(props: Partial<DropdownStateProps>): props is DropdownStateProps {
return typeof props.isOpen === 'boolean' && typeof props.setIsOpen === 'function';
}
const openMenu = () => {
if (isValidDropdownState({isOpen, setIsOpen})) setIsOpen(true);
else setIsOpenInternal(true);
};
const closeMenu = () => {
if (isValidDropdownState({isOpen, setIsOpen})) setIsOpen(false);
else setIsOpenInternal(false);
if (typeof onClose === 'function') onClose();
};
!!!문제 발생!!! 타입 가드 함수에 {isOpen, setIsOpen}
을 전달할 때, 이건 새로운 객체 리터럴을 생성되고, 이 새 객체는 원래의 isOpen
과 setIsOpen
값을 포함하지만, TypeScript 입장에서는 원본 props와는 다른 별개의 객체로 취급된다.
결과적으로:
- 타입 가드 함수는 매개변수로 전달된 새 객체의 타입을 좁혀주지만, 원본
isOpen
과setIsOpen
변수의 타입은 좁혀주지 않는다.. - 따라서 조건문 내에서 타입 정보가 원본 변수로 "흘러들어가지(flow)" 않는 문제가 발생했다.
🔍 시도 2: 개별 인자로 타입 가드 - 다중 인자 타입 가드 지원 안 함
객체로 만들면 참조 불투명성 때문에 문제가 발생한다는 것을 깨달았기 때문에, 인자를 개별적으로 받는 방식으로 접근했다:
function isValidDropdownState(
isOpen: DropdownStateProps['isOpen'] | undefined,
setIsOpen: DropdownStateProps['setIsOpen'] | undefined
): isOpen is DropdownStateProps['isOpen'] & setIsOpen is DropdownStateProps['setIsOpen'] {
return typeof isOpen === 'boolean' && typeof setIsOpen === 'function';
}
const openMenu = () => {
if (isValidDropdownState(isOpen, setIsOpen)) setIsOpen(true);
else setIsOpenInternal(true);
};
const closeMenu = () => {
if (isValidDropdownState(isOpen, setIsOpen)) setIsOpen(false);
else setIsOpenInternal(false);
if (typeof onClose === 'function') onClose();
};
문제 발생! 타입스크립트에서는 여러 인자에 대한 타입 가드가 아직 지원되지 않았다.
GitHub 이슈를 찾아보니 2018년에 이 기능에 대한 요청이 있었지만, 아직 구현되지 않았다:
✅ 해결책: 객체를 생성하고 해당 객체를 사용
고민 끝에 깨달은 해결책은 이거였다:
const externalDropdownState = {isOpen, setIsOpen};
const isValidDropdownState = (state: Partial<DropdownStateProps>): state is DropdownStateProps => {
return typeof state.isOpen === 'boolean' && typeof state.setIsOpen === 'function';
};
const openMenu = () => {
if (isValidDropdownState(externalDropdownState)) externalDropdownState.setIsOpen(true);
else setIsOpenInternal(true);
};
const closeMenu = () => {
if (isValidDropdownState(externalDropdownState)) externalDropdownState.setIsOpen(false);
else setIsOpenInternal(false);
if (typeof onClose === 'function') onClose();
};
생각해보니 문제는 새 객체의 타입은 좁혀지지만, 원본 isOpen
과 setIsOpen
변수의 타입이 좁혀지지 않는 것이었다.
그렇다면, 객체를 먼저 변수로 정의한 다음, 그 객체의 타입을 검증하고, 타입이 좁혀진 객체의 setIsOpen
을 사용하면 되는 것이였다.
이렇게 함으로써 다중 인자에 대한 타입 가드을 해결할 수 있었다.
후기
이때까지 1개의 인자에 대한 타입 가드만 사용했어서 처음 겪어보는 문제였다!
현재 수정된 결과는 렌더링 될때마다 객체가 새로 생성됨으로, 차라리 각각의 인자를 검색하는 개별 타입 가드 함수를 만들까 고민중이다.
그래도 다음번에도 다중인자에 대한 타입가드를 시도해야한다면 먼저 이 이슈가 닫혔는지, 다중 인자 타입가드를 지원하는지 확인하고 그렇지않다면 인자 단일화를 통해서 해결할 것 이다!
'💻 개발 > toy project' 카테고리의 다른 글
[트러블 슈팅] Supabase 트리거 디버깅: participant_count가 업데이트되지 않는 미스터리 (0) | 2025.03.27 |
---|---|
[트러블 슈팅] 배포하니까 에러가 바뀌었다 (ENV를 잘 작성하자) (0) | 2025.03.12 |
Next.js 프로젝트에서 styled-components 에서 tailwindCSS로 바꾼 이유 (0) | 2025.03.03 |
Zustand와 Axios: React 컴포넌트 외부에서 상태 관리하는 방법 (0) | 2025.02.25 |
json-server에서 관계형 데이터베이스 구축 및 사용 방법 (0) | 2025.02.25 |