멀티스텝 폼의 계정 정보 조회 경로
TL;DR
- 중복 API 호출을 막기 전에 그 호출이 화면에 어떤 값을 주입하는지 먼저 봐야 한다.
- 멀티스텝 폼은 단계 이동 시 컴포넌트가 언마운트될 수 있고,
useForm()상태도 함께 사라진다. - React StrictMode의 개발용 effect 재실행은 production 호출 수와 다르다.
- 중복 호출은 파일 단위가 아니라 사용자 경로 단위로 찾아야 한다.
신청 폼 첫 단계에서 계정 정보 API가 두 번 호출됐다. 서버 사이드에서 작성 중인 신청서를 이미 내려주고 있었고, 그 안에 이름과 연락처도 있었다. 그런데 클라이언트가 같은 계정 정보를 다시 받아오고 있었다. 표면적인 목표는 호출 수를 줄이는 것이었다. 실제 문제는 더 좁았다. fetchAccountProfile()이 “값을 받아오는 일”과 “폼에 초기값을 넣는 일”을 동시에 하고 있었다.
단계 전환과 form state
폼은 여러 단계로 나뉘어 있었다.
{stepType === "basic" && <BasicInfoStep />}
{stepType === "address" && <AddressStep />}
현재 단계가 아니면 컴포넌트는 언마운트된다. 첫 단계 컴포넌트 안의 useForm()도 함께 사라지고, 다시 돌아오면 새 form state가 만들어진다. 그래서 각 단계는 마운트 시점에 값을 다시 채운다.
useEffect(() => {
fetchAccountProfile().then((user) => {
form.setValue("name", user.name);
form.setValue("tel", user.tel);
});
}, []);
useEffect(() => {
if (savedForm) {
form.setValue("site", savedForm.site);
}
}, [savedForm]);
이름과 연락처는 계정 API가 넣고, 주소는 저장된 신청서가 넣는다. 출처가 필드별로 갈라져 있었다.
fetch 차단이 초기값 차단이 된 구조
저장된 신청서가 있으면 계정 정보를 다시 부르지 않도록 가드를 추가했다.
useEffect(() => {
if (savedForm) return;
fetchAccountProfile().then((user) => {
form.setValue("name", user.name);
form.setValue("tel", user.tel);
});
}, [savedForm]);
호출은 줄었다. 대신 이전 단계로 돌아왔을 때 이름과 연락처가 비었다. 저장본에서 되돌리는 코드는 주소만 채우고 있었기 때문이다. 게다가 두 필드는 사용자가 수정할 수 없는 disabled input이었다. 다음 버튼은 그 값을 필요로 했다.
const disabledNext =
!form.watch("name") ||
!form.watch("tel") ||
hasFieldErrors(form.formState.errors, ["name", "tel"]);
값을 넣는 경로를 막고, 사용자가 직접 넣을 수도 없게 만든 셈이다. 해결은 fetch 가드가 아니라 초기값 주입 경로 분리였다.
function applyBasicInfo(form: UseFormReturn<Form>, source: BasicInfo) {
form.setValue("name", source.name);
form.setValue("tel", source.tel);
}
useEffect(() => { if (savedForm?.basic) { applyBasicInfo(form, savedForm.basic); return; }
fetchAccountProfile().then((user) => {
applyBasicInfo(form, user);
});
}, [savedForm]);
API 호출을 막아도 폼을 채우는 경로는 남아 있어야 한다.
개발 모드 호출 수
네트워크 탭에서 호출 수를 셀 때 React StrictMode를 같이 봐야 한다. React 18 개발 모드의 StrictMode는 effect를 한 번 더 실행해 cleanup 누락 같은 문제를 드러낸다.
development + StrictMode
-> effect setup
-> cleanup
-> effect setup
production
-> effect setup
따라서 개발 화면의 호출 수를 그대로 production 호출 수로 볼 수 없다. 개발에서 두 번 보이는 호출 중 일부는 StrictMode 검사일 수 있다. 그렇다고 모든 중복이 StrictMode 때문인 것은 아니다. StrictMode는 절대 횟수를 오염시키지만, 특정 사용자 경로에서만 추가되는 호출은 다른 호출 지점을 찾아야 한다.
사용자 경로 기준 추적
중복 호출은 같은 파일 안에 없었다. 진입 화면에서 “새로 신청”을 누르는 경로에 계정 정보 호출이 있었다.
const startNew = async () => {
const result = await deleteDraft();
if (result.code === 200) {
await fetchAccountProfile();
moveToRequest();
} else {
moveToRequest();
}
};
반환값은 어디에도 쓰지 않았다. 신청 화면은 자기 초기화 과정에서 다시 필요한 값을 받는다. 진입 화면의 호출은 화면에도 state에도 남는 것이 없었다. 이 호출을 제거하자 특정 경로에서만 추가되던 호출이 사라졌다.
진입 화면 새로 신청
-> 불필요한 fetchAccountProfile 제거
신청 화면 첫 단계
-> 저장본 또는 계정 API로 폼 초기화
정리
API 호출 최적화는 조건문을 먼저 추가하는 일이 아니다. 그 호출이 어떤 값을 어디에 남기는지부터 분리해야 한다. 이번 문제의 기준은 세 가지였다.
- fetch와 form 초기값 주입을 같은 함수에 묶지 않는다.
- 개발 모드 StrictMode 호출 수와 production 호출 수를 구분한다.
- 중복 호출은 파일이 아니라 사용자 경로 전체에서 찾는다.
호출을 줄였는데 폼이 비면 최적화가 아니라 데이터 주입 경로를 끊은 것이다.