사용하는것에는 처음에는 러닝커브가 있으나, 점점 익숙해진다.
그리고 리랜더를 최대한 잘 관리할수잇는것이 장점.
여러 값들을 컴포넌트로 별로 나누면, 불필요한 렌더가 안되게 되어서 좋다.
근데 얘도 하나의 Context로 사용하는것이다보니,
만약 store 급의 범위를 가지게 되면, 데이터를 어떻게 해야할지,
또는 이중관리를 해야할지 생각을 하게된다.
const methods = useForm<FormValues>({
resolver: yupResolver(
registerAccountSchema,
) as unknown as Resolver<FormValues>,
context: { hasTradeGroupC },
//컨택스트로 내부에서 조건문으로 사용
shouldFocusError: false,
shouldUnregister: false,
defaultValues: {
//..데이터어어
}
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(onSubmit)}
>
//...
</form>
</FormProvider>
const {
control,
formState: { errors },
register, // register도 꺼낼 수 있음
watch, // watch도 가능
setFocus,
} = useFormContext<FormValues>();
const {
fields: infoListField,
append: infoListAppend,
remove: infoListRemove,
update: infoListUpdate,
} = useFieldArray({
control,
name: "rightInfo.infoList",
});
const companyRegNo = useWatch({
control,
name: "leftInfo.info.regNo",
defaultValue: "", // 옵셔널
});
return (
<>
//...
{infoListField.map((item, index) => (
<React.Fragment key={`infoListField{item?.id}`}>
<Controller
key={`infoListFieldController${index}`}
name={`rightInfo.infoList.${index}`}
control={control}
render={({ field }) => {
//...
const {
control,
formState: { errors },
register, // register도 꺼낼 수 있음
watch, // watch도 가능
setValue,
} = useFormContext<FormValues>();
<Controller
name="leftInfo.info.type"
control={control}
render={({ field }) => (
<KeyValueSelectBox
options={options}
value={field.value}
onChange={field.onChange}
size="95px"
/>
)}
/>
const {
control,
formState: { errors },
register, // register도 꺼낼 수 있음
watch, // watch도 가능
setValue,
} = useFormContext<FormValues>();
const { field: NameField } = useController<
FormValues,
"leftInfo.info.Name"
>({
name: "leftInfo.info.Name",
control,
rules: { required: true },
});
import { object, string, array } from "yup";
export const options = {
"0": "Option A",
"1": "Option B",
"2": "Option C",
"3": "Option D",
"4": "Option E",
} as const;
const optionKeys = Object.keys(options) as Array<keyof typeof options>;
export const formSchema = object({
basicInfo: object({
selectField: object({
choice: string()
.oneOf(optionKeys, "올바른 옵션을 선택해주세요")
.required("옵션 선택은 필수입니다"),
label: string().trim().required("라벨은 필수입니다"),
id: string()
.trim()
.max(12, "최대 10자만 입력 가능합니다")
.matches(/^[0-9-]+$/, "숫자와 하이픈(-)만 허용됩니다")
.test(
"length-10-digits",
"하이픈 제외 숫자 10자리여야 합니다",
(v = "") => v.replace(/-/g, "").length === 10
)
.required("ID는 필수입니다"),
ownerName: string().required("이름은 필수입니다"),
subId: string()
.trim()
.max(4, "최대 4자만 입력 가능합니다")
.matches(/^\d*$/, "숫자만 입력해주세요"),
corpId: string()
.trim()
.max(12, "최대 12자만 입력 가능합니다")
.matches(/^\d*$/, "숫자만 입력해주세요"),
typeCode: string().max(10, "최대 10자만 입력 가능합니다"),
format: string().trim(),
category: string().trim(),
phones: array()
.of(
string()
.trim()
.max(16, "최대 16자만 입력 가능합니다")
.matches(/^\d*$/, "숫자만 입력해주세요")
)
.ensure()
.min(1, "최소 하나 이상의 전화번호가 필요합니다"),
faxNumber: string()
.trim()
.max(16, "최대 16자만 입력 가능합니다")
.matches(/^\d*$/, "숫자만 입력해주세요"),
address: string().trim().max(256, "최대 256자만 입력 가능합니다"),
}).required(),
localeInfo: object({
labelEn: string().matches(/^[A-Za-z]*$/, "영문 알파벳만 입력해주세요"),
addressEn: string().matches(
/^[A-Za-z0-9]*$/,
"영문/숫자만 입력해주세요"
),
ownerNameEn: string().matches(/^[A-Za-z]*$/, "영문 알파벳만 입력해주세요"),
}).required(),
financialInfo: object({
names: array().of(string().trim()).ensure(),
number: string()
.trim()
.min(10, "최소 10자리입니다")
.max(14, "최대 14자리입니다")
.matches(/^\d*$/, "숫자만 입력해주세요"),
bankName: string(),
bankCode: string(),
}).required(),
extraInfo: object({
url: string(),
notes: string(),
tag: string(),
}).required(),
}).required(),
details: object({
items: array()
.of(
object({
image: string().ensure(),
organization: string().ensure(),
contact: string()
.trim()
.max(16, "최대 16자만 입력 가능합니다")
.matches(/^[0-9-]*$/, "숫자와 하이픈만 허용됩니다")
.ensure(),
phone: string()
.trim()
.max(16, "최대 16자만 입력 가능합니다")
.matches(/^[0-9-]*$/, "숫자와 하이픈만 허용됩니다")
.ensure(),
fax: string()
.trim()
.max(16, "최대 16자만 입력 가능합니다")
.matches(/^[0-9-]*$/, "숫자와 하이픈만 허용됩니다"),
email: string()
.trim()
.test(
"email-if-filled",
"유효한 이메일 형식(예: a@b.com)이어야 합니다",
(v = "") => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
)
.ensure(),
task: string().ensure(),
regId: string().ensure(),
remark: string().ensure(),
name: string()
.ensure()
.test(
"name-required-if-other",
"하나 이상의 정보 입력 시 이름이 필요합니다",
function (v = "") {
const parent = this.parent as Record<string, any>;
const others = [
parent.organization,
parent.contact,
parent.phone,
parent.fax,
parent.email,
parent.task,
parent.regId,
parent.remark,
];
const anyFilled = others.some((x) => x?.trim() !== "");
return !anyFilled || v.trim() !== "";
}
),
}).required()
)
.min(1, "최소 하나 이상의 항목이 필요합니다")
.required(),
}).required(),
});
아래와같이, schema에 넣어주면 들어오는 값을 확인해볼수있다.
.test("debug-hasC", "debug", function (v) {
// eslint-disable-next-line no-console
console.log(
"🧪 hasTradeGroupC in context:",
this?.options?.context?.hasTradeGroupC,
);
return true; // 항상 통과
})
schema에 when절 추가하여, 부분적으로 요구사항 및 필수값을 수정할수있다.
companyRegNo: string()
.trim()
.max(
10,
intl.formatMessage({ id: "tr.register.validation.hh" }),
)
.when("$hasTradeGroupC", {
is: Boolean, // ← truthy면 필수
then: (s) =>
s.required(
intl.formatMessage({
id: "tr.register.validation.gg",
}),
),
otherwise: (s) => s.notRequired(),
}),
const {
control,
formState: { errors },
register,
watch,
getValues,
setValue,
} = useFormContext<FormValues>();
const {
fields: addressField,
append: addressAppend,
remove: addressRemove,
update: addressUpdate,
} = useFieldArray({
control,
name: "leftInfo.info.address",
});
1️⃣ update
fields) 전체가 갱신되어, 배열을 순회(render)하는 모든 Controller가 다시 렌더링됩니다.2️⃣ setValue
.addressValue)만 변경할 때 사용➡️ 따라서,
update는 전체, setValue는 부분” ✅setValue는 배열 전체를 리렌더” ❌ (부분 리렌더가 이루어집니다)useController.ts:144 Uncaught TypeError: elm.focus is not a function
at Object.focus (useController.ts:144:1)
useController가 제공하는 ref는 실제 DOM input 요소에만 연결되어야 하는데, 커스텀 Component나 다른 라이브러리 Component에서 React.forwardRef를 지원하지 않는다면, <Controller />나 register API 사용을 고려해보아야 한다.