Sicilian - 전역 상태 기반 form 상태 관리 도구
이 문서는 sicilian@3.0.8를 기반으로 작성되었습니다
sicilian은 전역 상태를 기반으로 동작하는 form 상태 관리 도구입니다. 타입스크립트를 지원하며 React.js 18 버전 이상에서 사용할 수 있습니다.
프론트엔드 분야에서 form 상태 관리 도구로 널리 쓰이는 react-hook-form은 ref를 기반으로 동작합니다. 이 때문에 컴포넌트를 forwardRef로 감싸거나, 라이브러리에서 제공하는 useFormContext를 사용해야 합니다. React.js를 사용하는 개발자로서 이러한 제약 사항이 여러모로 불편하게 느껴졌습니다.
sicilian은 이러한 불편함을 해결하기 위해 전역 상태를 기반으로 개발되었습니다. 이는 sicilian이 각각의 input을 state로 관리하며, 제어 컴포넌트 방식으로 form을 작성하도록 도움을 줄 수 있다는 뜻이기도 합니다. 또한, 전역 상태를 기반으로 하기에 context API 혹은 여타 전역 상태 관리 도구의 도움 없이도 원하는 컴포넌트 어디에서나 호출하여 사용할 수 있습니다.
What's new in sicilian@3.0.0
- playDragon 함수가 CreateForm 클래스로 변경되었습니다.
- initValue가 반드시 제공되어야 했던 이전과 달리 CreateForm 클래스의 파라미터는 모두 선택적으로 제공할 수 있습니다.
- 이전까지의 sicilian은 텍스트 위주의 input type만을 처리하였습니다. 3 버전부터는 type="checkbox"와 type="file"을 포함한 모든 종류의 input을 처리할 수 있습니다. 이를 위해 validate에 input이 check 상태인지 확인할 수 있는 'checked'가 추가되었습니다.
- initValue를 선택적으로 제공하게 되면서, register 함수의 파라미터가 변경되었습니다. 자세한 내용은 아래에서 자세히 다뤄보겠습니다.
- validateOn에 'change'가 추가되었습니다. 이를 사용하여 onChange 이벤트가 트리거 될 때마다 input value를 validate할 수 있습니다. 다만, 이 방법은 어플리케이션에 부담을 줄 수 있으므로 사용에 주의가 필요합니다.
- form 상태와 error 상태를 관리하기 위한 함수의 이름이 setForm, FormState, setError, ErrorState에서 getValues, setValues, getErrors, setErrors로 변경되었습니다.
install 및 import
npm install sicilian
import { CreateForm } from "sicilian";
import { useForm } from "sicilian/useForm";
import { SicilianProvider, useSicilianContext } from "sicilian/provider";
CreateForm
CreateForm 클래스는 초기화 객체를 받아 form 상태 관리에 필요한 formController 객체를 생성합니다. sicilian은 초기화 객체의 initValue, validator, validateOn, clearFormOn 프로퍼티를 통해 옵트인 기능을 제공합니다.
- initValue : input에 초기값이 필요할 때 사용합니다.
- validator : 각 input value를 검증하는 방법을 지정합니다. 각각의 검증 방법에 대해서는 아래에서 자세히 다뤄보겠습니다.
- validateOn : 검증 방법을 적용할 시점을 정의합니다. 현재는 form이 접수될 때("submit"), input으로부터 포커스가 해제될 때("blur"), input이 변경될 때("change") input value를 검증할 수 있으며, 배열을 통해 여러 검증 시점을 동시에 적용할 수도 있습니다.
- clearFormOn : 다른 form 관리 라이브러리와 달리 sicilian은 다른 페이지로 이동해도 사용자가 입력한 값이 유지됩니다. 따라서 특정 상황에서 form을 초기화해야할 경우 clearFormOn을 사용합니다. 현재 지원하는 초기화 시점은 form이 접수될 때("submit")와 라우트가 변경될 때("routeChange")이며, validateOn과 마찬가지로 배열을 통해 여러 초기화 시점을 동시에 적용할 수 있습니다.
const signUpFormController = new CreateForm({
initValue: {
email: "",
password: "",
nickname: "anonymous"
},
validator: {
email: {
required: {},
checked: {},
minLength: {},
maxLength: {},
RegExp: {},
custom: {},
}
},
validateOn: ["submit", "blur", "change"],
clearFormOn: ["submit", "routeChange"]
});
CreateForm 클래스를 통해 얻어낸 formController 인스턴스에는 form 상태를 관리하는 데 도움을 주는 다양한 프로퍼티와 메서드가 존재합니다. 각각의 프로퍼티와 메서드가 어떤 기능을 맡는지는 이어지는 내용에서 자세히 살펴보겠습니다.
const {
initValue,
register,
getValues,
getErrors,
setValues,
setErrors,
handleValidate,
handleSubmit
} = signUpFormController
register
register 함수는 input 및 textarea 태그를 관리하기 위한 다양한 값과 메서드가 포함된 객체를 반환합니다. 이 함수는 객체를 인자로 받는데, input을 특정하기 위한 name 프로퍼티는 반드시 제공해야 하며, input type을 처리하기 위한 type 프로퍼티와 input value를 검증하기 위한 valiate 객체는 선택적으로 제공할 수 있습니다.
function register(
props: {
name: string,
validate?: RegisterErrorObj,
type?: ValidInputTypes
}): {
id: string,
name: string,
value?: string,
checked?: boolean,
onBlur: (e: SicilianEvent) => void,
onFocus: (e: SicilianEvent) => void,
onChange: (e: SicilianEvent) => void,
}
CreateForm 클래스에 initValue 혹은 validator 프로퍼티를 제공했다면, 타입스크립트는 register에 올 수 있는 name의 종류를 추론해냅니다. 만약 타입스크립트를 사용한다면 IDE를 통해 입력 가능한 name 문자열을 추천받을 수 있습니다.
nickname 필드는 initValue나 validator에 포함되지 않았지만, 문제 없이 등록할 수 있습니다. CreateForm의 모든 파라미터는 선택적이기 때문에, 명시적으로 정의되지 않은 필드도 정상적으로 작동합니다. 이렇게 하면 모든 입력 필드를 미리 정의하지 않아도 유연하게 폼을 처리할 수 있습니다.
handleValidate
시작하기에 앞서 sicilian에서 사용하는 비슷한 두 용어에 대한 정의를 짚고 넘어갈 필요가 있습니다. validate 객체는 하나의 input을 검증하는 데 사용되는 객체이며, validator 객체는 이러한 validate 객체를 name 필드에 따라 묶어놓은 다차원 객체입니다. handleValidate 메소드는 validator 객체를 반환하고, validator 객체에는 각 필드에 가져다 사용할 수 있는 validate 객체가 여럿 있는 식입니다. 이를 타입으로 서술한다면 아래와 같습니다.
Type Validator = Partial<Record<keyof initValue, Validate>>
필요하다면 handleValidate 없이 validate 객체와 validator 객체를 직접 만들어 사용할 수 있습니다. 하지만 handleValidate를 사용하면 타입 검사가 수반되기에 조금 더 타입 안전하게 코드를 작성할 수 있다는 장점이 있습니다. 이를 위해서는 아래의 caseOne과 같이 handleValidate에 객체 리터럴을 인자로 제공해야 합니다.
caseTwo와 같이 함수의 반환 값이나 변수 등을 인자로 제공하는 경우에는 초과 프로퍼티 검사(Excess Property Checks)가 진행되지 않습니다. 더 정확히는 타입스크립트의 함수 인자에 대한 반공변성으로 인해 초과 프로퍼티가 타입 에러를 일으키지 않습니다. 따라서 잘못된 값이 들어와도 handleValidate는 타입 에러를 발생시키지 않고, 런타임 때에나 겨우 에러가 있다는 사실을 알아차리게 될 것입니다.
const { handleValidate } = SignInFormController
const caseOne = handleValidate({
email: {},
password: {}
})
const caseTwo = handleValidate(SignValidate())
그러니 두 번 말해도 과하지 않다고 생각합니다. handleValidate을 사용하는 경우 반드시 객체 리터럴을 인자로 제공하십시오!
validator
handleValidate로부터 반환된 validator 객체는 각 name 필드에 대한 프로퍼티를 가지게 됩니다. 이를 통해 아래와 같이 register의 두 번째 인자로 validate 객체를 쉽게 제공할 수 있습니다.
register의 두 번째 파라미터는 옵셔널하기 때문에, 값을 검증할 필요가 없는 필드라면 validate 객체를 제공하지 않아도 됩니다. 이것은 위에서 Validator를 타입으로 서술할 때 Partial을 사용한 이유이기도 합니다. 덕분에 handleValidate는 모든 name 필드가 아닌 값 검증이 필요한 필드만을 대상으로 동작합니다.
const { handleValidate } = SignInFormController
const validator = handleValidate({
email: {},
password: {}
})
export default function SignUp() {
{...}
return (
<>
<input {...register({ name: "email", validate: validator.email })}/>
<input {...register({ name: "password", validate: validator.password })}/>
<input {...register({ name: "nickname")}/>
</>
)
}
validate
register의 onBlur는 validate 객체의 설정을 토대로 input 값을 검증합니다. sicilian은 아래의 네 가지 필드를 통해 각각의 인풋을 검증하며, 검증 방식과 검증 실패 시에 ErrorState에 전달할 에러 메세지를 담고 있습니다. 만약 에러 메세지를 제공하지 않는다면 sicilian은 기본 에러 메세지 템플릿을 사용합니다.
- required : input 값이 필수적인지 검증
- checked : input이 check 되어있는지 검증
- minLength & maxLength : input 값의 최소 & 최대 길이를 검증
- RegExp : 정규 표현식을 사용해 input 값을 검증
- custom : 사용자가 커스텀한 방식으로 input 값을 검증
required, checked, minLength, maxLength의 경우 메세지를 담은 객체 뿐만 아니라 boolean과 number라는 원시 타입을 사용할 수도 있습니다. 이 경우 에러가 발생하면 기본 에러 메세지 템플릿을 사용합니다.
required?: boolean | {required: boolean, message: string}
checked?: boolean | {checked: boolean, message: string}
minLength?: number | { number: number, message: string}
maxLength?: number | { number: number, message: string}
RegExp는 검증 객체 뿐 아니라 검증 객체로 이루어진 배열을 받습니다. 덕분에 하나 이상의 정규 표현식이나 검증 함수를 사용해 input 값을 여러 방면으로 검증해볼 수 있습니다. 위에서 살펴본 세 개의 필드와 다르게 RegExp 필드는 메세지 프로퍼티가 옵셔널합니다.
// 정규표현식 검증
RegExp?: RegExpErrorObj | Array<RegExpErrorObj>;
RegExpErrorObj = { RegExp: RegExp; message?: string };
email : {
RegExp: {
RegExp: new RegExp("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$"),
message: "이메일 형식과 맞지 않습니다",
},
},
password: {
RegExp: [
{
RegExp: new RegExp("^[^\\s]+$"),
message: "비밀번호는 공백을 포함할 수 없습니다.",
},
{
RegExp: new RegExp("^(?=.*[a-z])(?=.*\\d)(?=.*[@$!%*?&])[a-z\\d@$!%*?&]+$"),
message: "비밀번호는 소문자, 숫자, 특수문자를 모두 포함해야 합니다",
},
],
},
custom에 사용되는 콜백함수 checkFn은 input value와 전체 formState를 인자로 받아, 검증 로직을 거친 뒤 boolean을 반환합니다. 만약 이 결과가 참이면 에러가 발생하며, 거짓이면 에러가 발생하지 않습니다. RegExp와 마찬가지로 검증 객체로 이루어진 배열을 받을 수 있습니다. 따라서 필요하다면 하나의 값을 여러 방식으로 검증해볼 수 있습니다.
// 커스텀 검증
custom?: CustomErrorObj | Array<CustomErrorObj>;
CustomErrorObj = {
checkFn: (
value: string,
formState: Record<keyof initValue, string>>
) => boolean;
message?: string
};
회원가입 form을 만들 때는 비밀번호와 비밀번호 확인의 값이 일치하는지를 확인해야 하는데, custom 필드를 사용하면 아주 간단하게 이를 검증하고 에러를 반환하도록 만들 수 있습니다.
custom: {
checkFn: (
value: string,
formState: { password: string }
) => value === formState.password,
message: "비밀번호가 일치하지 않습니다",
},
서로 다른 input의 값을 비교하는 데에도 custom 필드가 요긴하게 사용되지만, 이 필드의 강력함은 복잡한 검증 로직을 구현하는 데 있습니다. 닉네임에 나쁜 단어를 쓰지 못하도록 검증하고 싶은데, 이 나쁜 단어 목록은 백엔드 데이터베이스로 관리되고 있습니다. 이런 경우에도 아래와 같이 서버로부터 데이터를 받아와 사용하면 문제 없이 검증할 수 있습니다.
데이터베이스에 나쁜 단어를 추가하면 프론트엔드 코드의 수정 없이 업데이트된 정책을 즉시 적용시킬 수 있습니다. 이것은 custom 필드의 또다른 장점이라고 할 수 있겠습니다.
export const isWordInclude = (wordList: string[]) => (value: string) => {
for (const word of wordList) {
if (value.includes(word)) return true;
}
return false;
};
export const useSignValidate = () => {
const { data } = useQuery({ ... })
const email = { ... }
const password = { ... }
const passwordCheck = { ... }
const nickname = {
custom: [
{
checkFn: !isWordInclude(data?.bad ?? []),
message: "닉네임에 욕설을 포함할 수 없습니다",
},
{
checkFn: !isWordInclude(wordList(data?.sexual ?? [])),
message: "닉네임에 성적인 단어를 포함할 수 없습니다",
},
]
}
return { email, nickname, password, passwordCheck }
}
검증 우위와 검증 순서
Sicilian에서 input을 검증할 때 반드시 주의해야 할 점이 몇 가지 있습니다. 그 중 하나는 register에 validate 객체를 제공했다면, CreateForm 클래스에 제공한 validate 객체는 무시된다는 것입니다. 이를 CreateForm 클래스의 validator 옵션에 대한 register 함수의 validate 객체가 갖는 검증 우위라고 표현합니다. 따라서 아래의 경우 maxLength와 minLength 검증은 무시되고, 오직 required 여부만을 검증하게 됩니다.
export const { register } = createForm({
initValue: { email: "" },
validator: {
email: {
maxLength: 16,
minLength: 8
}
},
})
export default function Comp() {
return <input {...register({ name: "email", validate: { required: true } })}>
}
또 한 가지는 Sicilain이 validate 객체에 필드가 입력된 순서대로 검증을 진행한다는 것입니다. 또한, 검증 과정에서 에러를 발견하면 그 즉시 검증 과정이 종료됩니다.
첫 번째 예시에서는 minLength 필드가 required 필드보다 먼저 검증되며, (minLength가 충족되면 required는 자연히 충족되므로) required는 사실상 아무 일도 하지 않습니다. 반면에 두 번째 예시에서는 required가 값의 유무를 먼저 검증하고, 이후 minLength가 값의 길이를 검증합니다. 따라서 어떤 input의 검증 결과가 예상과 다르다면 각 필드의 순서를 확인해보는 것이 좋습니다.
// required 필드 무의미
password: { minLength: 10, required: true }
// required 필드 유의미
password: { required: true, minLength: 10 }
Values와 Errors
form 상태와 error 메세지 등은 sicilian에 의해 자동으로 처리되지만, 가끔은 수동으로 이 값을 제어해야 할 때가 있습니다. 가령 사용자가 이전에 작성한 리뷰를 수정해야 한다면, 서버로부터 리뷰 데이터를 받아서 이 값을 각각의 input에 넣어주어야 합니다. 이럴 때 setValues를 사용할 수 있으며, 무한 랜더링이 발생하는 것을 막기 위해 보통은 useEffect와 함께 사용하게 됩니다.
const { setValues } = SignInFormController
const { data } = useQuery({
queryKey: ["review", reviewId],
queryFn: getReview(reviewId)
}
});
useEffect(() => {
setValues({
title: data?.title ?? "",
author: data?.author ?? "",
description: data?.description ?? ""
});
}, [data]);
getValues와 getErrors가 리턴하는 객체는 전역 상태 그 자체이기 때문에, Context API의 리랜더링 방식과 유사한 구조적 문제를 안고 있습니다. 즉, input의 상태를 아무리 잘 격리해두었다 해도 부모 컴포넌트에 getValues 객체가 있다면 부모 컴포넌트 전체가 리랜더링됩니다. 이 문제를 해결하기 위해 getValues 함수와 getErrors 함수는 전역 상태로부터 일부만을 구독할 수 있도록 selecting name을 인자로 받습니다. 타입으로 표현하자면 아래와 같습니다.
// selecting name을 제공하지 않는 경우 전역 상태인 T를 반환
function getValues<T extends InitState>() => T
// selecting name을 제공하면 input value 타입을 반환
function getValues<T extends InitState, K extends ExtractKeys<T>>(name: K) => T[K]
handleSubmit
handleSubmit 함수는 콜백 함수를 인자로 받습니다. 이 콜백 함수는 onSubmit이 발생한 시점의 전체 formState와 이벤트 객체를 인자로 받습니다. 또한 내부적으로 e.preventDefault() 처리가 되어있어 form submit으로 인한 리다이렉트가 발생하지 않습니다.
const { mutate } = useSignMutation("/auth/signup");
<form onSubmit={handleSubmit((data, event) => mutate(data)})} />
getValues 메소드로부터 전체 formState를 받아와 직접 submit 로직을 구현할 수도 있습니다. 하지만 handleSubmit을 몇 가지 이점을 얻을 수 있습니다.
만약 해결되지 않은 에러 메세지가 하나라도 남아있다면 handleSubmit은 submit을 중지시킵니다. 이를 통해 원치 않는 값이 백엔드로 가는 것을 방지할 수 있습니다. 비슷하게, formController가 관리하는 input이 모두 비어있는 경우에도 submit을 중지시킵니다. 따라서 유저가 실수로 submit 버튼을 눌러도 http 통신이 발생하지 않습니다.
clearFormOn: ["submit"] 옵션이 제공된 상태라도 submit에 실패했다면 form이 초기화되지 않습니다.
useForm
sicilian은 static form과 dynamic form으로 form을 구분합니다. static form이란 회원가입이나 로그인 등과 같이 그 구조가 완벽하게 고정되어있는 경우를 말합니다. 반대로 댓글과 같이 유동적으로 구성되는 경우라면 dynamic form에 해당합니다. 2.1 버전 이전의 sicilian으로는 dynamic form을 구현하는 것이 '거의' 불가능했습니다. 아래의 예시에서 볼 수 있는 것처럼, register는 createForm과 1대 1로 대응하며, formController 객체를 사용하여 등록된 모든 input에 영향을 미치기 때문입니다.
// sicilianComment.ts
const { handleSubmit, register, setForm } = createForm({
initValue: { comment: "" },
validator: {
comment: { required: true },
},
validateOn: ["submit"],
clearFormOn: ["routeChange", "submit"],
});
// CommentInput.tsx
function CommentInput({
initValue = "",
inputName,
buttonName,
}: {
initValue?: string;
inputName: string;
buttonName: string;
}) {
const isLogin = useIsLogin();
const { onSubmit, isPending } = useCommentMutation({ initValue, depth });
useEffect(() => {
setForm({ comment: initValue });
}, [initValue]);
return (
<Form.Textarea
{...register({name: "comment"})}
initValue={initValue}
className={styles.textarea}
disabled={!isLogin}
/>
)
}
이러한 문제를 해결하고 dynamic form을 지원하기 위해 useForm 훅이 추가되었습니다. 반드시 컴포넌트 밖에서 호출해야 했던 createForm과 다르게 useForm 훅은 role of hook에 따라 반드시 컴포넌트 안에서 호출해야 합니다. useForm 훅을 사용하면 컴포넌트의 생애주기에 따라 동적으로 생성되었다 사라지는 '지역 상태성'을 가지면서도 sicilian이 제공하는 기능은 모두 사용할 수 있습니다.
import
function CommentInput({
initValue = "",
inputName,
buttonName,
}: {
initValue?: string;
inputName: string;
buttonName: string;
}) {
const isLogin = useIsLogin();
const { onSubmit, isPending } = useCommentMutation({ initValue, depth });
const { handleSubmit, register, setForm } = useForm({
initValue: { comment: "" },
validator: {
comment: { required: true },
},
validateOn: ["submit"],
});
useEffect(() => {
setForm({ comment: initValue });
}, [initValue]);
return (
<Form.Textarea
{...register("comment")}
initValue={initValue}
className={styles.textarea}
disabled={!isLogin}
/>
)
}
로그인∙회원가입과 같은 static form에서도 useForm을 사용할 수 있습니다. 다만 관심사의 분리라는 관점에서 본다면 createForm을 사용하여 form 로직을 컴포넌트 외부로 분리하는 것이 더 바람직하겠습니다.
SicilianProvider 컴포넌트와 useSicilianContext 함수
기존에는 아래와 같이 input 태그 혹은 Input 컴포넌트에 스프레드 연산자를 사용해 register 함수가 반환하는 객체의 프로퍼티를 그대로 props로 넘겨주었습니다. 때문에 input에 입력시 form 전체가 리랜더링되는 문제가 있었습니다.
<input {...register('email', validator.email)}>
이를 억제하기 위해서는 스프레드 연산자가 아니라 register와 name을 props로 넘겨주고, Input 컴포넌트 내부에서 조합해 사용합니다. 문제는 register와 name가 굉장히 좁은 타입으로 추론되고 있어서 컴포넌트에 props으로 넘겨주는 일이 대단히 번거롭다는 점입니다.
이러한 문제를 해결하기 위해 sicilian은 SicilianProvider 컴포넌트와 useSicilianContext 함수를 제공합니다. 앞서 잠깐 언급했던 바와 같이 이들은 내부적으로 Context API를 사용하여 구현되었으며, 미리 정해진 값과 함수만을 전달할 수 있기 때문에 타입 정의 문제에서도 자유롭게 사용할 수 있습니다.
SicilianProvider 컴포넌트는 register, name, validate, type, getValues, getErrors 이렇게 여섯 개의 값과 메소드를 갖는 value 객체를 인자로 받게됩니다. 이 중 register와 name은 반드시 제공해야하며, register의 타입에 따라 name에 넣을 수 있는 문자열 타입의 종류가 자동으로 추론됩니다.
register와 name를 제외한 나머지는 옵셔널하게 제공할 수 있습니다. 그리고 이렇게 넣어준 값들은 하위 노드 어딘가에서 useSicilianContext 함수를 통해 조회할 수 있습니다. 만약 상위 노드에 SicilianProvider 컴포넌트가 존재하지 않으면 useSicilianContext 함수는 아래와 같은 에러를 던지게 됩니다.
만약 옵셔널하게 제공하는 validate, type, getValues, getErrors props를 SicilianProvider에 제공하지 않은 채 useSicilianContext 함수를 통해 조회하려 한다면 어떻게 될까요? validate와 type의 경우 타입 자체가 undefined를 포함하고 있어 상관이 없지만, 문제는 getValues와 getErrors 함수입니다.
이 두 함수를 SicilianProvider 컴포넌트에 제공하지 않은 채 useSicilianContext 함수를 통해 조회한다면, 실제로 조회되는 것은 getValues와 getErrors 함수가 아니라 콘솔에 에러 메세지를 남기는 함수가 조회됩니다. 이 에러 메세지를 통해 현재 어떤 컴포넌트에서 props로 넘겨주지 않고 getValues와 getErrors 함수를 사용하고 있는지를 확인할 수 있게 됩니다.
아래는 SicilianProvider와 useSicilianContext를 사용하는 Input 컴포넌트에 대한 간단한 예시입니다. 실제 프로젝트에서는 Map을 사용해 구현하였습니다.
import { register, getValues, getErrors } from "@/hooks/FormController/signUp.ts"
import { SicilianProvider } from "sicilian/provider"
export default function Home() {
{ ... }
return (
<>
{ ... }
<SicilianProvider value={{ register, name: "email", getErrors }}>
<Input />
</SicilianProvider>
<SicilianProvider value={{ register, name: "password", getErrors }}>
<Input />
</SicilianProvider>
{ ... }
</>
)
}
import { useSicilianContext } from "sicilian/provider"
export default function Input({className, ...props}: InputHTMLAttributes<HTMLInputElement>) {
const { register, name, validate, getErrors } = useSicilianContext()
const errorMessage = getErrors(name)
return (
<>
<input {...props} {...register({ name, validate })} className={clsx(styles.input, className)} />
<Show when={!!errorMessage}>
{errorMessage}
</Show>
</>
);
}
아래는 sicilian 1 버전을 사용한 프로젝트의 로그인 form을 크롬의 리액트 도구를 사용해 확인한 것입니다. 보는 바와 같이 email input에 입력을 하고 있어도 form 전체가 반복해서 리랜더링 되는 것을 알 수 있습니다.
반면에 최신 버전을 사용하여 다시 만든 로그인 form은 아래와 같습니다. SicilianProvider와 useSicilianContext, 그리고 전체 상태의 일부만을 구독할 수 있는 getErrors 함수를 활용한 덕분에 form 전체가 리랜더링 되는 것이 아닌, 키 입력 중인 Input 컴포넌트만 리랜더링 되는 것을 확인할 수 있습니다.
in App Router
Next.js App Router 환경에서 Sicilian은 반드시 'use client'를 붙여서 사용해야 합니다. 이는 Sicilian을 구성하는 ─ useSyncExternalStore, useContext 등의 ─ 요소는 모두 'use client'를 필요로 하기 때문입니다. 그 외의 문법은 기존과 동일하게 사용할 수 있습니다.
'use client'
import { SicilianProvider } from "sicilian";
import { register } from "@/component/play";
import Input from "@/component/Input";
export default function Home() {
return (
<div>
<SicilianProvider value={{register, name: "email"}}>
<Input />
</SicilianProvider>
<SicilianProvider value={{register, name: "password"}}>
<Input />
</SicilianProvider>
</div>
)
}
블로그의 정보
Ayden's journal
Beard Weard Ayden