회원 정보 페이지에는 닉네임 변경, 프로필 사진 변경, 비밀번호 변경 및 계정 삭제를 할 수 있는 필드가 있다.
회원가입 페이지를 맡으신 팀원 분이 react-hook-form을 사용하시면서 소개해주셔서, 나도 배워볼 겸 회원 정보 수정 페이지에 적용해보았다.
react-hook-form
React Hook Form relies on uncontrolled form, which is the reason why the register function capture ref and controlled component has its re-rendering scope with Controller or useController.
This approach reduces the amount of re-rendering that occurs due to a user typing in input or other form values changing at the root of your form or applications.
공식 페이지에서는, React Hook Form은 ref를 사용한 비제어 컴포넌트를 기반으로 작동하며, 이를 통해 props변경으로 인한 리렌더링을 줄여 성능을 개선시킨 폼 컴포넌트라고 설명하고 있다.
We can combine the two by making the React state be the “single source of truth”. Then the React component that renders a form also controls what happens in that form on subsequent user input. An input form element whose value is controlled by React in this way is called a “controlled component”.
Source: React
Controlled Component(제어 컴포넌트)는 입력값을 React가 직접 관리하는 컴포넌트다.
예를 들어, input
같은 입력 요소는 다양한 타입과 속성을 갖고 있고, 그 중 type
이 submit
인 입력 요소와 연결되면, 내부 상태에 직접 접극해서 값을 저장하거나 불러올 수 있다.
하지만 React에서는 보통 입력 이벤트가 발생할 때마다 이벤트 핸들러를 통해 state를 업데이트하는 방식을 사용한다. 이는 양식 요소들이 자체적으로 값을 관리한느 대신, React의 상태 관리 체계에 의해 관리되게 함으로써 신뢰 가능한 단일 출처(Single Source of Truth) 원칙을 준수하는 것이다.
문제는 이러한 제어 컴포넌트가 state 업데이트가 이뤄질 때마다 Re-rendering 되는 점이다. 특히 React에서 부모 컴포넌트의 state가 변경되면 자식 컴포넌트 역시 리렌더링되는데, 사용자 입력이 빈번한 폼 컴포넌트의 경우 이로 인해 불필요한 리렌더링이 자주 발생하게 되고, 이는 결국 성능에 부정적인 영향을 줄 수 있다.
반면 Uncontrolled Component(비제어 컴포넌트)는 입력값을 React가 아니라 DOM자체에서 관리하는 컴포넌트이다.
이 경우, 폼 내의 개별 입력 요소들이 자체적으로 상태를 가지며, React 코드는 ref
를 사용하여 특정 DOM 요소에 접근한다.
비제어 컴포넌트는 사용자 입력을 처리하는 코드를 작성하지 않아도 되므로 코드가 더욱 간결해지고, 사용자 입력에 따라 발생하는 state 변화와 이에 따른 불필요한 리렌더링을 줄일 수 있다.
그러나 비제어 컴포넌트는 상태 관리가 React 밖에서 이루어지므로, React의 상태 관리 체계와는 동떨어져있다. 그래서 폼의 상태를 외부 API나 다른 React 컴포넌트와 동기화하거나, 입력값에 대한 복잡한 검증 또는 변형하는 작업 등에는 제어 컴포넌트를 사용하는 것이 더 적합할 수도 있다.
React Hook Form은 이런 두 가지 접근법 사이의 간극을 줄여주는 도구다. 비제어 컴포넌트의 장점을 살리면서도, 필요한 경우에는 상태를 수동으로 제어할 수 있게 해준다. 이를 통해 사용자 입력에 따른 불필요한 리렌더링을 최소화하고 성능을 개선할 수 있다.
또한 패키지 자체도 super light이고, 타입스크립트도 제공된다는 점이 우리 프로젝트에 사용하기 좋겠다는 생각이 들었다.
적용하기
먼저 methods를 가지고 온다.
import { useForm } from "react-hook-form";
interface ProfileEditTypes {
nickname: string;
}
function EditProfile() {
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
} = useForm<SignupTypes>({
mode: "onChange",
criteriaMode: "all",
defaultValues: {
nickname: "",
},
});
register
은 유효성 확인, handleSubmit
은 폼 제출, watch
는 실시간으로 입력폼에 적힌 값을 확인한다. formState
는 전체 양식 상태에 대한 정보를 담는 객체이다.
const currentNickname = watch("nickname");
const handleSave = () => {
updateNickname(memberId, currentNickname)
.then(() => {
dispatch(setNickname(currentNickname));
nicknameChangeSuccess();
})
.catch((error) => {
console.error("닉네임 변경에 실패하였습니다.", error);
if (error.message === "닉네임 중복") {
nicknameChangeRetry();
} else if (error.message === "서버 오류") {
serverError();
}
});
};
watch
메서드를 사용하여 nickname 필드의 현재 값을 가져오고, 폼 제출 시 실행되는 함수인 handleSave를 작성한다.
return (
<ProfileEditContainer onSubmit={handleSubmit(handleSave)}>
...
<input
id="nickname"
type="text"
className="profile-input"
placeholder="한글 및 영어, 숫자 10자 이내"
{...register("nickname", {
required: true,
validate: (value) =>
validFunc.validNickName(value) ||
"닉네임은 10자 이하여야 합니다.",
})}
/>
...
{errors.nickname && (
<p className="error-msg">{errors.nickname.message}</p>
)}
...
</ProfileEditContainer>
);
ProfileEditContainer 컴포넌트의 onSubmit
prop을 handleSubmit(handleSave)로 설정한다.
유저가 폼을 제출하면 react-hook-form의 handleSubmit
메서드가 호출되어 전달된 handleSave 함수를 실행하고, 사용자가 입력한 nickname이 업데이트 된다.
input
요소에서는 register 메서드를 사용하여 nickname 필드를 등록한다. required: true
는 필드가 필수적임을 나타내며 validate
는 사용자 정의 유효성 검사를 실시한다. 여기서 사용한 validFunc는 회원가입 부분을 맡으셨던 팀원분이 유효성 검사로 만드신 파일이라 그대로 import해서 사용했다.
마지막으로 errors.nickname이 true일 때, 즉 nickname 필드에서 에러가 발생했을 때, register 메서드의 validate 함수에서 반환된 문자열이 errors.nickname.message
에 저장되어 에러 메시지를 출력한다.
같은 방식으로 비밀번호 변경에도 적용해보았다.
처음에는 현재 비밀번호, 새로운 비밀번호, 비밀번호 재확인 input을 각각 따로 만들어주었는데, 비슷한 폼이 반복되어서 각 객체가 들어있는 배열을 만들어 map
으로 돌려주는 형태로 바꾸었다.
<ProfileEditContainer onSubmit={handleSubmit(handleChange)}>
<TitleBox>비밀번호 변경</TitleBox>
<SectionBox>
{passwordArr.map((password) => (
<SubsectionBox key={password.content}>
<label htmlFor={`${password.content}`}>
{password.labelName}
</label>
<InputButtonContainer>
<input
id={`${password.content}`}
type="password"
className="profile-input"
placeholder={password.placeholder}
{...register(password.content, {
required: password.required,
validate:
password.content !== "confirmPassword"
? (value: string) =>
password.validFunction(value) ||
password.errorMessage
: (value: string) => {
const { newPassword } = getValues();
return (
newPassword === value ||
password.errorMessage
);
},
})}
/>
{password.content === "confirmPassword" && (
<DefaultButton
type="submit"
disabled={!isValid}
onClick={handleSubmit(handleChange)}
>
변경
</DefaultButton>
)}
</InputButtonContainer>
{errors[password.content] && (
<p className="error-msg">
{errors[password.content]?.message}
</p>
)}
</SubsectionBox>
))}
</SectionBox>
</ProfileEditContainer>
회고
회원가입 폼처럼 입력칸이 많았던 게 아니라서 퍼포먼스에 얼마나 영향이 갔는지는 모르겠지만, 새로 react-hook-form에 대해 배우고 사용해볼 수 있어서 의미 있었다.
원래 각각 input 필드를 만들던 것에서 map으로 돌리는 것으로 리팩토링하면서 또 여러 UI/UX적으로 개선할 부분이 여럿 보였다.
1. 비밀번호 재확인까지 잘 쳐놓고, 다시 새로운 비밀번호를 바꾸면 변경 버튼은 비활성화 되긴 하지만 비밀번호가 일치하지 않는다는 경고문은 다시 안 뜬다.
2. 비밀번호 변경 후 toastify 알림이 뜨지만, 입력칸에 입력된 것들이 그대로 남아있다.
또 회원 정보 수정에 사진 변경 부분에서는
1. 사진을 선택하자마자 그 사진으로 바뀌는데, 모달 창을 띄워 다시 한번 물어보는 게 좋을 것 같다.
2. 최대 1MB까지 서버에서 받아지는데, 1MB 이상의 파일을 받으면 받아지는 것처럼 toastify 알림이 뜨고 사진은 바뀌지 않는다.
https://www.react-hook-form.com/get-started/
https://dealicious-inc.github.io/2022/07/25/ss-studio.html
'Development > Frontend' 카테고리의 다른 글
Elog: Next.js and window object (0) | 2024.05.02 |
---|---|
Elog: Event Bubbling (0) | 2024.05.02 |
[게시글 상세] 댓글 대댓글 CRUD (1) | 2023.07.01 |
[게시글 상세] 게시글 상세 페이지 구조 (0) | 2023.06.22 |
[Navbar] Redux-toolkit 활용: 로그인/비로그인 구분, 프로필 관리 (1) | 2023.06.09 |