옛말에 "백문이 불여일견"이라고 했다.
영상 강의를 보는 것도 좋지만 직접 코드를 작성해보는게 더 빨리 익힐 수 있을것 같아서, 예전에 팀플에서 사용했던 코드를 리팩토링하며 React Query와 Axios를 실습해보기로 했다. React Query가 서버에서 데이터를 가져오는 기능이 있기 때문에, 새싹 수업 3차 팀플에서 만들었던 날씨 위젯 코드를 리팩토링해보았다.👩💻
※ React Query 기본 개념 - https://hjinn0813.tistory.com/126
※ Axios 기본 개념 - https://hjinn0813.tistory.com/127
일단 사용하려면 npm에서 설치부터 해야하는데, React Query와 Axios의 설치 명령어는 아래와 같다.
npm install @tanstack/react-query
npm install axios
설치가 완료되면 본격적으로 React Query, Axios를 사용해볼 순서다.
그 전에, 기존 날씨 위젯의 코드에 대한 설명은 아래 주소에 있다.
https://hjinn0813.tistory.com/59
SeSAC 3차 프로젝트 사용 기술 - 날씨 위젯
현재 SeSAC 교육과정 3차 팀 프로젝트의 마무리 단계여서 해당 프로젝트에서 사용했던 기술들을 정리해보았다.
hjinn0813.tistory.com
UseQuery 사용을 위한 필수 설정
/* root의 QueryClient.js */
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default queryClient;
/* root의 index.js */
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// QueryClient 인스턴스 생성
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
- React Query를 제대로 사용하려면 루트의 index.js나 app.js에서 <QueryClientProvider>로 전체 내용을 감싸야한다.
QueryClient.js 파일 따로 만들고 index.js에서 감싸도 되고, 그냥 바로 index.js에서 감싸도 된다.
나는 별도 파일을 만들고 index.js에서 추가 설정을 했다.
이걸 설정하지 않으면, React Query가 쿼리를 어떻게 관리할지 알 수 없어서 에러가 발생한다.
컴포넌트 초기 설정
import React, { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import '../style/UseQuery.scss';
/* api 설정 */
const WEATHER_API = {
key: process.env.REACT_APP_WEATHER_API_KEY,
base: 'https://api.openweathermap.org/data/2.5/',
};
- 기존 코드에서는 위치 가져오기, 날씨 정보 불러오기, 네이버 날씨 이동 이벤트를 전부 함수 컴포넌트 내부 Return문 상단에 작성했었다. 그러나 이렇게 작성하면 코드의 재사용성이 떨어져서, 수정된 코드에서는 전부 함수 바깥에 두었다.
- 컴포넌트의 초기 설정에서는, UseQuery와 Axios를 사용하기 위해 import했다는 부분 외에는 기존 코드와 같다.
위치 정보 가져오기
const getCurrentLocation = () =>
new Promise((res, rej) => {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
res({ lat: latitude, lon: longitude });
},
(error) => {
rej(error);
}
);
});
- navigator.geolocation.getCurrentPosition 메서드는 위치 정보를 요청한 뒤에, 정보가 준비될 때까지 기다렸다가 결과를 반환한다. React Query가 비동기적으로 서버에서 데이터를 가져오는 라이브러리여서, 위치 정보를 불러오는 함수도 비동기적으로 위치 정보를 반환해야 쿼리가 제대로 작동한다.
- 기존 코드에서는 let 키워드를 사용해서 각각의 변수를 따로 선언했지만, 수정된 코드에서는 latitude와 longitude를 구조분해할당하여 한번에 변수로 가져와서 position.coords에 담는 방식으로 코드를 간결하게 했다. 사실 다른 부분은 모두 수정하고 여기만 기존의 코드로 놔뒀더니 에러가 발생했었는데, 똑같이 비동기적으로 작동하는 함수인데도 여기서 Promise를 사용해야하는 이유가 있었다.
- 기존의 코드에서는 위치 정보를 불러온 뒤에 바로 현재 날씨를 불러오는 getNowWeather() 함수를 호출하도록 되어있었고, 위치를 가져오기 전에 함수부터 실행되니까 결과로 undefined를 반환하여 에러가 발생했다. 쉽게 말해, 이 코드는 '성공'에 대한 설정만 있고 '실패'에 대한 설정은 없었던 셈이다.
- 반면에 수정된 코드에서는 Promise를 통해 성공(res)과 실패(rej)를 모두 처리하니까, 위치 정보가 준비된 후에만 후속 작업을 하므로 에러가 발생하지 않은 것이다.
- 또한 Promise를 사용하면 비동기적인 흐름을 더욱 직관적으로 관리할 수 있어서 코드의 가독성이 높아진다는 장점도 있다.
API 불러오기, 위젯 클릭 이벤트 설정
/* API 불러오기 */
const getNowWeather = async ({ lat, lon }) => {
const url = `${WEATHER_API.base}weather?lat=${lat}&lon=${lon}&appid=${WEATHER_API.key}&units=metric&lang=kr`;
const response = await axios.get(url);
return response.data;
// axios는 자동으로 JSON을 파싱해 주기 때문에 바로 반환
};
/* 위젯 클릭시 네이버 날씨로 이동 */
const goToNaverWeather = () => {
let url = `https://weather.naver.com`;
window.open(url, '_blank'); // 새 창에서 열기
};
- 기존의 코드에서는 fetch()를 사용했기 때문에, 여러 개의 then()이라는 콜백 체인으로 코드를 관리해서 가독성이 떨어졌다.
- 반면에 수정된 코드에서는 async/await를 사용하여 비동기 코드도 쉽게 읽을 수 있고, "const response" 부분 한 줄로 요청과 응답을 처리할 수 있어서 코드가 훨씬 간결해졌다. 또한 axios는 JSON을 자동으로 파싱해주기 때문에 response.data만 반환하면 되지만, fetch()는 response.json()으로 설정해야 한다.
- 위젯 클릭하면 네이버 날씨로 이동되는 함수는 기존과 같다.
컴포넌트 작성
export default function UseQuery() {
const [location, setLocation] = useState(null);
useEffect(() => {
getCurrentLocation()
.then((loc) => setLocation(loc))
.catch((error) => console.error('Location error:', error));
}, []);
// 이하 생략
}
- 기존의 코드에서는 api 불러오면서 set함수로 설정했던 정보들이, 컴포넌트 작성 시작할때 useState로 들어있다. 그리고 useEffect에 위치 가져오는 getCurrentLocation()만 들어있어서, 위치 정보가 준비되지 않았는데 getNowWeather()가 호출되어 에러가 발생할 수 있다. 또한 useEffect의 의존성 배열이 비어있어서, 컴포넌트가 렌더링될 때마다 위치 허용 여부를 묻게 된다.
- 반면에 수정된 코드는 useState로 위치 정보의 상태를 관리하여, 필요 시에만 위치 정보를 업데이트할 수 있다. then과 catch를 통해 비동기적으로 위치 정보를 가져온 후에만 setLocation(loc)으로 상태를 업데이트하고, 에러 관리도 가능하도록 하여 코드의 안정성이 높아졌다.
React Query가 비동기적으로 동작하는 라이브러리니까 여기도 비동기적으로 작동하도록 해야 한다.
useEffect에는 의존성 배열이 비어있어서, 위치 정보를 처음 한번만 요청하도록 하여 불필요한 요청을 방지했다.
React Query로 날씨 정보 불러오기
const { data, error, isLoading } = useQuery({
queryKey: ['weather', location], // 쿼리 키
queryFn: () => getNowWeather(location), // 쿼리 함수
enabled: !!location,
refetchOnWindowFocus: false,
// 사용자가 브라우저를 다시 활성화할 때 데이터를 자동으로 다시 가져오는 기능 OFF
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
React Query로 날씨 정보 불러오는 현재 로직과 대칭되는 부분은, 기존 코드에서 useEffect로 위치 정보를 받아오고 fetch()로 날씨 정보를 가져오는 부분이다. 수정된 코드에서는 useQuery()로 서버에서 날씨 데이터를 비동기적으로 가져오며, 데이터를 자동으로 캐싱하고 리팩토링해 관리한다. React Query 로직을 한 줄씩 살펴보자.
- 먼저 data, error, isLoading를 구조분해할당하여 useQuery에 담았다. useQuery 사용을 위한 가장 기본적인 설정이다.
- queryKey: 캐싱과 상태 관리를 최적화하는 쿼리의 고유한 키. v4부터는 반드시 배열로 들어가도록 규정하고 있다. 그래서 여기서는 쿼리 키 'weather'와 동적으로 변하는 값인 location을 함께 배열에 담았다. 배열을 사용하면 여러 가지 값을 조합해서 고유한 키를 만들 수 있어서 상태 관리와 캐싱이 더 유연해지니까 일반적으로 많이 사용한다. 여기서는 위치 정보(location)가 변경되면 날씨 정보('weather')를 다시 가져온다. 만약 다른 API에서 사용할때 location에 해당하는 값이 없다면 ['weather']의 형식으로 사용한다.
- queryFn: 실제로 API 요청을 보내서 데이터를 가져오는 함수. 여기서는 getNowWeather(location)을 호출하고, 서버 요청을 보내 날씨 데이터를 가져온다.
- enabled: 쿼리를 언제 실행할지 조건 설정. 여기서는 !!location이니까, location이 null이나 undefined가 아닐 때만 true가 되어 쿼리가 동작한다.
- refetchOnWindowFocus: 사용자가 브라우저를 다시 활성화할 때 데이터를 새로 가져오는 옵션. 기본적으로는 true로 되어있지만, 여기서는 false로 설정하여 쓸데없는 재요청을 막았다.
- 그리고 아래 if문의 isLoading과 error에는 각각 로딩중, 에러 시에 화면에 어떤 텍스트를 렌더링할지를 설정했다.
또한 중요하게 기억할 부분은, React Query v5부터는 쿼리 키와 쿼리 함수를 포함하여 모든 훅의 설정을 객체 형태로 전달해야 한다. v4에서는 권장사항이었지만, v5부터는 필수이다.
Return문 설정
return (
<div className="weather-main" onClick={goToNaverWeather}>
<div className="weather-place">{data?.name}</div>
<div className="weather-info">
{data?.weather[0].icon && (
<img
className="weather-icon"
src={`https://openweathermap.org/img/w/${data.weather[0].icon}.png`}
alt="Weather-icon"
/>
)}
<div className="weather">{data?.weather[0].description}</div>
</div>
<div className="temp-now">{Math.round(data?.main.temp)}°C</div>
</div>
);
기존 코드에서 fetch로 데이터 추출하면서 set함수로 설정했던 place나 temp 같은 부분들을, 수정된 코드에서는 Return에서 바로 추출해서 사용하고 있다. Axios로 받아온 data에 '점 접근법'으로 prop을 연결해서 가져왔고, data에는 옵셔널 체이닝으로 data가 undefined일 경우에도 에러가 발생하지 않도록 했다.
여기까지가 fetch()를 React Query와 Axios로 리팩토링해본 코드 설명이었다.
직접 사용해보니 확실히 전보다 가독성이 좋아진 느낌이 들기는 한다.
이걸 실서비스에는 어떻게 사용할 수 있을지 고민을 해봐야겠다.🤔
'💻 Frontend > React' 카테고리의 다른 글
DB에 CRUD가 되는 게시판 만들기 - Frontend (1) | 2024.11.05 |
---|---|
React Query에서 데이터 길이 확인하기 (0) | 2024.11.03 |
Axios 기본 개념 정리 (0) | 2024.10.10 |
React Query 기본 개념 정리 (2) | 2024.10.08 |
React로 구글 애널리틱스 차트 렌더링하기 (0) | 2024.09.13 |