앞서서 이미지를 서버에 업로드하고, multer 를 통해 이미지를 저장한 다음, vision AI 를 통해서 현 이미지를 분석하여 이미지의 주소와 분석결과를 클라이언트에서 받아 Preview Card 를 렌더했었다.
그리고 그 전 이미 의류이름과 가격 등등 입력값들을 React-Hook-Form 을 통해서 관리하고 있었고, 이제 실제 데이터베이스에 입력값을 저장할 일만 남아있다. 그리고 저장된 의류 데이터를 수정할 수 있어야 했기에 기존 입력 데이터를 불러와 input 창에 기입되어 있는 상태로 렌더하는 경우도 고려해야했다. 정리해보자면,
- 받아온 이미지 주소와 함께 React-hook-form 에서 관리하는 입력값들을 서버 데이터베이스에 저장
- 상세 페이지에서 정보를 수정하고자 할 경우, 수정 이벤트 발생 시 기존 입력값들이 input 창에 그대로 렌더되어야 한다
위 두 구현 목표를 해결하기 위해 React-hook-form 의 reset 메서드를 활용해보도록 하자.
Reset
watch 메서드처럼 사용할 수 있는 reset 메서드는, 초기 설정한 defaultValue 의 폼 데이터 양식을 갖추면서, 내부 입력값들이 변경된 새로운 newValue 를 reset(new Value) 에 적용시켜주면, 실제 input 입력창에 변경된 value 값들이 그대로 입력되어 렌더가 된다.
옵션을 설정해주어 value 값을 유지할지, error 값을 유지할지 등을 설정할 수 있다. 다만 프로젝트의 목표는 기존 데이터 값을 가져와 defaultValue 대신 변경시켜주는 것이기에 다른 옵션은 굳이 사용하지 않을것이다.
Reset 을 사용하려면 몇가지 지켜줘야 할 사항이 있는데, 기본 예시 코드를 보면서 살펴보자
import { useForm, useFieldArray, Controller } from "./src";
function App() {
const {
register,
handleSubmit,
reset,
formState,
formState: { isSubmitSuccessful }
} = useForm({ defaultValues: { something: "anything" } });
const onSubmit = (data) => {
// It's recommended to reset in useEffect as execution order matters
// reset({ ...data })
};
React.useEffect(() => {
if (formState.isSubmitSuccessful) {
reset({ something: '' });
}
}, [formState, submittedData, reset]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("something")} />
<input type="submit" />
</form>
);
}
만약 control 을 통해 input value 를 관리하는 경우, Reset 을 활용하기 위해서는 useForm 에서 defaultValue 를 설정해주어야 한다. 위 코드를 보게 되면 useForm 부분에 defaultValue 가 설정되어있음을 확인할 수 있다. 간단한 예제이기에 한가지 input 값에 대한 기본값이 'anything' 으로 설정되어있다.
사용자는 등록된 input 'somthing' 에서 value 값을 변경할 것이고, submit 하게 될 것이다. useForm 에서는 제출의 성공상태를 나타내는 isSubmitSuccessful 를 가져올 수 있는데, 제출이 성공적으로 이루어 져 값이 true 로 되었다면 입력된 value 값을 지우는 reset({ somthing: "" }) 을 useEffect 를 통해 실행시킨다. 이를 통해 다시 입력창을 초기화 할 수 있다.
이 과정을 활용하면 기존 데이터값을 가져와 reset 을 통해 설정해주면, 이전에 입력했던 값들이 화면에 그대로 표시되게 될 것이다.
조금 주의할 점은 공식문서에서는 reset 을 useEffect 와 함께 사용하는것을 권장하고 있다. reset 이 발동되기 전에 useForm 의 구독과정이 먼저 끝나는 것이 좋기 때문이다. 위 코드에서도 useEffect 를 통해서 reset 를 호출하고 있다. 다만 구현 경험상 반드시 위 규칙을 지킬 필요는 없는것 같았고, 실제로 구현할떄도 위 사항을 지키진 않았었다.. useEffect 를 최대한 줄이는 것이 목표이기도 하였기에..
실제 의류 데이터를 가져와 reset 시켜보자
다시 프로젝트 구현 코드로 돌아가서 살펴보도록 하자.
const defaultValues = {
productName: '',
description: '',
image: [],
price: 0,
color: '',
categori: '카테고리를 선택해주세요',
purchaseDay: '',
categoriItem: {
shoulder: 0,
arm: 0,
totalLength: 0,
chest: 0,
rise: 0,
hem: 0,
waist: 0,
thigh: 0,
size: 0,
},
};
현재 입력폼에 설정해둔 defaultValue 부분이다. 이름부터 시작하여, 각각의 입력값의 초기값을 설정해두었다.
이렇게 설정해둔 defaultValue 는 useForm 에서 구독된다.
const methods = useForm<AddInitialValue>({
mode: 'onSubmit',
defaultValues: defaultValues,
});
const {
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful },
} = methods;
method 로 나누긴 했지만, 사실상 예제코드와 같은 상황이다. control 을 호출하고, watch, reset 메서드를 가져오고 있다.
잠깐 멈쳐서, 현재 입력폼들이 구현되어있는 컴포넌트 ItemForm 에 대해서 생각해보면, 이 컴포넌트는 Add 페이지와 Details 페이지에서 동시에 활용될 컴포넌트이다. 즉 어떤 상황에서는 defaultValue 그대로 적용되어야 하며, 어떤 상황에서는 이전 의류 입력값들이 적용된 newValue 가 구독되어야 한다.
그렇기에 우선 이 상황을 결정짓는 조건에 대해서 생각해야한다.
데이터를 수정한다는 것은, 이미 그 데이터가 존재한다는 의미이고, 그 데이터를 가져올 수 있으면 되는것이다. 그렇기에 만일 상태를 저장하는 reducer 에서 수정하고자 하는 의류 데이터가 존재함에 따라 구독할 Value 를 결정해주면 될 것이다.
export default (state = initialState, action: AnyAction) => {
return produce(state, draft => {
switch (action.type) {
// 생략
case t.LOAD_ITEM_SUCCESS: {
draft.loadItemLoding = false;
draft.loadItemDone = true;
draft.loadItemError = false;
draft.singleItem = action.data;
draft.imagePath = action.data.Images;
break;
}
// 생략
위 코드는 Details 페이지에서 하나의 의류에 대한 데이터값이 필요할 때, 서버로부터 특정 id 에 해당하는 의류값을 가져오는 reducer 의 action 부분이다. load 가 성공한다면 singleItem 이라는 상태에, 의류에 대한 데이터가 등록이 된다. 즉 상세페이지로 이동 시 getServerSideProps 에 의해 이미 reducer 에는 singleItem 이 null 이 아닌 의류 데이터 값으로 등록이 되어있다.
이 singleItem 을 조건으로 활용하면 될 것 같다. singleItem 이 존재한다면 newValue 를 useForm 에 구독시키고, 아니라면 기존 defaultValue 를 구독시키는 방향으로 조건 reset 을 적용하면 될 듯 싶다.
우선은 서버에서 받아오는 데이터가 useForm 의 defaultValue 의 양식과는 맞지 않기 때문에, 이 양식에 맞게 데이터를 수정해주어야 한다.
const ItemForm = ({ title, subTitle, type, itemId, Submit, resultNumber, setState }: FormProps) => {
const dispatch = useDispatch();
const [isClothes, setIsClothes] = useState(false);
const isDataChange = useRef(false);
const { imagePath, uploadItemsDone, uploadItemsError, imageUploadLoding, lastAddDataIndex, singleItem } = useSelector((state: rootReducerType) => state.post);
const methods = useForm<AddInitialValue>({
mode: 'onSubmit',
defaultValues: defaultValues,
});
const {
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful },
} = methods;
// 이름을 beforeValue 라 하였지만, 의류 데이터를 구독시키기 위한 객체를 선언해주자
let beforeValues = {};
// 만약 singleItem 이 존재한다면,
if (singleItem) {
// 카테고리 부분을 제외하고 나머지 데이터를 구조분해한다(singleData)
const { Outer, Shirt, Top, Pant, Shoe, Muffler, ...singleData } = singleItem;
// 분류한 카테고리 데이터 중에서 null 인 카테고리들은 모두 제거한다.
const categoriObject = [Outer, Shirt, Top, Pant, Shoe, Muffler].filter(v => v !== null)[0];
// 카테고리 데이터를 구조분해한다.
const { id, createdAt, updatedAt, ClothId, ...measure } = categoriObject!;
// defaultValue 의 형식에서 categoriItem 부분을 따로 구조분해한다.
const { categoriItem, ...rest } = defaultValues;
// defaultValue 의 categoriItem 부분에, 현재 의류의 카테고리 부분을 합쳐준다.
const measureItem = { categoriItem: { ...categoriItem, ...measure } };
// 최종적으로 이전 의류의 카테고리 제외 singleData 와 카테고리 부분 measureItem 부분을 합쳐준다.
beforeValues = { ...singleData, ...measureItem };
// singleItem 은 계속 존재하니, reset 이 무한으로 진행되기에 이를 막아주어야 한다.
// useRef 를 통해 boolean 값을 false 에서 true 로 변경해준다.
if (!isDataChange.current) {
isDataChange.current = true;
reset(beforeValues);
}
// singleItem 이 없다면
} else {
// 역시나 무한 랜더링을 막기 위함이다.
if (isDataChange.current) {
isDataChange.current = false;
reset(defaultValues);
}
}
// 어차피 singleItem 이 있다면 defaultValue -> beforeValues 로 변경된다.
useEffect(() => {
// 제출이 성공적이라면 defaultValue 로 우선 reset 해준다(Add 페이지의 reset 을 위해)
if (isSubmitSuccessful) {
reset({ defaultValues });
}
}, [isSubmitSuccessful]);
데이터를 가공하는 단계마다 주석을 통해 설명을 달아놓았다.
위 코드에서는 먼저 useSelector 를 통해서 상태값 singleItem 을 가져오게 된다. 상세페이지에 들어갔을 때 singleItem 이 업데이트 된다면 현 Itemform.tsx 는 자동으로 리렌더링이 되며(리덕스의 흐름상), 이제 singleItem 이 null 이 아니기에 조건문에서 true 로 적용된다.
데이터를 적절하게 defaultValue 형태로 가공한 다음 이 가공된 beforeValue 부분을 reset 메서드의 인자로서 실행시켜 useForm 에 구독시킨다.
처음 코드를 작성할 때 했었던 실수라면, useEffect 를 굳이 사용하지 않아도 되겠다 판단하여 reset(beforeValue) 를 바로 적용시켰더니, 무한 렌더링이 발생하였다. 사실 당연한 결과인것이, 상세페이지에서는 singleItem이 계속 true 일 것이고 이로서 계속해서 beforeValue 가 생성되어 reset 될 테니 무한 렌더링이 일어날 수 밖에 없다.
이를 해결하기 위해 함수 컴포넌트가 제 렌더링 되더라도 state 와 같이 이전 값을 기억할 수 있는 useRef 를 활용해서 boolean 값을 기억해놓도록 하였다. 처음 설정된 값이 false 라면, reset 이 실행되기 전에 false 를 true 로 변경한 다음 reset 을 실행시킨다. 이렇게 하면 설사 singleItem 이 존재하기에 다시 조건문이 실행된다 하더라도 최후 조건문인 !isChangeData.current 에 걸려서 reset 이 진행되지 않는다.
마찬가지로 singleItem 이 존재하지 않는 상황으로 갈 때, defaultValue 를 무한정 구독시키지 않게 하기 위해 isChangeData.current 조건을 걸어주었다.
마지막으로 useEffect 를 활용해서 서버에 데이터를 잘 제출했다면, 그에 따라 다시 defaultValue 로 reset 을 시켜주도록 하였다. 이 부분도 조금 고민을 했던 부분인데, 만일 데이터 수정을 위해 기존 데이터를 가져 온 뒤, 수정을 마치고 상세페이지로 가서 수정된 부분을 확인했을 때, 그 다음 다시 수정이 필요해서 수정버튼을 누르면 당연하게도 역시 기존 데이터가 구독되어있어야 했다. 즉 흐름은 다음과 같다
- singleItem으로 beforeValue 구독 -> 일부 입력값 수정 -> 수정 완료 시 submitSuccessful ->
useEffect 로 defaultValue 구독 -> 상세페이지에서 다시 수정하기 클릭 -> defaultValue 구독 적용???
만일 제출 후 defaultValue 구독상태가 이어져있다면, 기존데이터 구독이 아닌 초기값이 구독이 되어있을 것이다. 하지만 실제로 진행시켜보면 수정페이지에서는 beforeValue 로 잘 구독이 되어서 나온다.
사실 이 부분을 고려해서 beforeValue 부분을 useEffect 내부에서 진행하지 않았던 것이기도 하다. 실제로 useEffect 가 실행이 되면 함수 컴포넌트가 다시 실행되기에, 이 부분을 활용해서 다시 실행 시 singleItem 유무를 체크할 것이고, 이에 따라 beforeValue 로 다시 구독시킬것을 알았기 때문이다. (렌더의 효율성을 좀 더 상승시키기 위해 코드를 구현했지만 실제로 어느정도 효율이 발생할것인지에 대해선 아직 테스트를 해보진 못했다..)
실제 상세페이지에서 수정을 눌렀을 때의 모습을 확인해보자
정리
사실 의류 데이터를 서버에 저장하고 수정하는 과정은 프론트단 보다 백엔드 단이 더 나에게 고통을 주었던 걸로 기억이 된다. 하지만 프론트 역시 reset 을 사용하면 되겠구나 라고만 지례짐작하고 실제 코드를 작성해보니 여러 오류들을 마주하면서 나름 고생을 했었다.
처음 add 페이지만 생각하여 ItemForm 컴포넌트를 작성했었는데, 당시에는 다른 페이지에서 현 컴포넌트를 재사용할 것이라고 판단하지 않았고 그렇기에 큰 고민없이 코드를 작성했었던 것 같다.
그러다 다른 페이지에서도 ItemForm 컴포넌트가 필요하게 되었고, 그러다 보니 처음부터 설계가 재사용을 고려하여 작성된 것이 아니다보니 다시 수정하는데 꽤나 시간이 들었던 것 같다. 물론 지금 코드도 확장성이 좋다는것은 아니지만... 컴포넌트를 작성할 때는 항상 이 컴포넌트가 다른 환경에서도 사용될 수 있음을 고려하고 작성하는것이 추후 유지보수에 유리할 수 있음을 느끼는 경험이었다.
다음 포스팅은 마지막으로 서버단에서 데이터를 어떻게 수정할지, 수정할 때 무엇을 고려했어야 하는지에 대해 다뤄보고자 한다.
'Practice' 카테고리의 다른 글
[Closet]Cursor-based-pagination 이 효율은 좋지만.. (0) | 2023.05.01 |
---|---|
[Closet]Sequelize custom method 을 활용한 데이터 수정 (0) | 2023.04.30 |
[Closet]이미지 파일을 Multer 와 Vision AI로 처리해보자 (0) | 2023.04.30 |
[Closet] 이미지 업로드 시 Drag and Drop을 활용해보기 (0) | 2023.04.29 |
[Closet]React-hook-form을 통해 입력폼 구성하기 (0) | 2023.04.29 |