문제 마주하기
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처럼 상태, 에러 등을 관리할 필요가 있다면 상태관리를 위해 훅으로 빼보자.
찾아보니 그 훅은 비슷비슷한 것 같다.
- data, status(loading), error : 이 3가지 상태를 관리한다.
- 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 에서 넘어오는 데이터의 에러 처리를 간단하게 해준다는 장점이 있어서 쓴다고 하는데, 한 번 가볍게 다뤄보는 것도 좋겠다.
'Programming > 웹 프론트엔드' 카테고리의 다른 글
[React] 리액트에서의 상태 관리, 무엇이고 어떻게 해야할까 (0) | 2024.07.10 |
---|---|
[JavaScript] 문제로 이해하는 이벤트 루프(Event Loop)의 개념 (feat. JavaScript에서 비동기를 제어하는 4가지 방법) (0) | 2023.03.20 |
[React] 컴포넌트 분리, 어떻게 해야할까? (0) | 2023.03.19 |