2023. 9. 29. 00:02ㆍProject Tours/Tour on Plantopia
1. 고민의 시작
Plantopia라는 식물 관리 어플리케이션을 만들면서 식물 등록을 위한 form을 다루게 되었다. 처음 코드를 작성할 때는 여러 개 state를 두고 각각에 onChange 함수를 두는 방식으로 작성했다. 또한, 에러 역시 하나하나 조건을 걸며 작성해야하는 것 또한 반복되었다. 프로젝트 당시 작성을 하면서도 여러번 반복을 하는 코드를 적는 것에 심적으로 불편했다. 하지만, 프로젝트를 할 때는 3주 밖에 안되는 시간에 기획부터 시작하려니 이런 고민을 더 이어나가지 못했는데 프로젝트가 끝난 후 form을 다루는 좋은 방법을 찾아나서게 되었다.
2. 고민 해결의 과정
처음 코드는 아래와 같았다.
고민 - 1 . 중복된 코드로 코드 길이가 길어지고 가독성이 떨어지는 문제
const [searchInputValue, setSearchInputValue] = useState(name);
const [plantName, setPlantName] = useState<string>('');
const [purchasedDay, setPurchasedDay] = useState<string>('');
const [wateredDays, setWateredDays] = useState<string>('');
const [frequency, setFrequency] = useState(waterCodeToNumber(waterCode));
const [imgUrl, setImgUrl] = useState<string | null>(null);
const [previewImg, setPreviewImg] = useState<string>();
const [saving, setSaving] = useState(false);
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchInputValue(e.target.value);
};
const navigateSearch = () => {
navigate('/dict/search', {
state: { inputValue: '' },
});
};
const plantNameHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setPlantName(e.target.value);
};
const handleFrequency = (e: React.ChangeEvent<HTMLInputElement>) => {
setFrequency(Number(e.target.value));
};
const purchasedDayHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setPurchasedDay(e.target.value);
};
const wateredDaysHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setWateredDays(e.target.value);
};
> 때로는 중복이 나을 수 있다고 하지만 만약 input이 늘어난다면 찾기 힘들고 지금과 같이 변수명이 통일되지 않는 상태에서 변수명을 바꾼다면 혹은 input을 관리하는 함수에 추가 기능을 넣는다면 그 확장성에 문제가 생기지 않을까 생각이 들었다. 그리고 이런 부분에서 가장 먼저 떠오르는 해결책은 커스텀 훅이었다.
import { useState } from 'react';
const useInput = input => {
const [value, setValue] = useState(input);
const handleValue = e => {
setValue(e.target.value);
};
return { value, handleValue };
};
> 하지만, 여기서 이 전 배웠던 렌더링 최적화에 대한 이야기가 생각났다. 커스텀 훅은 코드의 수를 줄여줄 수 있지만 onChange 함수를 통해서 input값이 변할 때마다 state를 변하게 하고 state는 변화는 렌더링을 일으킨다. 사실 우리에게 필요한 것은 form이 완성되었을 때만 state 값을 변경하는 것이다.
> 그러다가 인프런의 글을 보게되었다.
https://tech.inflab.com/202207-rallit-form-refactoring/react-hook-form/
> 해당 내용에서 눈여겨 볼 점은 react-hook-form이 비제어 컴포넌트 방식으로 구현되어 있어 state를 사용하지 않는다는 점이다. 그렇기 때문에 불필요한 리렌더링을 막을 수 있었다. 그래서 이와 더불어 많은 기업들이 이미 도입해 쓰고 있기에 써봐야겠다고 결정했고 이 결정과 별개로 라이브러리 도입에 대해서 배운 것이 있었다. 나는 혼자 프로젝트를 만들고 있어서 도입 결정에 어려움이 없지만 회사에서는 많은 사람과 일하다보니 새로운 라이브러리 도입 시 많은 사람들이 모두 그 라이브러리를 배워야하고 더 큰 규모에 프로젝트에서는 전반적인 코드 상의 안정성을 따져야한다는 것을 배웠다.
고민 - 2 . 에러 처리 및 추가에 대한 어려움
const handleRegister = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSaving(true);
if (!searchInputValue) {
errorNoti('식물을 지정해주세요.');
setSaving(false);
return;
}
if (!plantName) {
errorNoti('식물 닉네임을 설정해주세요.');
setSaving(false);
return;
}
if (!frequency) {
errorNoti('식물의 물 주기를 설정해주세요.');
setSaving(false);
return;
}
if (!purchasedDay) {
errorNoti('식물과 함께한 날을 지정해주세요.');
setSaving(false);
return;
}
const q = query(
collection(db, 'plant'),
where('userEmail', '==', user?.email),
);
const querySnapshot = await getDocs(q);
const isEmpty = querySnapshot.empty;
const newPlantData = {
frequency: waterCodeToNumber(waterCode),
imgUrl: imgUrl || image,
isMain: isEmpty ? true : false,
nickname: plantName,
plantName: searchInputValue,
purchasedDay: dateToTimestamp(purchasedDay),
userEmail: user?.email,
wateredDays: wateredDays ? [dateToTimestamp(wateredDays)] : [],
};
await addDoc(collection(db, 'plant'), newPlantData);
successNoti('새 식물 등록에 성공하였습니다');
navigate('/myplant');
};
> 코드 중복을 줄일 수 있고 불필요한 렌더링 문제를 해결했지만 react-hook-form 꼭 써보고 싶다는 생각을 한 것은 라이브러리에서 제공하는 다양한 메서드가 그 중 에러 처리를 아주 수월하게 해결할 수 있었다. onSubmit에 함수로 두 개의 인자를 받는데 vaild, invalid할 때를 함수로 받아 분리해서 처리할 수 있어 이해하기 쉽고, 위 코드를 보면 저장할 때 모든 에러를 하나하나 처리했어야하는데 제공해주는 메서드들이 훨씬 수월하게 처리하게 해준다.
3. 적용
const onInvalid = (errors: FieldErrors) => {
for (const fieldName in errors) {
if (errors[fieldName]?.message) {
const message = errors[fieldName]?.message as string;
errorNoti(message);
return;
}
}
};
const onValid = async (data: UserPlantForm) => {
setSaving(true);
if (!user?.email) return;
const isEmpty = await isUserPlantEmpty(user?.email);
const newPlantData = {
frequency: data.frequency,
imgUrl: imgUrl || image,
isMain: isEmpty ? true : false,
nickname: data.nickname,
plantName: data.plantName,
purchasedDay: dateToTimestamp(data.purchasedDay),
userEmail: user?.email,
wateredDays: data.wateredDays ? [dateToTimestamp(data.wateredDays)] : [],
};
registerPlantData(newPlantData as UserPlant);
successNoti('새 식물 등록에 성공하였습니다');
navigate('/myplant');
};
> 저 위의 코드가 아래와 같은 코드로 변모하였다. 무엇보다도 눈에 띄는 점은 코드 수를 획기적으로 줄인 점이다. 생각보다 적용이 어려웠던 점은 프로젝트에서 react-toasfify 라이브러리와 함께 적용하는 것에서 헤맸다. react-hook-form에서 error 메서드는 새로 html을 만들어서 에러 메시지를 띄우는 방식을 주로 알려주는데 방법을 찾다가 toastify를 invalid 함수로 빼주었다.
> 추가적으로 인프런의 글을 읽으면서 필자의 고민의 깊이를 느껴볼 수 있었다. 지금의 나는 미약하지만 좀 더 지식과 경험을 쌓아 저 깊이를 따라잡고 싶다.
'Project Tours > Tour on Plantopia' 카테고리의 다른 글
[Revisit Project] 압축을 통해 이미지 최적화 해보기 (0) | 2023.10.03 |
---|---|
[Revisit Project] Vite과 코드 분할로 성능 최적화 (0) | 2023.10.02 |
[Revisit Project] Firebase 코드 분리로 레이어 (0) | 2023.10.01 |