다정한 타입스크립트는 저에게 자주 메시지를 보내는데요.이번에는 아래와 같은 메시지를 받았습니다.
회원가입 페이지의 회원가입 약관 동의를 받는 부분을 구현하고 있습니다.
회원가입에 필요한 사용자의 입력값은 다음과 같이 타입을 정의하고, useForm
에 해당 타입을 적용하여 사용했습니다.
// 타입 정의
export interface RegisterSchema {
email: string;
username: string;
password: string;
passwordCheck: string;
imgUrl: string;
termsAgreement: {
service: boolean;
privacy: boolean;
marketing: boolean;
};
}
// 다음과 같이 타입 적용
const { register, setValue } = useForm<RegisterSchema>();
약관 항목은 다음과 같이 상수로 관리하고 있습니다.
const TERMS_AND_CONDITIONS = [
{ id: 'service', title: '서비스 이용약관', required: true },
{ id: 'privacy', title: '개인정보 수집 및 이용 동의', required: true },
{ id: 'marketing', title: '마케팅 활용 동의', required: false },
];
TERMS_AND_CONDITIONS
의 값을 이용하기 위해 map
을 이용하여 구현합니다.
<CheckboxList>
{TERMS_AND_CONDITIONS.map((term) => {
const { id, title, required } = term;
const fieldName = `termsAgreement.${id}`;
return (
<li key={`terms-and-conditions-${id}`}>
<CheckboxInput
id={id}
type="checkbox"
checked={agreedToTerms[id]}
{...register(fieldName, { // 타입스크립트 오류 발생 위치
required: !!required,
onChange: handleOnToggleCheckbox,
})}
/>
</li>
);
})}
</CheckboxList>
이렇게 하면 타입스크립트 오류가 발생합니다. 어디에서 발생하냐면 저기 register
의 fieldName
이 적용된 곳에서요. 주석 보이시죠? 하하
오류는 글 상단에서 본 바로 그 오류입니다!
register
의 타입을 확인해보기~
register
는 usrForm
에 input
요소를 등록하는 역할을 합니다.UseFormRegister라는
함수 타입으로 정의되어 있습니다.이어서 UseFormRegister
타입 확인
TFieldName
이 나타납니다.TFieldName
은 register
할 input
의 name
을 나타내는 제너릭 타입으로, FieldPath<TFieldValues>
타입을 extends
한 타입입니다.<TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>
TFieldName
을 지정하지 않는다면, FieldPath<TFieldValues>
타입이 기본값으로 사용된다는 것을 의미합니다.TFieldName
은 register
의 첫 번째 매개변수인 input
요소의 name
을 나타내는 타입으로 사용됩니다.FieldPath<TFieldValues>
다음과 같이 타입이 정의되어 있습니다.
Path<TFieldValues>
는 TFieldValues
객체의 모든 키를 문자열 리터럴로 연결한 타입입니다.TFieldValues
객체의 키를 "key1.key2.key3...."와 같은 형태로 표현합니다.RegisterSchema
를 FieldPath
에 적용한다면 FieldPath<RegisterSchema>
는 "email" | "username" | "password" | "passwordCheck" | "imgUrl" | "termsAgreement" | "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing"
형식이 됩니다.TFieldValues
는 FieldValues
를 extends
한 타입인데요. FieldValues
를 확인해보면 다음과 같습니다.
string
타입이고, 값은 any
타입이므로 값으로는 어떤 타입이든 가능합니다.결국 register
의 첫 번째 매개변수인 input
요소의 name
의 타입은 string
이지만 Path<TFieldValues>
에 의해 TFieldValues
객체의 키를 문자열 리터럴로 연결하면서 "email" | "username" | "password" | "passwordCheck" | "imgUrl" | "termsAgreement" | "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing"
타입으로 타입이 좁혀진 것 같습니다.
제가 적용한 const fieldName = `termsAgreement.${id}`;
의 경우 "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing"
타입이 아닌 string
타입으로 추론되어 오류가 발생한 것입니다!! 끄아아아!!!!!!!!
그래서 해결 방법 중 하나는 RegisterSchema
에 인덱스 시그니처를 추가하는 것입니다.
export interface RegisterSchema {
email: string;
username: string;
password: string;
passwordCheck: string;
imgUrl: string;
termsAgreement: {
service: boolean;
privacy: boolean;
marketing: boolean;
};
[key: string]: any; // 인덱스 시그니처 추가
}
이렇게 하면 오류가 사라집니다. 그리고 오타나 잘못된 string
키를 사용해도 오류를 발생시키지 않습니다.
그래서 저는 다음과 같이 FieldPath<RegisterSchema>
의 형식에서 허용하는 유니온 타입을 정의하여 오류가 발생하지 않도록 했습니다.
// `FieldPath<RegisterSchema>`의 형식에서 허용하는 유니온 타입을 정의
type TermsAgreementField = 'termsAgreement.service' | 'termsAgreement.privacy' | 'termsAgreement.marketing';
// TermsAgreementField 타입 적용
<CheckboxList>
{TERMS_AND_CONDITIONS.map((term) => {
const { id, title, required } = term;
// fieldName의 타입을 TermsAgreementField 타입으로 단언
const fieldName = `termsAgreement.${id}` as TermsAgreementField;
return (
<li key={`terms-and-conditions-${id}`}>
<CheckboxInput
id={id}
type="checkbox"
checked={agreedToTerms[id]}
{...register(fieldName, { // 타입 단언으로 오류 발생하지 않음!
required: !!required,
onChange: handleOnToggleCheckbox,
})}
/>
</li>
);
})}
</CheckboxList>
타입 오류가 발생했을 때는 정의된 타입을 직접 확인하고 해당 타입이 어떤 타입인지 이해하는 것이 확인한 후 적절한 방법을 찾아내는 것이 중요한 것 같습니다.
d.ts
파일의 정의된 타입을 봐도 아직 모르는 부분이 많아 중간에 어려운 부분은 Chat GPT에게 질문하여 답변을 통해 이해했습니다.
그렇다고 모든 것을 이해한 것은 아니지만, 이전보다는 많이 이해했으니 만족입니다!
그리고 틀린 부분이 있다면 알려주세요. 헿