react-hook-form은 일반적인 input 혹은 textarea를 다루는 것처럼 파일 또한 다룰 수 있다.
따라서 우리는 form에게 이미지를 주고, 또 실제로 연결해보도록 해보자.
<input
id="picture"
type="file"
className="hidden"
accept="image/*"
/>
form 태그 안에 위와 같이 file을 받는 input태그를 다룬다고 가정해보자.
input 태그 안에 늘 그러해왔듯이 {...register("image")} 속성을 넣어주자.
이렇게만 하면, 아주 간단하게 우리의 form은 파일도 받을 수 있게 된다.
import { useForm } from 'react-hook-form';
import useMutation from '@libs/client/useMutation';
const EditProfile: NextPage = () => {
const {
register,
setValue,
handleSubmit,
setError,
formState: { errors },
} = useForm<EditProfileForm>();
useEffect(() => {
if (user?.name) setValue('name', user.name);
if (user?.email) setValue('email', user.email);
if (user?.phone) setValue('phone', user.phone);
}, [setValue, user]);
const [editProfile, { data, loading }] =
useMutation<EditProfileResponse>(`/api/users/me`);
const onValid = ({ email, phone, name }: EditProfileForm) => {
if (loading) return;
if (email === '' && phone === '' && name === '') {
return setError('formErrors', {
message: 'Email OR Phone number are required. You need to choose one.',
});
}
editProfile({
email,
phone,
name,
// 프론트 딴에서 이메일, 폰넘버가 변경 없을 시 에러 처리
// email: email !== user?.email ? email : '',
// phone: phone !== user?.phone ? phone : '',
});
};
useEffect(() => {
if (data && !data.ok) {
return setError('formErrors', {
message: data.error,
});
}
}, [data, setError]);
// ...
return (
// ...
<form onSubmit={handleSubmit(onValid)}>
// ...
<input
{...register("image")}
id="picture"
type="file"
className="hidden"
accept="image/*"
/>
// ...
</form>
// ...
)
실제로 {...register("image")}를 등록해주었기 때문에, 이제 form을 제출하면
유저가 제출한 파일이 실제로 담기게 된다(참고로 우린 이미지 파일만을 허용한다).
이제 유저가 파일을 함께 담아서 form을 제출하게 된다면, 어떤 데이터가 담겨 있는지 함께 테스트를 한번 해보도록 하자.
react-hook-form의 handleSubmit을 이용하여 다음과 같이 확인해본다.
const onValid = ({ email, phone, name, image }: EditProfileForm) => {
console.log(image); // 우리가 등록한 {...register("image")} 에 대응하는 데이터를 출력해본다.
return;
};
이제 콘솔창을 확인해볼 시간이다.
보다시피 image에 대한 정보가 콘솔에 잘 나오고 있다.
FileList를 열어보면, 첫번째 원소에 우리의 파일이 들어가 있다. 파일의 이름, 사이즈, 타입에 대한 정보도 확인할 수 있다.
여기까지가 첫번째 단계였다. 이제 유저가 사진을 제출하면 우리는 handleSubmit의 onValid 함수에서 데이터로 받을 수 있다.
두 번째로는 유저가 이미지 파일을 제출했을 시, 이러한 변경을 감지했으면 좋겠다.
위같은 화면에서 유저가 이미지를 선택하였다면, 실제로 프로필 수정을 완료하기 전에, 위 프로필 사진 영역에 프로필 사진을 미리보기로 보여주는 것이다.
그러려면 useForm의 watch 함수를 이용하면 된다.
watch는 모든 form의 변경을 감지할 수 있다(원한다면 하나만 감지할 수도 있다).
console.log(watch("image"))
이를 실제로 컴포넌트에 넣고 콘솔창을 확인해보자.
이미지의 변경이 있을 때마다 변경된 정보를 출력해준다.
이제 위 watch("image")를 새로운 변수에 넣어주고 useEffect를 활용하여 원하는 미리보기 기능을 구현할 수 있다.
const avatar = watch('image');
useEffect(() => {
if (avatar && avatar.length > 0) {
const file = avatar[0];
// URL.createObjectURL(file);
}
}, [avatar]);
알다시피 자바스크립트는 유저가 직접 파일을 선택하여 브라우저에 업로드를 하는 것이 아니라면 유저의 컴퓨터 파일에 접근할 수 없다. 하지만 한번 파일을 선택하고나면 그 파일은 브라우저의 메모리 어딘가에 저장이 된다.
또한 위의 미리보기 기능을 구현하기 위해서는 유저가 선택한 그 파일을 읽을 수 있어야 한다. 그 파일을 읽어서 우리는 HTML의 img 태그에 사용하기 위해서 그 파일의 url을 알아야 한다.
따라서 브라우저의 메모리에 있는 파일의 url을 가져오기 위한 방법이 URL.createObjectURL(file) 이다. 이를 console.log로 출력하게되면 파일의 url이 출력된다. 이는 브라우저에서 사용할 수 있는 url이다. 즉 유저의 컴퓨터에 있는 사진이 이제는 url(브라우저가 만들어주는 url)을 통해서 브라우저도 접근할 수 있게 요청하는 것이 URL.createObjectURL(file)의 역할이다.
// ...
const file = avatar[0];
console.log(URL.createObjectURL(file))
// blob:http://localhost:3000/1af7c85e-5dfb-4e15-933a-0d9072aece50
// ...
위의 주석처리된 부분(blob: 부분)을 실제 브라우저의 url 부분에 넣고 이동하면 실제 우리가 선택한 파일이 잘 나오는 것을 확인할 수 있다. blob이 붙은 이 url은 브라우저의 메모리에만 존재한다.
이제 useState로 빠르게 새 state를 만들어보자.
const [avatarPreview, setAvatarPreview] = useState('');
그리고 setAvatarPreview 부분을 다음과 같이 설정해주자.
const [avatarPreview, setAvatarPreview] = useState('');
const avatar = watch('avatar');
useEffect(() => {
if (avatar && avatar.length > 0) {
const file = avatar[0];
setAvatarPreview(URL.createObjectURL(file));
}
}, [avatar]);
결과적으로는 avatarPreview에 브라우저에서 접근할 수 있는 유저가 선택한 이미지의 url이 담기게 된다.
이제 이를 위의 동그라미안에만 넣어주면 되는 것 아닌가? HTML의 img 태그에 avatarPreview만 넣어주면 될 것 같다.
{avatarPreview ? (
<img
src={avatarPreview}
className="h-14 w-14 rounded-full bg-slate-500"
/>
) : (
<div className="h-14 w-14 rounded-full bg-slate-500" />
)}
정상적으로 유저의 이미지 미리보기가 잘 나오고 있는 것을 확인할 수 있다.
https://nomadcoders.co/carrot-market