기술과제를 구현하던 중 열심히 만든 코드가 문득 눈에 들어왔다. api를 호출해 결과 값을 전역 상태에 넣는 커스텀 훅이었다. 생각이 드는 대로 코딩하다 보니, 그리고 동작하는 데 주안을 두다 보니 처음엔 알아차리지 못했다. 아래 사진이 그 코드다.
// useGetEurInfo.tsx
import { useCallback, useEffect } from "react";
import { useRecoilState } from "recoil";
import { EurInfoAtom } from "../store/eurInfo";
export const useGetEurInfo = () => {
const [eurInfo, setEurInfo] = useRecoilState(EurInfoAtom);
const getEurInfo = async () => {
const krweur = await fetch(
"https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWEUR"
)
.then((response) => response.json())
.then((array) => array[0]);
setEurInfo(krweur);
};
useEffect(() => {
getEurInfo();
}, [getEurInfo]);
return eurInfo;
};
문제 상황
위 사진처럼 커스텀 훅울 호출했는데 eurInfo가 무한 반복 중이었다. 즉, 계속 리렌더링이 일어나고 있었다. (콘솔로 출력하면 스크롤바가 끊임없이 늘어나는 게 아주 인상적이다.)
해결 1 (임시방편)
앞으로 해야할게 많이 남아 있기 때문에 리렌더링이라도 막고자 useCallback을 사용했다.
해결 2
접근
다른 작업을 끝내고 이 부분으로 돌아왔다. 이 코드를 살펴보면 총 3단계로 이뤄져 있었다.
1. 브라우징이 끝나면 getEurInfo()를 실행한다.
2. api에서 값을 가져와서 setEurInfo에 담는다.
3. eurInfo를 반환한다.
저 값을 사용하기 위해 useEffect부터 시작해 api 통신 그리고 전역 상태 영역까지 갔다가 그 값을 반환한다. useState로도 충분할 텐데 굳이 recoil을 쓴 것도 애매하다고 느껴졌다. 문제는 저 3단계가 의존성 배열에 묶여 계속 리렌더링이 된다는 것. MVVM 패턴을 적용하고자 model 영역에 값을 넣기 위해서긴 했지만, 비효율이라고 느꼈다. 아래 코드는 recoil store다.
// store/eurInfo.ts
import { atom, selector } from "recoil";
import { KrwFormatter } from "../lib/utils";
import { IEurInfo } from "../types/eurInfo";
export const EurInfoAtom = atom<IEurInfo>({
key: "EurInfoAtom",
default: {} as IEurInfo,
});
export const EurInfoSelector = selector({
key: "EurInfoSelector",
get: ({ get }) => {
const eurInfo = get(EurInfoAtom);
const exchangeEurToKrw = (eur: number) =>
KrwFormatter(eur * eurInfo.basePrice);
return { exchangeEurToKrw };
},
});
MVVM 패턴에서 model 역할인 이 파일은 API 값을 전역 상태에서 보관하고, selector를 통해 특정 함수를 불러 동작하도록 만들었다. 위 캡처 코드와 위 코드를 비교하면서 봐도 얘를 model로 써야 할 이유가 보이지 않았다. seletor 부분도 viewModel 쪽에서 일반 함수로 써도 무방 할 테니까. MVVM 패턴을 유지하고, 리렌더링을 막고 싶은 방법을 여러 검색으로 찾았으나 알맞은 방법이 없었다. 그러다 우연히 jest 관련 사항을 공부하다가 recoil 공식 문서에 있는 한 코드에서 힌트를 얻었다.
수정
유레카. 정답은 아주 기초에 있었다. 기본값을 페치한 데이터로 넣는 것! 지금까지 필자는 기본 값에 무조건 원시 혹은 참조값(객체, 배열) 그 자체만 넣어야 한다는 고정관념에 사로잡혀 있었던 것. 결국 함수의 반환 값이 객체면 그것도 객체란 사실을 잊고 있었다. 그리고 위 코드를 참고해 수정했다.
코드를 viewModel에 적용시켰더니, 무한 리렌더링이 사라졌다. 웹도 문제 없이 잘 동작한다.
1. recoil이 동작할 때 getEurInfo()를 호출한다.
2. getEurInfo()가 api를 호출하고 결과값을 반환한다.
코드가 확 줄었다. 22줄에서 6줄. 단순 동작도 한 단계 줄었지만, 이 단계를 반복하지 않는 다는 게 중요하다. 불필요한 setState코드나 눈 가리고 아웅한 useCallback 같이 임시방편 코드도 없어지니 보기도 좋다. 이 맛에 코딩하나 싶을 정도로 기분이 좋다.
(다만, recoil이 동작하기전 기본 값이 없을 수 있으니 suspense 등을 이용해 임시 페이지를 만들어 두어야 한다.)
마무리
api를 커스텀 훅으로 만들 때 useEffect를 아무 생각 없이 넣으면 안 될 듯하다. 의존성 배열에 무엇을 넣느냐에 따라 리렌더링 지옥에 빠지기 쉬운 듯하다. 커스텀 훅 연습하겠다고 무턱대고 만든 필자 잘못도 있다. 커스텀 훅을 만들기 전에 이게 최선인지 한 번쯤 브레이크를 밟아야 하나... 그럴 시간이 없다면, 리팩터링 할 때 커스텀훅을 한 번 살펴봐야 할 듯하다. 이 상황을 겪고 보니 해결했다는 것에 뿌듯하면서도 아직도 가야 할 길이 멀다고 느껴진다.
'FE > React' 카테고리의 다른 글
useForm handleSubmit이 동작하지 않는다. (0) | 2022.12.12 |
---|---|
memoization(feat. useMemo, useCallback) (0) | 2022.10.26 |
Curring (0) | 2022.10.26 |
Optimistic-UI (0) | 2022.10.25 |
Shallow Routing (0) | 2022.10.25 |