Frontend : React Native/Axios

[React Native] React Query (2) useQuery 적용 (+Axios 구조화)

0BigLife 2022. 5. 19. 19:59
728x90

퐁퐁

 너무너무 오랜만에 글이다.. 최근에는 졸업 전시 관련 하드웨어 쪽 코드를 다루고 이력서 작성에 정신이 팔려서 개발의 비중이 조금 줄어들었다. 이력서를 작성하면서 느낀 것 중에서 클린 코드에 대해 다룬 것이 있다. 서칭을 하면서 찾아보는데 클린 코드를 위한 가장 기본적인 참고사항은 적용을 마쳤다. 

  • 변수명(네이밍 규칙) 명시적으로 변경
  • 삼항연산자의 상황에 따른 대응 ( + 논리 연산자(&&)로 조건부 렌더링 적용 )
  • True(boolean) 값을 Props로 넘길시 생략
  • useEffect Hook 내부 함수는 밖으로 뺄 것
  • 인자가 Event 뿐인 이벤트 핸들러 함수는 함수명만 명시
  • React-Query와 Axios 모듈화로 API 요청 관리 (진행중)

 정도가 될 것 같은데 마지막 항목은 현재 작업 중이다. 현재는 검색 기능에서 조금 말썽을 부리는 중인데 우선 홈뷰와 맵뷰에서 unsplash API 로 데이터를 불러올 때 어떤 방식으로 .get 요청을 완료했는지를 정리해보려고 한다. 그리고 마지막에서는 react-query를 적용하기 전과 후의 코드 모두 비교해볼 예정이다.

api 폴더 구조

 React-Query 를 적용하기 전에 Axios 관련 폴더와 네이밍 정리를 먼저 했다. 일단 한눈에 봐도 좀 지저분하다. 가져와서 쓰고픈 API들이 있는데 서로 역할이 다르다보니 이런 식으로 짜여졌다. (나중에 진행할 프로젝트는 백엔드가 마련되어 있기 때문에 간단해질지도?) 

 Firebase : 유저 프로필 관리를 위해서 적용했다. 유저가 프로필 편집할 시 이미지 전송을 위해 firebase-storage가 필요했고, 유저가 이미지와 텍스트 등의 데이터를 게시글로 업로드하기 위해서는 해당 데이터 구조가 마련되어 있는 서버가 필요하여 firestore를 사용했다.
 MarkerAPI : 이 API는 지금 만들고 있는 앱 이전에 강의를 구입해서 듣던 프로젝트에서 강사분께서 직접 작성해서 공유해준 서버이다. 이 API에서는 로그인, 회원가입에 대한 요청을 보내면 유저 정보를 저장해주며, 일정 시간마다 지도 위에 마커를 뿌려준다. 강좌에서는 배달앱을 위하여 사용자들이 배달기사에게 요청을 보내는 마커를 필자는 생존자의 위치 표시에 사용했다.
 SampleData : 이 폴더는 조만간 지울 폴더이다. Axios 구현 테스트를 하면서 API 요청을 보내기 전에 데이터를 내장된 폴더에서 가져와서 화면에 뿌려주는 용도로 작성되었으며, json 형태로 유저 정보, 게시글을 담당한다. 
 UnsplashAPI : 독학하면서 가장 편하게 사용하고 있는 API이다. 이미지를 요청 파라미터에 count 변수에 원하는 만큼, 원하는 키워드로 요청해서 가져올 수도 있고, Unsplash 유저의 정보도 가져올 수 있어 정말 감사하게 생각하고 있는 API이다. 현재 내 프로젝트에서는 홈뷰의 게시글유저 데이터, 맵뷰에서의 마커에 표시할 유저 정보, 채팅 카테고리의 유저 리스트가 이 API로 적용된 상태이다.
 WeatherAPI : 맵뷰에서 사용자에게 날씨 정보를 제공해주기 위해 가져온 API이다. 날씨 정보를 날짜별로 가져오면 좋았을텐데 오늘에 대한 데이터만 제공해주는 부분은 조금 아쉬웠다. 

 이제 어떤 방식으로 axios를 정리했는지 정리해본당. 크게 client, service, type으로 분류를 했다. client.tsx는 가장 기본적인 인스턴스 생성을 위하여 만들었고 내부에는 url, header처리를 포함시켜다. service.tsx는 나중에 프로젝트 크기가 커지면서 api 호출이 많아졌을 때 좀 더 분기처리가 필요하겠지만, 일단은 service.tsx 하나에 다 포함시킬 생각이며 내부에는 사진 요청에 대한 api, 유저 정보 요청에 대한 api 함수를 넣어둔 상태이다. 마지막으로 type.ts는 현재 TypeScript를 적용했기 때문에 불러온 데이터에 대한 타입을 정리해주기 위한 용도로 작성되었다. 순서대로 코드는 아래와 같다!

// : UnsplashAPI/client.tsx

import axios from 'axios';
import Config from 'react-native-config';

const client = axios.create({
  baseURL: 'https://api.unsplash.com',
  params: {
    client_id: `${Config.UNSPLASH_ACCESSTOKEN}`,
  },
});

export default client;
// : UnsplashAPI/service.tsx

import client from './client';
import {randomPhotoState, searchUserState} from './type';

export const getPhoto = async () => {
  const response = await client.get<randomPhotoState[]>('/photos/random', {
    params: {
      count: 3,
      query: 'war',
    },
  });
  return response.data;
};

export const getUser = async () => {
  const response = await client.get<randomPhotoState[]>('photos/random', {
    params: {
      count: 12,
    },
  });
  return response.data;
};

export const searchUser = async (text: string) => {
  const response = await client.get<searchUserState>('/search/users?', {
    params: {
      query: text,
    },
  });
  return response.data.results;
};
// : UnsplashAPI/type.ts

export type randomPhotoState = {
  alt_description: string;
  blur_hash: string;
  color: string;
  created_at: string;
  description: string;
  downloads: number;
  id: string;
  liked_by_user: boolean;
  likes: number;
  links: {
    // 생략
  };
  location: {
    // 생략
  };
  promoted_at: string;
  sponsorship: string;
  updated_at: string;
  urls: {
    // 생략
  };
  user: {
    // 생략
    profile_image: {
    // 생략
    };
    username: string;
  };
  view: number;
  width: number;
  height: number;
};

export type searchUserState = {
  total: number;
  total_pages: number;
  results: [searchUserResults];
};

export type searchUserResults = {
  id: string;
  updated_at: string;
  username: string;
  name: string;
  first_name: string;
  last_name: string;
  bio: string;
  location: string;
  links: {
    // 생략
  };
  profile_image: {
    // 생략
  };
  social: {
    instagram_uesrname: string;
  };
  photos: [
    {
    // 생략
      urls: {
    // 생략
      };
    },
  ];
};

 이젠 홈뷰에서 게시글 정보들을 unsplash api 를 통해 가져와서 react-query에서 제공해주는 useQuery훅을 통하여 화면에 뿌려줄 것이다. (기본 문법에 대한 링크는 여기!) 데이터를 조회하기 위해서는 useQuery 훅을 쓰고, 갱신하기 위해서는 useMutation 을 사용하는데 여기서는 전자만 사용한다.
 가장 먼저 useQuery 첫 번째 인자로는 키 값, 두 번째 인자로는 Promise 반환 함수를 넣어줄 것인데, Axios 구조를 정리한 덕에 두 번째 인자는 미리 정리된 함수만 불러와서 넣어준다. 

const {data, isLoading} = useQuery('homePost', getPhoto);

 반환값은 실제 가공할 data와 data가 들어오지 않았을 때 로직처리를 위한 isLoading 값을 받아서 다음과 같이 처리했다. data가 없는 동안 ActivityIndicator를 넣어주고 data가 있을 때에는 콘솔에 Flatlist의 renderItem에 넣어줄 데이터를 넣어주었다.

if (!data) {
  console.log('data uploading : ', isLoading);
  return (
    <ActivityIndicator
      style={{
        flex: 1,
        justifyContent: 'center',
        alignContent: 'center',
      }}
      size="small"
      color="gray"
    />
  );
} else {
  console.log('Home useQuery Data : ', data);
}

 이제 이 data를 원하는 형태로 가공해서 화면에 뿌려주면 된다! 구현 자체는 생각보다 너무 간단했다. 아래 Axios를 직접 불러서 사용했을 때보다는 전체 코드 로직에 대한 가독성이 뛰어나다. useState를 통해 데이터를 받고 데이터가 없을 때의 상태 관리도 try-catch 문 안에서 잡아줬어야했는데 이 과정도 축약되고 한 눈에 보기 굉장히 편해졌다. 추가적으로, useQuery의 isError, isFetching을 통해 추가 로직 생성도 가능하고, Options을 사용해서 요청에 대한 자세하게 다룰 수도 있는데 아직 이 단계는 스터디 및 적용되지 않은 상태이다. 

전체 코드 

// React-Query 적용 전 !

const HomeFeed = () => {
  const [loading,setLoading] = useState<boolean>(false);
  const [data,setData] = useState();

  const getUser = async () => {
      try {
        setLoading(true);
        const response = await client.get('/photos/random', {
          params: {
            count: 1,
            query: 'war',
            client_id: `${Config.UNSPLASH_ACCESSTOKEN}`,
          },
        });
        setData(response.data);
        console.log('HomeFeed Unsplash Data : ', response.data);
      } catch (e) {
        console.log('HomeFeed/getUser Error : ', e);
      } finally {
        setLoading(false);
      }
    };
    
    useEffect(() => {
      getUser();
    }, []);
   
   // 생략
   
 }
 
 export default HomeFeed;
// React-Query 적용 후 !

import {useQuery} from 'react-query';

const HomeFeed = () => {
  const navigation =
    useNavigation<NativeStackNavigationProp<HomeFeedStackParamList>>();
    
  const {data, isLoading} = useQuery('homePost', getPhoto);
  
  if (!data) {
    console.log('data uploading : ', isLoading);
    return (
      <ActivityIndicator
        style={{
          flex: 1,
          justifyContent: 'center',
          alignContent: 'center',
        }}
        size="small"
        color="gray"
      />
    );
  } else {
    console.log('HomeFeed useQuery Data : ', data);
  }
  
  const renderItem = ({item}: {item: randomPhotoState}) => {
    return (
      <CellContainer
        style={{
          height: item.user.bio ? 390 : 320,
          shadowColor: 'black',
          shadowOpacity: 0.2,
          shadowRadius: 3,
          shadowOffset: {width: 3, height: 3},
        }}>
        <HeaderSection>
          <ProfileView
            activeOpacity={0.2}
            onPress={() =>
              navigation.navigate('UserProfile', {
                id: item.id,
                user_name: item.user.name,
                user_location: item.user.location,
                user_profile: item.user.profile_image.large,
              })
            }>
            <ProfileImage source={{uri: item.user.profile_image.large}} />
          </ProfileView>
          <InfoView>
            <Name>{item.user.name}</Name>
            <Location>{item.user.location}</Location>
          </InfoView>
        </HeaderSection>
        <ImageSection
          style={{
            borderBottomLeftRadius: item.user.bio ? 0 : 10,
            borderBottomRightRadius: item.user.bio ? 0 : 10,
          }}
          source={{uri: item.urls.full}}
        />
        <BodySection>
          <BodyText numberOfLines={3}>{item.user.bio}</BodyText>
        </BodySection>
      </CellContainer>
    );
  };
  return (
    <MainContainer>
      <SearchBar/>
      <IonIcon
        // 생략
      />
      <FlatList
        data={data}
        renderItem={renderItem}
        keyExtractor={item => item.id}
        showsVerticalScrollIndicator={false}
      />
    </MainContainer>
  );
};

export default HomeFeed;
728x90