본문 바로가기
FE/React

coustom hook보다 일반 function이 나을 수도 있다.

by ideal_string 2023. 1. 19.

기술과제를 구현하던 중 열심히 만든 코드가 문득 눈에 들어왔다. 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;
};

import 쪽에서 사용한 코드

문제 상황

위 사진처럼 커스텀 훅울 호출했는데 eurInfo가 무한 반복 중이었다. 즉, 계속 리렌더링이 일어나고 있었다. (콘솔로 출력하면 스크롤바가 끊임없이 늘어나는 게 아주 인상적이다.)

 

해결 1 (임시방편)

앞으로 해야할게 많이 남아 있기 때문에 리렌더링이라도 막고자 useCallback을 사용했다.

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 공식 문서에 있는 한 코드에서 힌트를 얻었다.

수정

https://recoiljs.org/ko/docs/guides/testing/

유레카. 정답은 아주 기초에 있었다. 기본값을 페치한 데이터로 넣는 것! 지금까지 필자는 기본 값에 무조건 원시 혹은 참조값(객체, 배열) 그 자체만 넣어야 한다는 고정관념에 사로잡혀 있었던 것. 결국 함수의 반환 값이 객체면 그것도 객체란 사실을 잊고 있었다. 그리고 위 코드를 참고해 수정했다.

(왼쪽)불필요한 커스텀훅을 덜어내고 함수만 남겼다. 파일명도 useGetEurInfo.tsx -> getEurInfo.tsx로 변경. (오른쪽)기본 값을 빈객체에 타입 지정해 둔 것을 지우고, 바로 반환 값이 전역 상태에 담기게 변경했다.
위 코드를 사용하는 viewModel 쪽 코드

코드를 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