문제 마주하기

5/23/23 Tue

문제 상황

  • 지도 생성 기능 앱을 만드려고 함.
  • 지도 생성, 마크 표시 등 모든 기능들이 현재 유저 위치를 받는 것으로 부터 시작함.
  • but, 현재 유저 위치를 받는 getCurrentPosition 내장 함수는 “비동기적”으로 수행됨.
  • 모든 코드를 App 파일에 순차적으로 때려넣었을 때는 순서대로 진행됐지만, 훅으로 분리하니 getCurrentPosition 함수가 값을 받아오는 동안 벌써 다른 함수들이 실행되는 문제가 발생함.
  • 당연히 값이 없으니 undefined 값으로 실행되고 에러가 남.

고민점

  • 어떻게 훅으로 분리할 때 비동기함수를 동기적으로 처리할 수 있을까?

접근

1: useMap 안의 setState
But, 초기값 { lat: 0, lon: 0 } 이 맨처음에 렌더링 되는 게 지저분해보임. 비동기 함수를 좀 더 깔끔히 다루는 방법이 있을 것 같음.

// useMap.tsx

const useMap = () => {
  const [currentPosition, setCurrentPosition] = useState({ lat: 0, lon: 0 });

  const getUserPosition = async () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    navigator.geolocation.getCurrentPosition((position) => {
	    const lat = position.coords.latitude;
	    const lon = position.coords.longitude;
	
	    setCurrentPosition({ lat, lon });
	  });
  };

  //...
}
// App.tsx
function App() {
  const { currentPosition, getUserPosition, createMap } = useMap();

  if (Object.keys(currentPosition).length !== 0) {
    createMap(currentPosition.lat, currentPosition.lon);
  }

  useEffect(() => {
    getUserPosition();
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 

2. promise, async/await → return new Promise를 통해 해결.

const getUserPosition = () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition((position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;

        resolve({ lat, lon });
      });
    });
  };
function App() {
  const { getUserPosition, createMap, displayMarker } = useMap();

  useEffect(() => {
    getUserPosition().then((res) => {
      const { lat, lon } = res;
      const map = createMap(lat, lon);
      displayMarker(lat, lon, "내 위치", map);
    });
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 


useHttp 훅 만들기

5/24/23 Wed

 

추가적으로, 비동기함수 중 fetch처럼 상태, 에러 등을 관리할 필요가 있다면 상태관리를 위해 훅으로 빼보자.

찾아보니 그 훅은 비슷비슷한 것 같다.

  1. data, status(loading), error : 이 3가지 상태를 관리한다.
  2. useCallback과 useReducer, 그리고 dispatch를 사용한다.

상태를 다루는 방법에는 알다시피 2가지가 있다: useState, useReducer 가 그것이다. 그런데 useHttp훅은 알다시피 다뤄야할 상태가 3가지이다. 따라서 복수 상태를 다루기 더 편리한 useReducer를 사용하는 것이 편할 것이다.(물론 useState를 사용할 수 도 있다.)

2가지 방법으로 모두 훅을 만들어 사용해 보았다.

 

1. useState를 이용한 커스텀훅

// useFetchMoviesUsingUseState.tsx

import { useCallback, useEffect, useState } from "react";

const useFetchMoviesUsingUseState = (url: string) => {
  const [movies, setMovies] = useState([]);
  const [loading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchMoviesHandler = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

      const data = await response.json();

      const transformedMovies = data.results.map((movieData: any) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_data,
        };
      });
      setMovies(transformedMovies);
    } catch (error: any) {
      setError(error.message);
    }

    setIsLoading(false);
  }, []);

  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

  return { movies, loading, error };
};

export default useFetchMoviesUsingUseState;

2. useReducer를 이용한 커스텀훅

// useFetchMoviesUsingUseReducer.tsx

import { useCallback, useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "SEND":
      return {
        data: undefined,
        error: null,
        status: "pending",
      };
    case "SUCCESS":
      return {
        data: action.payload,
        error: null,
        status: "completed",
      };
    case "ERROR":
      return {
        data: undefined,
        error: action.errorMessage,
        status: "completed",
      };
    default:
      return state;
  }
};
const initialState = {
  data: undefined,
  error: null,
  status: "ready",
};

function useFetchMoviesUsingUseReducer(requestFunction) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const loading = state.status === "pending";

  const sendRequest = useCallback(
    async (...requestData) => {
      dispatch({ type: "SEND" });
      try {
        const responseData = await requestFunction(...requestData);
        dispatch({ type: "SUCCESS", payload: responseData });
        return responseData;
      } catch (error) {
        dispatch({ type: "ERROR", errorMessage: error.message });
        return;
      }
    },
    [requestFunction]
  );

  return {
    sendRequest,
    loading,
    ...state,
  };
}

export default useFetchMoviesUsingUseReducer;

확실히 여러가지 상태를 관리할 때에는useState보단 useReducer를 사용하는 편이 가독성이 올라간다고 생각은 하는데, 여전히 작성할 때 코드가 늘어난다는 점에서 (reducer를 따로 작성해줘야 했다) 더 편리한지는 모르겠더라.

 

🔖 학습 참조글
https://itchallenger.tistory.com/258
https://velog.io/@sae1013/Reactcustom-hook으로-httpRequest-구현하기
https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599794#overview

react-query 이용하기

5/27/23 Sat

 

이렇게 서버측 데이터를 불러오고 관리하는 커스텀훅을 만들다보니 하나 알게된 사실: 한 라이브러리를 쓰면 이렇게 커스텀훅을 만들지 않아도 이러한 훅을 쓴 것처럼 data, isLoading 등 상태관리를 쉽게 할 수 있게 된 것을 알게됐다. 그 라이브러리의 이름은 react-query. 그렇게 react-query에 대해서도 공부해보게 되었다.

 

react-query란?

여러 종류의 데이터(state 등) 중에서도 서버에서 받아온 데이터 상태의 관리를 도와주는 라이브러리이다. 이 글은 react-query를 상세히 다루기 위한 글이 아니어서 이에 대한 학습 및 설명글은 따로 뺐다. 아주 간단히 말하자면  처음 사용시 queryProvider로 감싸주기만 하면 그 하위에선 커스텀훅을 사용했던 것처럼 useQuery훅을 통해 어디서든 서버측 데이터와 데이터의 상태를 이용할 수 있었다. 이렇게 위에서 작성했던 모든 커스텀hook과 api함수들이 불필요해지는 편리함을 맛보았다.

 

react-query 사용시 장점

기대했던대로 수많은 보일러플레이트를 없앨 수 있을 뿐 아니라 staleTime, cacheTime등을 활용해 캐싱 기능을 더 수월하게 다룰 수 있다는 점이 인상깊었다. 또 혼자 공부하다보니 놓쳤는데 커스텀훅으로 상태관리를 하면 말그대로 ‘커스텀’이기 때문에 개발자마다 다르게 작성된다. 하지만 react-query를 사용하면 통일된 방식으로 서버측 데이터 관리가 가능해진다. 이렇게 react-query를 공부하고 사용하며 느낀 장점을 정리하면 아래와 같다:

  • 귀찮은 boilerplate x — isLoading 등 상태, 캐싱 기능
  • 팀원들끼리 통일된 방식으로 상태 관리 가능 — 누구는 isLoading, 누구는 pending 변수명을 다르게 사용하는 것을 막을 수 있다.
// index.tsx

const queryClient = new QueryClient();
console.log(queryClient);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
// App.tsx

const { data, status, error } = useQuery(["movies"], fetchMovies, {
    refetchOnWindowFocus: false,
    retry: 0,
    staleTime: 5000,
    cacheTime: Infinity,
    onSuccess: (data) => console.log(data),
    onError: (e) => console.log(e.message),
  });

  let content = <p>No Found Movies.</p>;

  if (status === "success") {
    content = (
      <div>
        {data.results.map((movie, idx) => (
          <div key={idx}>{movie.title}</div>
        ))}
      </div>
    );
  }

  if (status === "loading") {
    content = <p>Loading...</p>;
  }

  if (status === "error") {
    content = <p>{error.message}</p>;
  }

  return <div>{content}</div>;
}

 

🔖 학습 참조글
https://tech.kakaopay.com/post/react-query-1/
https://tanstack.com/query/latest/docs/react/overview?from=reactQueryV3&original=https://tanstack.com/query/v3/docs/overview
https://velog.io/@jay/10-minute-react-query-concept#:~:text=리엑트 쿼리는 데이터,데이터를 다시 불러온다
https://2ham-s.tistory.com/407
https://kyounghwan01.github.io/blog/React/react-query/basic/
https://www.youtube.com/watch?v=novnyCaa7To

 


Sum Up

어찌어찌 하다보니 비동기함수로 시작해, 비동기 함수중에서도 fetch를 집중적으로 다루게 되었다. 나아가 fetch해온 데이터(서버측 데이터)를 프론트측에서 리액트로 관리하는 방법에 대해 찾아보게 된 시간이었다.

 

네이티브(useState/useReducer) 그리고 툴(react-query)

useState+useEffect 혹은 useReducer+useCallback를 사용해 관리할 수 있다.(이경우 훅을 따로 분리해 관리하면 깔끔하다.) 그러나 이 경우 수많은 보일러 플레이트가 필요하고, 특히 캐싱에 관련해서도 수많은 작업을 진행해줘야 한다. 또한 규명된 상태관리 폼이 존재하지 않아 팀원들끼리 공통된 상태관리가 힘들 수 있다.(다른 상태명, 로직….) 이러한 점을 보다 쉽게 해주는 것이 라이브러리겠지. redux의 saga 미들웨어와 mobx의 __을 이용할 수 있겠지만, 이들은 서버상태 전용 라이브러리가 아니다. 서버 상태 전용 라이브러리 react-query는 보일러 플레이트를 줄이며, 캐싱, 클라이언트 측과 서버 측 상태의 동기화에 특화된 인터페이스를 제공해 서버측에서 넘어온 데이터 관리를 간단하게 한다. 또한 공통된 서버 상태 관리로 팀작업을 원활히 한다.

 

마무리하며&다음에는...

fetch를 가지고 작업하다 서버측 데이터 상태관리까지 넘어가게 되었는데 많은 사람들이 fetch 대신 axios를 사용하고 있었다. fetch 에서 넘어오는 데이터의 에러 처리를 간단하게 해준다는 장점이 있어서 쓴다고 하는데, 한 번 가볍게 다뤄보는 것도 좋겠다.

+ Recent posts