본문 바로가기

개발이야기

Why useEffect is a bad place to make API calls

아래 글을 번역한 글 입니다.

 

Why useEffect is a bad place to make API calls

The design choices react team have made in useEffect hook are still a heated debate. Some people like it and some don’t.

articles.wesionary.team

 

React 팀이 useEffect hook에서 선택한 디자인 선택은 여전히 열띤 토론입니다. 어떤 사람들은 그것을 좋아하고 어떤 사람들은 그렇지 않습니다.

 

여러분이 React 세계에서 오지 않았다면 기본 동작이 두려운 무한렌더 루프이기 때문에  확실히 이상하게 들릴 것입니다. (의존성 배열에 아무것도 없으면 매 렌더마다 실행)

예를 들어 아래 코드는 괜찮아 보입니다.

 

useEffect(()=>{console.log("Hello World")})

 

이것은 모든 렌더링에서 "Hello World"를 프린트할 것입니다. 저는 이 코드를 아래와 같이 바꾸고 싶습니다.

 

useEffect(()=>{console.log("Hello World")},[])

 

우리는 위의 의존성 배열을 항상 확인해줘야합니다. 이게 다이죠. 우리는 문제를 해결했고 위의 코드는 한번만 실행되어야 합니다. 맞나요? 기술적으로 항상 그런것만은 아닙니다.

 

The Strict mode problem

개발 모드에서 react strict 모드의 이점을 이용하면 이는 실제로 두번 호출됩니다.

이것이 왜 문제인지 아래 코드를 통해 알 수 있습니다. 

 

useEffect(()=>{
  api.post("/view",{})
},[])

 

사용자가 위의 useEffect에서와 같이 페이지를 보았다는 이벤트를 보내는 코드가 있다고 가정해보겠습니다. strict mode에서는 이 요청을 두번 실행하고 이중 이벤트를 보냅니다. 아래와 같이 ref를 사용해 해결할 수는 있겠죠.

 

export default function useEffectOnce(fn: () => void) {
  const ref = useRef(false);
  useEffect(() => {
    if (ref.current) {
      fn();
    }
    return () => {
      ref.current = true;
    };
  }, [fn]);
}

 

그러나 본질적으로 이것은 문제이며 우아한 수정은 아닙니다.

 

The performance problem

공식 문서에 따르면 useEffect는 컴포넌트의 렌더링이 완료된 후 실행됩니다. 따라서 API 호출을 넣으면 다음과 같이 UI의 완전한 렌더링이 완료된 후 API 호출이 시작됩니다.

 

 

반응이 빠른 렌더링이지만 UI가 항상 약간의 시간을 소비하고 렌더링 시작시 실행될 수 있는 API 호출을 지연시키기 때문에 이것은 최적이아닙니다. 따라서 더 나은 접근 방식은 데이터와 렌더링을 하는 것을 동시하는 것 입니다. (병렬)

 

어떻게 이를 달성할 수 있을까요?

 

React Query to the rescue

React 쿼리는 우리가 논의한 것과 정확히 일치합니다. useQuery와 같은 훅 렌더링이 시작되자마자 데이터를 가져오므로  다음과 같이 전체 구성 요소를 로드할 때까지 기다릴 필요가 없습니다.

 

 

// with react query
const { status, data, error, isFetching } = useQuery(
  ['data'],
  async () => {
    const data = await (
      await fetch(`${API_BASE_URL}/data`)
    ).json()
    return data
  }
)
// without react query
useEffect(() => {
  try {
    setLoading(true)(async () => {
      const data = await (await fetch(`${API_BASE_URL}/data`)).json();
      setData(data);
    })();
  } catch (error) {
    setError(error);
  } finally {
    setLoading(false);
  }
}, []);

 

구문을 보면 리액트 쿼리 사용시 쿼리를 바로 실행할 뿐아니라 로딩상태, 에러 상태 및 실제 데이터와 같은 많은 것을 처리한다는 것을 알 수 있습니다. 더 나아가서 쿼리를 다시 실행/무효화하는 것은 다음과 같이 간단합니다.

 

queryClient.invalidateQueries(['data'])

 

이 문제를 해결하는 또 다른 방법은 SSR을 수행하여 데이터가  이미 백엔드에서 렌더링되거나 리액트 라우터 로더와 같은 기능을 사용하는 것입니다.

 

 

loader (dev branch)

loader Each route can define a "loader" function to provide data to the route element before it renders. This feature only works if using a data router, see Picking a Router createBrowserRouter([ { element: , path: "teams", loader: async () => { return fak

reactrouter.com

 

Conclusion

useEffect 내부에서 API 호출은 오류가 발생하기 쉽거나 매우 느릴 수 있습니다. 따라서 꼭 필요한 경우가 아니면 피하는 것이 좋습니다.