728x90
리액트 훅 폼+yup을 사용해 input 공통 컴포넌트 만들기
✨초기작업
타입에 따라 input 분기처리하기
import { passwordOffIcon, passwordOnIcon } from '@/libs/utils/Icon';
import Image from 'next/image';
import React, { useState } from 'react';
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
type InputProps = {
label: string;
name: string;
type?: 'text' | 'password';
placeholder: string;
register: UseFormRegisterReturn;
error?: FieldError;
};
export default function Input({
label,
name,
type,
placeholder,
register,
error,
}: InputProps) {
const [isVisibilityIcon, setIsVisibilityIcon] = useState(false);
const togglePasswordVisibility = () => {
setIsVisibilityIcon(!isVisibilityIcon);
};
return (
<div className="relative flex w-full max-w-[640px] flex-col gap-2">
<label htmlFor={name}>{label}</label>
{type === 'text' ? (
<input
type="text"
id={name}
placeholder={placeholder}
{...register}
className={`h-[58px] rounded-[6px] border-2 px-4 outline-none ${error && 'border-red-400'}`}
/>
) : (
<>
<input
type={isVisibilityIcon ? 'text' : 'password'}
id={name}
placeholder={placeholder}
{...register}
className={`h-[58px] rounded-[6px] border-2 px-4 outline-none ${error && 'border-red-400'}`}
/>
<Image
src={isVisibilityIcon ? passwordOffIcon : passwordOnIcon}
width={24}
height={24}
alt="password"
className="absolute right-4 top-12 cursor-pointer"
onClick={togglePasswordVisibility}
/>
</>
)}
{error && <p className="text-red-400">{error.message}</p>}
</div>
);
}
💣register 타입 에러
UseFormRegister - 오류… 자꾸 타입을 쓰라고 나왔다.
UseFormRegisterReturn로 써서 해결!
UseFormRegisterReturn는 register 함수의 반환값을 나타내는 타입이니다. 즉, register 함수를 호출했을 때 반환되는 객체의 타입이다. 이 반환값은 입력 필드에 필요한 이벤트 핸들러(onChange, onBlur)와 ref를 포함하고 있다. 입력 필드에 직접 전달할 때는 이 타입을 사용해야 한다!
🤔input의 크기가 다를 때..?
기본값을 설정하고 필요할 때 프롭스로 내려주자.
import passwordOffIcon from '@/../public/icon/passwordOffIcon.svg';
import passwordOnIcon from '@/../public/icon/passwordOnIcon.svg';
import Image from 'next/image';
import React, { useState } from 'react';
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
/**
* 재사용 가능한 입력 컴포넌트로, 텍스트, 비밀번호, 숫자 타입을 지원합니다.
* 비밀번호 필드에는 비밀번호 표시/숨기기 토글이 포함되어 있습니다.
*
* @example
* ```jsx
* const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
* resolver: yupResolver(schema),
* });
*
* const onSubmit: SubmitHandler<FormData> = (data) => {
* console.log('폼 제출', data);
* };
*
* <Input
* label="이메일"
* name="email"
* type="text"
* placeholder="이메일을 입력해주세요"
* register={register('email')}
* error={errors.email}
* maxWidth="640px"
* />
* ```
*
* @typedef {Object} InputProps
* @property {string} label - 입력 필드의 레이블
* @property {string} name - 입력 필드의 이름
* @property {'text' | 'password' | 'number'} [type='text'] - 입력 필드의 타입
* @property {string} placeholder - 입력 필드의 플레이스홀더 텍스트
* @property {UseFormRegisterReturn} register - react-hook-form의 register 함수 반환값
* @property {FieldError} [error] - 입력 필드의 에러 정보
* @property {string} [maxWidth='640'] - (반응형을 위한)입력 필드 컨테이너의 최대 너비. 기본값 640px
* @author 김보미
*/
type InputProps = {
label: string;
name: string;
type?: 'text' | 'password' | 'number';
placeholder: string;
register: UseFormRegisterReturn;
error?: FieldError;
maxWidth?: string;
};
export default function Input({
label,
name,
type = 'text',
placeholder,
register,
error,
maxWidth = '640px',
}: InputProps) {
const [isVisibilityIcon, setIsVisibilityIcon] = useState(false);
const togglePasswordVisibility = () => {
setIsVisibilityIcon(!isVisibilityIcon);
};
return (
<div
className={`relative flex w-full flex-col gap-2`}
style={{ maxWidth: maxWidth }}
>
<label htmlFor={name}>{label}</label>
{type === 'text' || type === 'number' ? (
<input
type={type}
id={name}
placeholder={placeholder}
{...register}
className={`h-58 rounded-md border-2 px-16 outline-none focus:border-custom-green-200 ${error && 'border-red-400'}`}
/>
) : type === 'password' ? (
<>
<input
type={isVisibilityIcon ? 'text' : 'password'}
id={name}
placeholder={placeholder}
{...register}
className={`h-58 rounded-md border-2 px-16 outline-none focus:border-custom-green-200 ${error && 'border-red-400'}`}
/>
<Image
src={isVisibilityIcon ? passwordOffIcon : passwordOnIcon}
width={24}
height={24}
alt="비밀번호 가시성 토글"
className="absolute right-20 top-42 cursor-pointer"
onClick={togglePasswordVisibility}
/>
</>
) : null}
{error && (
<p className="pl-8 text-xs-regular text-custom-red-200">
{error.message}
</p>
)}
</div>
);
}
- 기본 640 설정하고 필요하다면 props로 maxWidth써주기!
✨최종 코드
- input 컴포넌트
import React, { useState } from 'react';
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
import PasswordInput from './PasswordInput';
type InputProps = {
label: string;
name: string;
type?: 'text' | 'password' | 'number' | 'email';
placeholder: string;
register: UseFormRegisterReturn;
error?: FieldError;
maxWidth?: string;
onBlur?: () => void;
};
/**
* @param {string} label - 입력 필드의 레이블
* @param {string} name - 입력 필드의 이름
* @param {'text' | 'password' | 'number' | 'email'} [type='text'] - 입력 필드의 타입
* @param {string} placeholder - 입력 필드의 플레이스홀더 텍스트
* @param {UseFormRegisterReturn} register - react-hook-form의 register 함수 반환값
* @param {FieldError} [error] - 입력 필드의 에러 정보
* @param {string} [maxWidth='640px'] - 반응형을 위한 입력 필드 컨테이너의 최대 너비. 기본값 640px
* @param {() => void} [onBlur] - 포커스 아웃 시 호출되는 함수
* @returns {JSX.Element} - 렌더링된 Input 컴포넌트
* @example
* const { register, handleSubmit, formState: { errors }, onBlur } = useForm<FormData>({
* resolver: yupResolver(schema),
* });
*
* const onSubmit: SubmitHandler<FormData> = (data) => {
* console.log('폼 제출', data);
* };
*
* <Input
* label="이메일"
* name="email"
* type="text"
* placeholder="이메일을 입력해주세요"
* register={register('email')}
* error={errors.email}
* maxWidth="640px"
* onBlur={() => trigger('email')}
* />
*/
export default function Input({
label,
name,
type,
placeholder,
register,
error,
maxWidth = '640px',
onBlur,
}: InputProps) {
const [isVibrating, setIsVibrating] = useState(false);
const handleBlur = () => {
if (onBlur) onBlur();
if (!error) return;
setIsVibrating(true);
setTimeout(() => setIsVibrating(false), 300);
};
return (
<div className={`relative flex w-full flex-col gap-2`} style={{ maxWidth }}>
<label htmlFor={name}>{label}</label>
{(type === 'text' || type === 'number' || type === 'email') && (
<input
type={type}
id={name}
placeholder={placeholder}
{...register}
onBlur={handleBlur}
className={`h-58 rounded-md border-2 px-16 outline-none focus:border-custom-green-200 ${isVibrating && 'animate-vibration'} ${error && 'border-red-400'}`}
/>
)}
{type === 'password' && (
<PasswordInput
name={name}
placeholder={placeholder}
register={register}
error={error}
onBlur={handleBlur}
/>
)}
{error && (
<p className="pl-8 text-xs-regular text-custom-red-200">
{error.message}
</p>
)}
</div>
);
}
2. PasswordInput 컴포넌트
import { PasswordOffIcon, PasswordOnIcon } from '@/libs/utils/Icon';
import React, { useState } from 'react';
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
type PasswordInputProps = {
name: string;
placeholder: string;
register: UseFormRegisterReturn;
error?: FieldError;
onBlur?: () => void;
};
/**
* 비밀번호 입력 필드 컴포넌트로, 비밀번호 표시/숨기기 기능을 포함합니다.
*
* @param {string} name - 입력 필드의 이름
* @param {string} placeholder - 입력 필드의 플레이스홀더 텍스트
* @param {UseFormRegisterReturn} register - react-hook-form의 register 함수 반환값
* @param {FieldError} [error] - 입력 필드의 에러 정보 (선택적)
* @param {() => void} [onBlur] - 포커스 아웃 시 호출되는 함수
* @returns {JSX.Element} - 렌더링된 PasswordInput 컴포넌트
*
* @author 김보미
*/
export default function PasswordInput({
name,
placeholder,
register,
error,
onBlur,
}: PasswordInputProps) {
const [isVisibilityIcon, setIsVisibilityIcon] = useState(false);
const [isVibrating, setIsVibrating] = useState(false);
const togglePasswordVisibility = () => {
setIsVisibilityIcon(!isVisibilityIcon);
};
const handleBlur = () => {
if (onBlur) onBlur();
if (!error) return;
setIsVibrating(true);
setTimeout(() => setIsVibrating(false), 300);
};
return (
<>
<input
type={isVisibilityIcon ? 'text' : 'password'}
id={name}
placeholder={placeholder}
{...register}
onBlur={handleBlur}
className={`h-58 rounded-md border-2 px-16 outline-none focus:border-custom-green-200 ${isVibrating && 'animate-vibration'} ${error && 'border-red-400'}`}
/>
{isVisibilityIcon ? (
<PasswordOnIcon
width={24}
height={24}
className="absolute right-20 top-42 cursor-pointer"
onClick={togglePasswordVisibility}
/>
) : (
<PasswordOffIcon
width={24}
height={24}
className="absolute right-20 top-42 cursor-pointer"
onClick={togglePasswordVisibility}
/>
)}
</>
);
}
- Textarea 컴포넌트
import React, { TextareaHTMLAttributes } from 'react';
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
label: string;
register: UseFormRegisterReturn;
error?: FieldError;
maxWidth?: string;
height?: string;
onBlur?: () => void;
};
/**
* Textarea 컴포넌트
* @param {string} label - 텍스트영역의 레이블
* @param {UseFormRegisterReturn} register - react-hook-form의 register 함수 반환값
* @param {FieldError} [error] - 텍스트영역의 에러 정보 (선택적)
* @param {string} [maxWidth='640px'] - (반응형을 위한)입력 필드 컨테이너의 최대 너비. 기본값 640px
* @param {string} [height='346px'] - (반응형을 위한)입력 필드의 높이. 기본값 346px
* @returns {JSX.Element} - 렌더링된 Textarea 컴포넌트
* @example
* const {
* register,
* handleSubmit,
* formState: { errors },
* } = useForm<FormData>({
* resolver: yupResolver(schema),
* });
*
* const onSubmit: SubmitHandler<FormData> = (data) => {
* console.log('폼 제출', data);
* };
*
* <Textarea
* label="내용"
* name="content"
* placeholder="내용을 입력해주세요"
* register={register('content')}
* error={errors.content}
* onBlur={() => trigger('content')}
* />
*
* @author 김보미
*/
export default function Textarea({
label,
register,
error,
maxWidth = '640px',
height = '346px',
onBlur,
...props
}: TextareaProps) {
const { name, placeholder } = props;
return (
<div className="relative flex w-full flex-col gap-2" style={{ maxWidth }}>
<label htmlFor={name}>{label}</label>
<textarea
id={name}
placeholder={placeholder}
{...register}
onBlur={onBlur}
className={`resize-none rounded-md border-2 p-16 outline-none focus:border-custom-green-200 ${error ? 'border-red-400' : ''}`}
style={{ height }}
{...props}
/>
{error && (
<p className="pl-8 text-xs-regular text-custom-red-200">
{error.message}
</p>
)}
</div>
);
}
✨결과
'🔥React 뽀개기' 카테고리의 다른 글
Toast 공통 컴포넌트 만들기 (feat.react-toastify) (0) | 2024.08.08 |
---|---|
react-hook-form + yup = 회원가입/로그인(feat.쿠키) (0) | 2024.08.02 |
라이브러리 없이 state로 폼 만들기 (0) | 2024.07.31 |
스크롤시 투명한 배경에서 배경색 넣기(feat. 화살표 클릭 시 맨 위로 이동하기) (0) | 2024.07.27 |
서버 상태 💞 클라이언트 상태 (0) | 2024.07.20 |