Frontend : React Native/Axios

[React Native] React Query (1) 기본기

0BigLife 2022. 2. 11. 20:29
728x90

 이번 게시글에서는 리액트 쿼리에 대해 정리해보려고 한다. 개념을 정리함에 있어서 이 기능이 어떤 의미와 역할을 가지는지 파악하는 과정은 중요하며, 이는 내가 구현하는 코드에 어떤 이로운 영향을 미치는지 알아내는 중요한 과정이다.

 우선, 리액트 쿼리(React Query)는 API 연동에 특화된 라이브러리로서 Hook을 기반으로 데이터 로딩을 편하게 구현하도록 도와준다. 기존에 우리가 데이터를 넣어주거나 로딩 indicator를 띄워줄 때 또는 모달을 생성해줄 때 다음과 같이 useState를 써서 구현해주었다. 

const Example = () => {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const fetchData = () => {
    setLoading(true);
    try {
      const posts = await getPosts();
      setData(posts);
    } catch (e) {
      setError(e);
    } finally {
      setLoading(false);
    }
  };

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

  // ...
};

 이러한 과정도 사실 어려운 과정을 아니지만 요청하는 API의 수가 많아질수록 그 작업은 더욱 복잡하고 귀찮아질 것이고, 컴포넌트 내부에 선언된 API 이기 때문에 사용자가 다른 화면으로 넘어가면서 해당 컴포넌트가 사라지면 지정된 상태 역시 사라진다. 따라서 나중에 다시 컴포넌트를 다시 생성시킬 때 기존에 받아온 응답 결과를 사용하지 못하고 새로운 API를 재요청해야 한다. 즉, 한 마디로 캐싱이 불가능하다는 것이다.(=저장되지 않는다.)

 캐싱이 필요하다면 데이터를 Context, 리덕스, 리코일과 같은 라이브러리로 관리해야 하지만 그렇게 할 시에는 코드가 복잡해진다. 따라서, 리액트 쿼리를 사용한다면 컴포넌트에서 Hook을 기반으로 데이터 로딩을 효율적으로 관리할 수 있고, 캐싱도 제공하기 때문에 여러 이점을 취할 수 있다!

useQuery Provider 적용

 useQuery를 적용하기 앞서 리액트 쿼리에서 캐시를 관리할 때 사용하는 QueryClient 인스턴스를 자식 컴포넌트에서 사용할 수 있도록 Provider 세팅을 해줘야 한다.
 Provider의 위계가 없기 때문에 우선 ThemeProvider를 감싸도록 하였고 내부 인자로 QueryClient를 넣어주었다.

import {QueryClient, QueryClientProvider} from 'react-query';

const queryClient = new QueryClient();

const App = () => {
  return (
    <QueryClientProvider client={queryClient}> //Provider Setting
      <ThemeProvider theme={theme === 'dark' ? dark : light}> //ThemeProvider Setting
        <RootNavigation />
      </ThemeProvider>
    </QueryClientProvider>
  );
};

export default App;



useQuery의 반환값


 현재 홈뷰에서 서버로 뉴스피드 데이터를 .get하는 방식을 useQuery로 구현하려는 것이기 때문에 기존에 구현해놓은 코드를 밀고 새로 작성했다. useQuery는 첫 번째 인자로는 조회하려는 데이터의 키 값을 받고, 만약 이미 존재하는 데이터를 조회하려고 할 시 기존 데이터를 바로 보여준다. 두 번째 인자에는 Promise를 반환하는 함수를 받는다. 예를 들어, 다음과 같다.
 (Promise는 자바스크립트에서 비동기적 작업을 편하게 관리하도록 도와주는 객체이다. Promise와 동기/비동기에 대한 내용은 추후 정리해서 올릴 예정!)

const Example = () => {
  const result = useQuery('posts', postData);
  const { data, error, isLoading } = result;

  if (isLoading) return <Component>...</Component>;
  if (error) return <Component>...</Component>;

  return <View>{ ...Data... }</View>
}


  이때, useQuery Hook을 사용하여 반환된 result 객체는 다음과 같은 값을 가지고 있다.

  • status : API의 요청 상태를 문자열로 나타낸다.
    - isLoading : 아직 데이터를 받아오지 않은 상태이며, 현재 데이터 요청 중인 상태
    - isError : 오류 발생
    - isSuccess : 데이터 요청 성공
    - idle : 비활성화된 상태(따로 설정하여 비활성화된 상태인 경우)
    - data : 요청 성공한 데이터
    - refetch : 다시 요청을 시작하는 함수

만약에 요청 함수를 호출하는 상황에서 파라미터를 넘겨줘야 한다면 데이터 키 값과 함께 배열을 선언하고(이때 배열 키 값의 순서는 위계가 없다) 두 번째 인자로 들어갈 함수는 화살표 함수로 선언해준다.

const result = useQuery(['posts', id], () => postData(id, name));
//객체 키값 순서 무관
const result = useQuery(['posts', {id: 1, age: 23}], () => ...);
const result = useQuery(['posts', {age: 23, id: 1}], () => ...);

 

useQuery의 Options

 useQuery의 세 번째  인자에 options 객체를 넣어 Hook의 작동 방식을 설정할 수 있다.

const Example = () => {
  const result = useQuery('posts', postData, { //Options
    enabled: true,
    refetchOnMount: true,
  });
);
// Options: enabled, retry, staleTime, cacheTime, onSuccess, onError...

 

useQuery 적용

 우선 홈뷰에서 게시글을 받아오기 위한 API 요청을 담당하는 getArticles 함수를 따로 선언을 해주었다. 이 함수는 useQuery의 두 번째 인자로 들어가야 하기 때문에 Promise 반환을 위하여 async, await을 사용하였다. 요청한 데이터 응답을 위하여 Generic에는 Feed[]를 넣어주었다. 

//Data Generic
export interface Feed {
  id: number;
  name: string;
  title: string;
  url: string;
  thumbnailUrl: string;
  postTime: string;
}

 
위에서 언급하였듯, 반환값으로 data와 isLoading을 주고 FlatList의 data에 반환된 data를 넣어주었다. renderItem에는 받아온 값을 따로 만들어놓은 <PostCard> 컴포넌트 안의 파라미터에 적합하도록 넣어주었다. 

export interface HomeProps {
  navigation: StackNavigationProp<HomeParamsList, 'HomeView'>;
}

const getArticles = async () => {
  const response = await apiClient.get<Feed[]>('/feeds');
  return response.data;
};

const HomeView: React.FC<HomeProps> = ({navigation}) => {
  const {data, isLoading} = useQuery('articles', getArticles);
  const [refresh, setRefresh] = useState<boolean>(false);

  console.log({data, isLoading});

  if (!data) {
    return <ActivityIndicator size="large" style={{flex: 1}} />;
  }

  const wait = (timeout: number) => {
    return new Promise(resolve => setTimeout(resolve, timeout));
  };

  const refreshing = () => {
    setRefresh(true);
    wait(1400).then(() => setRefresh(false));
    getArticles();
  };

  return (
    <SafeContainer>
      <FlatList
        data={data}
        renderItem={({item}) => (
          <PostCard
            id={item.id}
            name={item.name}
            title={item.title}
            postTime={item.postTime}
            url={item.url}
            thumbnailUrl={item.thumbnailUrl}
            // navigation={navigation}
          />
        )}
        keyExtractor={item => item.id.toString()}
        showsVerticalScrollIndicator={false}
        onRefresh={() => refreshing()}
        refreshing={refresh}
      />
    </SafeContainer>
  );
};

export default HomeView;

 

그 결과, 전에 구현한 상태와 동일하게 나온 것을 확인 !

 

728x90