입력 폼은 웹페이지를 구성할 때 빠지지 않고 포함되며, 그렇기에 html 내 input 의 type 들 역시 기본적인 입력부터 시작하여 number, tel, select, color, file 등등 다양하게 존재한다. 상황마다 달라지겠지만 기본적으로는 이메일과 비밀번호를 입력하는 로그인부터, 유저의 다양한 정보들을 한번에 전송해야 하는 입력폼을 생성해야할 때도 존재한다.
진행중인 closet 프로젝트에도 입력폼은 반드시 포함되었어야 했는데, 당연하게도 로그인 및 회원가입때의 입력폼과, 프로젝트의 핵심이라 할 수 있는 의류들의 정보들을 입력하게 될 add 페이지와 입력했던 의류정보를 수정하게 될 detail 페이지에서 사용될 입력폼이 필요했고, 그렇기에 좀 더 입력폼을 구현하는데 수월한 방법이 없을 까 고민하게 되었다.
사실 로그인이나 회원가입의 경우 현재 설계대로라면 그다지 많은 입력 input 이 필요하지 않아서, 직접 구현하는 방향으로 진행하였다. (다만 앞으로 더 구현할 수 있을 아이디 및 비밀번호 찾기와 같은 기능을 추가하려면 입력 양식 또한 변경이 될 가능성이 커진다)
실제 회원가입을 구현한 코드를 보면 아래와 같다. 코드가 좀 긴 편이라 대충대충 훓어 보기만 해도 된다.
const Signup = (props: SIprops) => {
const dispatch = useDispatch();
const divref = useRef<HTMLButtonElement>(null);
const { signInDone } = useSelector((state: RootState) => state.user);
const { toggleGotoAccount } = props;
const [isCollect, setIsCollect] = useState<boolean>(false);
const [name, setName, onChangeName] = useInput('');
const [email, setEmail, onChangeEmail] = useInput('');
const [password, setPassword] = useState<string>('');
const [passwordCheck, setPasswordCheck] = useState<string>('');
// 함수들 생략하겠다.
const emailRegExp = /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]{2,3}$/;
const passwordRegExp = /^.{8,}$/;
const isEmailValid = emailRegExp.test(email);
const isPasswordValid = passwordRegExp.test(password);
return (
<SignupBox>
<LeftTopBrand>
<span>Closet</span>
</LeftTopBrand>
<SignupSection>
<SignupForm>
<h1>Create an account</h1>
<span>
이메일 양식에 적합하게 작성해주시고,
<br />
비밀번호는 8자리 이상 해주세요
</span>
<input type='text' value={name} onChange={onChangeName} placeholder='Name' required />
<div></div>
<input type='email' value={email} onChange={onChangeEmail} placeholder='Email' />
<div>{email && !isEmailValid && `이메일이 올바르지 않습니다`}</div>
<input type='password' value={password} onChange={onChangePassword} placeholder='Password' />
<div>{password && !isPasswordValid && `비밀번호가 올바르지 않습니다`}</div>
<input type='password' value={passwordCheck} onChange={onChangePasswordCheck} placeholder='Password Check' />
<div>{passwordCheck && !isCollect && `비밀번호가 일치하지 않습니다`}</div>
<AButton ref={divref} color='black' disabled={!(isEmailValid && isPasswordValid && isCollect)} onClick={onSubmit} dest='Create account' />
<AButton ref={divref} color='' disabled={false} onClick={toggleGotoAccount} dest='back' />
<div></div>
<div></div>
</SignupForm>
</SignupSection>
</SignupBox>
);
};
상태값을 변경시켜줄 함수들은 우선 생략하였고, 각 input 들의 type 을 지정해주고, 유효성 체크를 위해 이메일 양식과 비밀번호 양식을 정규표현식으로 체크를 해주며, 이에 대한 boolean 값인 isEmailValid, isPasswordVaild 을 통해 false 일 경우 error 메세지가 렌더되도록 설정해주었다.
이렇게 직접 구현을 했을 당시, 그렇게까지 힘든 부분은 없었지만 그 이유는 입력해야할 input 의 종류가 적었기 때문이며, 만일 입력해야할 값들이 2배로 늘어난다고 가정하면, 혹은 값들도 증가하면서도 하나의 입력값에 대한 유효성 검사가 2~3가지로 늘어난다면? 위 코드에서는 함수를 생략해놓았지만, 함수까지 포함된다면 하나의 입력값이 증가할 때마다 추가될 함수와 상태값들을 구현하기 귀찮기도 하고 효율성도 좋지 않을것이다.
사실 위 코드에서도 커스텀훅을 활용해서 조금이라도 함수를 줄인것인데, 커스텀훅은 아래처럼 작성하였었다.
import React, { useState, useCallback } from 'react';
type UserInputProps = [string, React.Dispatch<React.SetStateAction<string>>, (event: React.ChangeEvent<HTMLInputElement>) => void];
const useInput = (initialState: string): UserInputProps => {
const [value, setValue] = useState(initialState);
const onChangeValue = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}, []);
return [value, setValue, onChangeValue];
};
export default useInput;
input 의 상태값과 그 상태값을 변경시켜줄 onChange 함수를 함께 훅으로 묶어서 좀 더 컴포넌트가 정리될 수 있도록 활용하였다. 그럼에도 여전히 input 의 갯수가 증가하거나, 위 훅처럼 단순하게 입력값의 변화만을 담당해야 하는것이 아니라 각 입력값마다 서로 연관을 가지게 된다면(예를 들어 비밀번호와 비밀번호확인), 위 커스텀훅을 활용하기에 제한이 걸린다.
이러한 이유로 좀 더 효율적으로 입력폼을 작성하고 싶었고, 그래야 더 다른 구현에 집중할 수 있을 것이라 판단하여 입력폼 라이브러리를 찾아보니 React-hook-form 을 발견할 수 있었다.
React-hook-form
좀 예전이지만 한번 formik 을 사용해본적이 있었는데, 그 이후로 입력폼 라이브러리를 사용한 적이 없어서, 최근 사용 추세를 알아보고 싶었다. npm trend 는 이를 직관적으로 확인할 수 있기에 실제 확인해본 결과 react-hook-form 의 사용량이 가장 많다는 것을 알 수 있었다.
사용량이 많다는 이유만으로 선택하기에는 어떤점에서 직접 입력폼을 생성하는것보다 이득이 있는지를 파악하고 싶었고, 실제 공식문서를 참고해보니 여러 상황에서 충분히 활용될만한 좋은 라이브러리라는 것을 알 수 있었다.
공식 홈페이지의 첫 페이지부터 react-hook-form 이 다른 입력폼 라이브러리에 대비되어 여러 장점을 가지고 있다고 설명하고 있는데, 간략하게 살펴보면
- 각 input 하나하나 독립적인 re-render 가 진행된다는 점
- 급격하게 감소한 component mount 횟수를 통해 시간 단축
- 입력 폼을 구상하는데 들어가는 코드의 양이 적다
이정도로 요약 할 수 있겠으며, 이 외에도 여러 편의성을 제공하는데 위에서 언급한 에러사항이나 유효성검사과정 등을 더욱 손쉽게 코딩할 수 있도록 도와준다.
기본적인 사용법은 공식문서에도 잘 설명이 되어있으며, 기본적으로는 라이브러리를 설치한 다음 아래 코드와 같이 사용할 수 있다.
import ReactDOM from "react-dom";
import { useForm, SubmitHandler } from "react-hook-form";
enum GenderEnum {
female = "female",
male = "male",
other = "other"
}
interface IFormInput {
firstName: String;
gender: GenderEnum;
}
export default function App() {
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>First Name</label>
<input {...register("firstName")} />
<label>Gender Selection</label>
<select {...register("gender")} >
<option value="female">female</option>
<option value="male">male</option>
<option value="other">other</option>
</select>
<input type="submit" />
</form>
);
}
기본적으로 useForm API 를 통해서 register 를 사용할 수 있게 되는데, 의미 그대로 input 자체를 전체 form 에 등록시키겠다고 이해하면 편할 것이다. 위 코드에서 firstName, gender 2가지의 입력부분을 register 하고 있다. 이렇게 등록된 입력부분의 value 들에 한하여 유효성을 검사할 수 있으며, 제출을 담당하는 handleSubmit 에서 value를 읽을 수 있게 된다.
handleSubmit 을 통해서 등록된 value 들을 관리할 수 있는데, 함수 handleSubmit 에 인자로서 실제 입력폼에 등록된 값들을 컨트롤 할 수 있는 함수가 들어갈 수 있다. 여기서는 onSubmit 인데 이렇게 인자 onSubmit 의 경우 data 라는 입력값들에 접근할 수 있게 된다. 위에서는 console.log(data) 로 단순하게 콘솔에 입력값들이 어떤 형식으로 저장되고 있는지를 확인하고 있는데, 추후 코드를 작성할 때는 이러한 data 부분을 fetch 나 dispatch 등을 통해서 서버에게 전달해줄 수 있다.
또한 유효성에 대한 검사 역시 실제 HTML 기준을 따르고 있다(require, max, min, pattern...). 그래서 좀 더 친숙하게 사용할 수 있다.
import { useForm, SubmitHandler } from "react-hook-form";
interface IFormInput {
firstName: string;
lastName: string;
age: number;
}
export default function App() {
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true, maxLength: 20 })} />
<input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
<input type="number" {...register("age", { min: 18, max: 99 })} />
<input type="submit" />
</form>
);
}
기입할 때는 register 에서 추가해줄 수 있다.
이렇게 유효성이나 패턴을 검사한다면, 자연스럽게 이에 맞지 않는 기입을 할 시 에러 메세지를 표현하고 싶을것이다. 이럴때는 formState 를 활용할 수 있다.
import { useForm, SubmitHandler } from "react-hook-form";
interface IFormInputs {
firstName: string
lastName: string
}
const onSubmit: SubmitHandler<IFormInputs> = data => console.log(data);
export default function App() {
const { register, formState: { errors }, handleSubmit } = useForm<IFormInputs>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true })} />
{errors.firstName && "First name is required"}
<input {...register("lastName", { required: true })} />
{errors.lastName && "Last name is required"}
<input type="submit" />
</form>
);
}
미리 지정해둔 require 에 적합하지 않다면 errors 를 통해 표현하고 싶은 에러 메세지를 렌더할 수 있다.
결론적으로 사용하고 싶은 input 을 register 하며 전체 form 의 submit에 handleSubmit 을 기입함으로서 좀 더 손쉽게 입력폼을 다룰 수 있게 되었다. useState 를 input 마다 지정해줄 필요가 없어진 점에서 확실히 깔끔해진 느낌이다.
다만, 직접 input 태그를 통해서 타입을 지정하고 사용하는 것이라면 위처럼 register 를 활용해서 사용하겠지만, mui, antd 등 스타일 라이브러리를 사용하게 될 경우, 스타일 라이브러리의 입력 컴포넌트들 역시 자체 컨트롤러를 가지고 있다. 어떤 엘리먼트는 직접 HTML 엘리먼트를 사용하고 캘린더와 같은 입력 시 외부 라이브러리를 활용하겠다고 한다면, 이들을 모두 react-hook-form 에서 컨트롤 하는 방법으로 Controller 가 있다.
실제로 이번 프로젝트를 진행하면서 antd 를 활용하고 있었기에, 위 register 대신 controller 를 활용해서 전체 입력값을 컨트롤해야했고, 이러한 과정은 공식문서의 예시보다 현재 작성중인 프로젝트의 코드로서 대신 설명하고자 한다.
Add, Details 페이지를 구현하기전 고려해야 했던 부분들
지금 진행중인 프로젝트를 다시 상기해보면 집 옷장에 있는 의류들의 정보를 모두 인터넷에서 저장하고 관리하며 언제든 정보에 대해 쉽게 파악하는것을 목표로 하고 있었으며, 또한 기존 옷장에서는 얻기 힘든 정보(예를 들면 기장, 폭, 색상 등)들을 손쉽게 확인 할 수 있는 것이 구현 목표였다. 덩달아 입력된 정보를 가공하여 총 의류갯수, 총 가격 등 추가적인 정보 제공 역시 계획에 들어있었다.
그렇기에 다양한 입력값이 필요해서 입력 라이브러리를 선택하게 되었고, 이러한 입력페이지 혹은 입력폼을 단순히 Add 페이지에서만 사용할 것인가에 대해서부터 여러 고민들이 생겨나기 시작하였다.
- 입력 컴포넌트를 'ItemForm' 컴포넌트라고 지칭한다면, 이러한 ItemForm 컴포넌트는 당연하게도 Add 페이지에서 활용이 되어야 하며, 추후에 만들 상세 페이지에서 어떠한 수치를 수정하고 싶을 때도 사용할 수 있도록 구상하였어야 했다. 데이터를 수정할 때는 기존의 의류 데이터가 입력값에 미리 입력되어있어야 한다는 점을 고려해야한다.
- 의류는 Outer, Top 등 카테고리가 있고, 이러한 카테고리에 따른 실측 수치입력값이 차이가 발생한다. 당연하게도 입력 폼 자체에도 변화가 있어야 하는데 이를 어떻게 설정할 것인지 고려해야한다.
그 외, 이미지 업로드 방식이나 업로드된 이미지가 카테고리에 적합한지에 대한 검사 등 좀 더 구현할 사항이 남아있지만, 이 부분은 추후에 좀 더 다루도록 하고 우선 위 2가지 고려사항에 대해서 생각해야한다.
ItemForm 구상하기
베타버전의 프로젝트에서 입력값들을 어떤식으로 구상할지 고민하면서 의류의 이름 및 가격, 구매날짜, 색상 등 적절한 입력값을 선정하였고, 각각의 입력값들은 handleSubmit 을 통해서 관리되며, 입력값 전체는 dispatch 를 통해서 saga 를 거치면서 서버에 전달되도록 설정했다.
첫번째로 고민했었던 부분은, 각 입력값마다 동일한 input type 을 사용하는 것이 아니라, 어떤 부분은 달력, 어떤 부분은 숫자, 어떤 부분은 색상을 고르는 듯 각각의 type 이 모두 차이가 발생했고, 몇몇 입력폼은 antd 나 react-color 등을 사용하는 것이 더 효율적이라 판단하였다. 이렇게 하다 보니, 각각의 입력폼의 제목이나 설명문들을 일일히 하드코딩하여 작성할 수 밖에 없었는데 그 이유는 반복문을 통해 렌더를 하려고 하다보니, 우선 입력 컴포넌트 자체가 제각각이였고, 추가적으로 각각의 입력 컴포넌트들이 제각각의 속성들을 가지고 있어서 이를 모두 고려하는 배열 데이터를 만드는게 쉽지가 않았다.
글로만 설명하면 뭔가 이해하기 힘드니 코드를 참고해보자
입력폼에 필요한 Title 과 subTitle 들은 clothData 에 저장이 되어있다.
export const clothData = [
{
name: 'productName',
subTitle: '저장하시고 싶은 의류를 구별될 수 있도록 작성해주세요',
placeholder: 'product name',
errorMessage: '기입해주세요',
},
{
name: 'price',
subTitle: '구매하셨을 당시의 금액을 대략적으로 (원) 단위로 작성해주세요',
placeholder: 'price',
errorMessage: '가격을 기입해주세요',
},
{
name: 'color',
subTitle: '현 의류의 대표색상을 주어진 파레트에 따라 대략적으로 선택해주세요',
placeholder: 'color',
errorMessage: '색상을 선택해주세요',
},
{
name: 'preference',
subTitle: '현 의류의 선호도를 별점으로 매겨주세요(1~5점)',
placeholder: 'preference',
errorMessage: '별점을 매겨주세요',
},
{
name: 'purchaseDay',
subTitle: '월 단위로 언제 구매를 하셨는지 날짜를 기입해주세요',
placeholder: 'purchase month',
errorMessage: '대략적인 구매시기를 선택해주세요(월)',
},
];
보통은 이렇게 데이터를 미리 저장한다음, map 을 통하여 위 배열요소만큼 반복을 돌려서 렌더하는 방식을 react 에서 많이 사용하게 되는데, 이번에 Input 컴포넌트들에 한하여 map 을 활용하려고 하다보니, 각 input component 들의 속성들이 재각각이어서 map 을 활용하기가 힘들었다.
아래 코드에서 다른부분은 좀 생략하고 name 에 따른 input 컴포넌트들의 각 속성값들에 주목해보자.
// AinputElement.tsx
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { CirclePicker } from 'react-color';
import { FieldValues, useController } from 'react-hook-form';
import { InputNumber, Select, Input, DatePicker, Rate } from 'antd';
// 생략
// 타입인데 추후 설명
export type TPorps<T extends FieldValues> = CustomSelectProps<T> & TControl<T>;
function AInputElement<T extends FieldValues>(props: TPorps<T>) {
// props 를 구조분해한다
const { name, rules, control, errorMessage, options, defaultValue, placeholder } = props;
const {
field: { value, onChange }, // 필드값 및 onChange
fieldState: { isDirty, isTouched, error }, // 에러 등 필드 상태값
} = useController({ name, rules, control }); // control 과 rules, name 을 통해 useController 가 각 input 값들에 필요한 수치들을 반환한다.
return (
<>
// value, onChange 부분을 각각 input 컴포넌트에 적합하게 부여한다.
// 여기서 전달받은 name 에 따라 렌더할 input 컴포넌트가 결정이 되며, 보면 알겠지만 각각의 컴포넌트마다 속성값들에서 차이가 발생한다
{name == 'productName' ? <Input value={value} id={name} onChange={onChange} {...props} style={{ height: '30px', width: '100%' }} autoComplete='off' allowClear /> : null}
{name == 'color' ? <CirclePicker color={value} colors={colors} onChange={(color, event) => onChange(color.hex)} {...props} circleSize={25} width='100%' /> : null}
{name == 'price' ? <InputNumber value={value} id={name} min={1} onChange={onChange} style={{ height: '30px', width: '100%' }} placeholder={placeholder} /> : null}
{name == 'preference' ? <Rate value={value} onChange={onChange} /> : null}
// 특히 캘린더의 경우 defaultValue 가 필요했는데(데이터 수정 시 기존 데이터값을 불러올때), 이때 dayjs 를 활용한다.
{name == 'purchaseDay' ? (
<DatePicker defaultValue={dayjs(value || currentDate, dateFormat)} onChange={(date, dateString) => onChange(dateString)} picker='month' style={{ width: '100%', height: '30px' }} />
) : null}
{name == 'description' ? <TextArea value={value} id={name} onChange={onChange} placeholder={placeholder} rows={5} style={{ width: '100%' }} /> : null}
{name == 'categori' ? <Select defaultValue={defaultValue} value={value} id={name} options={options} onChange={onChange} style={{ height: '30px', width: '100%' }} /> : null}
// 에러가 발생할 경우 에러 메시지를 띄어준다. 이 역시 props 로 미리 전달해둔 값
<ErrorMessage>{error && errorMessage}</ErrorMessage>
</>
);
}
몇몇 코드와 타입을 생략했지만 그래도 뭔가 복잡해보인다.
일단 확인할 점은 Input, CirclePicker, InputNumber, Rate, DatePicker... 등등 각각의 입력폼의 속성값들이 저마다 다르며, 이를 모두 반영하여 ClothData 를 만드는것이 힘들다고 판단하였다. 그렇다고 ClothData 부분은 모두 ItemForm 부분에 기입하자니 추후에 입력폼이 추가될 경우 수정하기가 힘들 것 같았다.
이러한 고민 끝에 우선은 Props 로 받아오는 name 값에 따라 입력폼을 결정하게 하였고, 그 코드는 위에서 확인할 수 있을 것이다.
// ItemFrom.tsx
// 전체 다 생략하고 실제 AInputElement.tsx 컴포넌트가 쓰이는 부분만 나타내었다.
// 미리 생성해둔 clothData 의 배열 요소만큼 입력창이 렌더된다.
// control이 전달되고 있음을 확인하자.
return(
// 생략
{clothData.map(v => {
return (
<InputBackground key={v.name} title={v.name} subTitle={v.subTitle}>
<AInputElement control={control} name={v.name} errorMessage={v.errorMessage} placeholder={v.placeholder} rules={{ required: true }} />
</InputBackground>
);
})}
이렇게 구상하였기에 실제 ItemForm.tsx 에서 clothData 의 배열 요소만큼 map 으로 각 요소의 title, subTitle 을 전달해주면서 동시에, title 에 적합한 입력 컴포넌트를 렌더할 수 있었다. React-hook-form 의 control 을 활용하여, props 로 control 을 넘겨주고, 그로 인해 input 값의 value, onChange 를 컨트롤 할 수 있게 된다.
AInputElement 컴포넌트에서는 각 입력폼 컴포넌트들의 속성값들을 적당하게 설정해주고, name 에 따라 렌더할 입력폼을 선택적으로 렌더링한다.
사실 AInputElement 에서 name 에 따라 전달되는 입력 컴포넌트에 차이를 주는 방식이 마음에 드는것은 아니었다. 다르게 생각하면 저 부분도 결국 하드코딩이 아닐까 싶으면서 더 나은 방법에 대해 찾고 있지만, 현재로서는 일단 이러한 방식으로 코딩을 하였다.
이렇게 하게 되면 기본적인 입력 컴포넌트의 구상을 갖추게 된다.
카테고리별로 입력폼을 렌더해야한다
위 사진을 보게 되면 카테고리에 대한 select input 이 적용되어있음을 확인할 수 있다. 의류의 카테고리마다 실측 사이즈를 다르게 기입하여야 하기 때문에 select 를 통해서 다른 입력폼을 제공하려고 하였다.
일반적인 select input 의 경우 말그대로 여러 옵션중에 하나를 선택하는것으로 끝이난다. 실제 antd 의 select 입력컴포넌트만 해도 거기까지만 기능이 있다. 그렇기 때문에 선택된 값이 조건화로 되려면 실제 유저가 카테고리를 선택하였을 경우, 그 선택된 값을 읽은 뒤 이를 바탕으로 입력폼을 re-render 할 수 있어야 하며, 이렇게 어떠한 입력값의 변동이 일어날때마다 체크할 수 있는 기능을 찾아야했다.
다행이도 React-hook-form 에서는 이러한 기능을 수행할 수 있는 방법을 제시하는데, watch 를 이용하는 것이다.
watch 는 useForm 에서 제공하는 메서드로서, 특정 input의 값을 관찰할 수 있고, 그 값을 반환하게 된다. 예를 들자면 'price' 라는 이름을 가진 input 폼의 value 를 얻고싶다면 watch('price') 를 통해 얻을 수 있다. watch 에 대한 사용법은 공식 홈페이지 API -> useForm 부분에서 설명하고 있으니 좀 더 다른 기능을 사용하고 싶다면 확인하면 될 듯 하다.
현재 프로젝트에서 요구되는 사항을 다시 복귀해보자. 카테고리의 값에 따라 다른 셋의 입력폼이 렌더되어야 한다.
이러한 요구사항은 크게 2가지로 나뉠 수 있겠는데, 우선 카테고리의 값에 따라니깐 값을 파악할 수 있어야 조건화를 해줄 수 있을 것이며, 입력폼이 렌더되어야 한다고 했으니 실제 다시 렌더링이 일어나야 한다.
다행스럽게도 watch 메서드는 자동적으로 실행시 re-render 가 된다. 물론 상황에 따라서 전체가 다시 렌더링 되는것이니 퍼포먼스적으로 좋지 않을 수 있으나, 현 프로젝트의 요구사항상 여러개의 입력폼이 렌더되어야 하기에 큰 문제는 아니겠다고 판단하였다. 만약 실제 배포 후 퍼포먼스에 문제가 너무 심하게 발생한다면 이를 위해 useWatch 라는 훅의 사용을 고려해볼 수 있을 것이다.
실제 watch를 적용한 코드를 살펴보자
const ItemForm = ({ title, subTitle, type, itemId, Submit, resultNumber, setState }: FormProps) => {
const dispatch = useDispatch()
// 실제 useForm 을 통해 control, watch 등의 메서드를 가져올 것이다.
const methods = useForm<AddInitialValue>({
mode: 'onSubmit',
defaultValues: defaultValues,
});
// watch 를 포함한 여러 메서드를 구조분해로 가져온다
const {
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful },
} = methods;
// handleSubmit(onSubmit) 을 통해 입력값 data 에 접근한다
// 이후 이 data 를 수정하여 서버에 전송하기 위해 dispatch 한다.
const onSubmit = (data: AddInitialValue) => {
data.image = imagePath;
const Type = Submit();
console.log(data);
dispatch({
type: Type,
data: { items: data, clothId: itemId },
});
};
return (
<>
<PageMainLayout title={title} subTitle={subTitle}>
// 생략
// 카테고리 선택 input 폼이다. select input 이며 유저는 각 카테고리를 선택할 수 있다.
<InputPartial title='SORT CLOTHES' subtitle='카테고리를 선택해주시고, 각 카테고리에 맞는 측정치수를 cm 단위로 기입해주세요. 카테고리를 기입하셔야 이미지체크가 가능합니다'>
{categori.map(v => {
return (
<InputBackground key={v.name} title={v.name} subTitle={v.subTitle}>
<AInputElement control={control} name={v.name} errorMessage={v.errorMessage} options={v.options} defaultValue={v.defaultValue} rules={{ required: true }} />
</InputBackground>
);
})}
// watch('categori') 의 값이 무엇이냐에 따라 Measure 컴포넌트의 값을 결정해준다. nameArray 를 통해 input 폼을 가져온 다음 렌더한다.
{['Outer', 'Shirt', 'Top'].includes(watch('categori')) ? <Measure control={control} nameArray={topMeasureName} subTitleArray={topMeasureSub} placeholder='cm' /> : null}
{['Pant'].includes(watch('categori')) ? <Measure control={control} nameArray={bottomMeasureName} subTitleArray={bottomMeasureSub} placeholder='cm' /> : null}
{['Shoe'].includes(watch('categori')) ? <Measure control={control} nameArray={shoesMeasureName} subTitleArray={shoesMeasureSub} placeholder='mm' /> : null}
{['Muffler'].includes(watch('categori')) ? <Measure control={control} nameArray={mufflerMeasureName} subTitleArray={mufflerMeasureSub} placeholder='cm' /> : null}
</InputPartial>
// 생략
</>
);
};
좀 긴 코드지만 하나하나 살펴보면, useForm 을 통해서 watch 메서드를 가져온 다음, watch('categori') 를 통해 실제 카테고리의 값이 무엇인지에 따라 렌더할 Measure 컴포넌트가 결정된다. watch 는 자동으로 다시 렌더해주기 때문에, 추가된 Measure 컴포넌트 역시 다시 렌더링 되게 된다.
Measure 컴포넌트를 한번 살펴보자면,
// placeholder 를 추가해준다.
export type CustomSelectProps<T> = {
placeholder: string;
};
// 보통은 value 에 한해서는 FieldValues 타입으로 충분하나, 다른 props 가 많기 때문에 새롭게 타입을 정의하자
// 기본과 달리 nameArray 를 props 로 받기 때문에 FieldPath<T>[] 로 타입을 가져와야한다
// 이러한 점에서 기존에 정의한 TControl 과 차이가 있어서 새로 정의했다.
export type TPorps<T extends FieldValues> = CustomSelectProps<T> & TControlArray<T>;
function Measure<T extends FieldValues>(props: TPorps<T>) {
const { nameArray, rules, control, placeholder, subTitleArray } = props;
return (
<>
{nameArray.map((categori, i) => {
return (
<InputBackground title={categori.split('.')[1]} subTitle={subTitleArray && subTitleArray[i]}>
<ANumberInput control={control} name={categori} rules={rules} placeholder={placeholder} />
</InputBackground>
);
})}
</>
);
}
props 로 전달받은 nameArray 의 인자만큼 ANumberInput 컴포넌트를 렌더링 해줄 것이다. 예를 들어 'Top' 이라는 카테고리를 선택했다고 가정하겠다. 이렇게 되면, 우선적으로 Top 에 관련된 nameArray 를 가져오게 된다.
export const topMeasure = [
{ sort: 'categoriItem.chest', subtitle: '의류를 펼친 상태에서 가슴 가로 길이를 측정해주세요(cm)' },
{ sort: 'categoriItem.shoulder', subtitle: '왼쪽 어깨선과 오른쪽 어깨선을 가로로 측정해주세요(cm)' },
{ sort: 'categoriItem.arm', subtitle: '어깨선부터 팔 끝까지 길이를 측정해주세요(cm)' },
{ sort: 'categoriItem.totalLength', subtitle: '상의 라벨쪽부터 세로로 총 기장을 측정해주세요(cm)' },
];
// 실제 전달될 nameArray
export const topMeasureName = topMeasure.map(v => v.sort);
전달될 nameArray 는 ['categoriItem.chest', 'categoriItem.shoulder', 'categoriItem.arm', 'categoriItem.totalLength'] 가 된다. 이렇게 전될된 배열만큼 반복적으로 input 폼을 렌더하게 된다.
추가적으로 전달되는 Props 에 대한 Type 정의를 살펴보자면
import { Control, FieldPath, FieldValues, RegisterOptions } from 'react-hook-form';
export interface TControlArray<T extends FieldValues> {
nameArray: FieldPath<T>[];
control: Control<T>;
rules?: RegisterOptions<T>;
subTitleArray?: string[];
}
기본적으로 FieldValues 를 기반으로 타입을 정의할 수 있다. name 의 경우 FieldPath<T> 타입이 적용되며, 배열타입이니 배열로 적용하였다. control 의 경우 역시 기존에 정의된 Control<T> 로 정의하면 되고, rule 은 RegisterOptions<T> 로 정의해주면 된다.
만일 name 이 배열이 아니라면, 그냥 FieldPath<T> 로 변경만 해주면 된다.
이제 실제로 화면에서 어떤식으로 카테고리 선택창이 작동하는지 확인해보자.
좀 더 다룰 내용이 많지만...
단순하게 React-hook-form 에 대해서 다루고자 하였는데, 진행중인 프로젝트의 입력컴포넌트까지 다루게 되면서 글 자체가 혼란스러워 진것 같아 우선은 여기까지만 작성하고자 한다.
이번 포스팅에서는 React-Hook-Form의 장점과 더불어, 간단한 사용 방법과 진행중인 프로젝트에 어떻게 적용하고 있는지를 살펴보았다.
기본적인 입력셋과 카테고리에 따른 입력셋 변동을 어떻게 표현할지에 대해서 좀 더 쉽게 설명하고 싶었으나, 아직까지 표현력이 부족한거 같아서 아쉽다... (계속 열심히 작성해봐야지..)
다음 포스팅에서는 실제 이미지를 어떻게 업로드 할지에 대해 다뤄볼 예정이며, 클라이언트 부터 서버까지 전체적인 흐름을 어떤식으로 구현했는지, 어째서 Vision AI 를 활용하려고 하였는지에 대해 살펴보도록 하자
그 다음 포스팅에서는 실제 detail 페이지에서 정보를 수정할 때 어떻게 기존 정보를 가져올지에 대해서 간략하게 살펴보면서 입력 컴포넌트 ItemForm 에 대해서 포스팅을 마무리 하고자 한다.
'Practice' 카테고리의 다른 글
[Closet]이미지 파일을 Multer 와 Vision AI로 처리해보자 (0) | 2023.04.30 |
---|---|
[Closet] 이미지 업로드 시 Drag and Drop을 활용해보기 (0) | 2023.04.29 |
페이지마다 다른 레이아웃을 적용하기(Next.js) (0) | 2023.04.27 |
Next.js Middleware (1) | 2023.04.24 |
Polymorphic Component 는 어떻게 구현될까 (0) | 2023.03.02 |