React Query 도입기

김윤진·2022년 10월 23일
2

우아한테크코스

목록 보기
5/8

레벨로그 프로젝트

레벨로그 프로젝트에서는 어떠한 기술 도입에 있어 필요하다고 생각이 들고 타당한 이유가 존재해야 도입하기로 팀원과 상의했다. 그리고 프로젝트가 상당부분 진행되고 나서 필요하다는 생각이 강하게 들어서 리액트 쿼리를 도입하였다.
리액트 쿼리를 도입하기 전에 잘 알려져있는 데이터 패칭 라이브러리인 SWR과 React Query 중에 어떤 데이터 패칭 라이브러리를 도입하는 것이 좋을까? 라는 고민을 하게 되었고 이에 대해 팀원과 토론도 나누었다.

일단 두 라이브러리 모두 HTTP 캐시 무효 전략인 stale-while-revalidate 를 기반으로 동작한다. 캐시되어 있는 데이터를 반환하고 재검증하는 개념이다.

  • SWR

    SWR의 추구하는 방향은 일단 화면을 먼저 캐싱되어있는 데이터로 보여준다. 그리고 API 요청을 보내(재검증) 응답값이 캐싱되어있는 값과 다르다면 변경된 데이터에 대한 UI만 바꿔주는 방식이다. 화면을 최대한 빠르게 보여주는 것이 중요하다고 생각하는 것이다.

  • React Query

    기존의 상태관리 라이브러리들은 클라이언트단의 상태를 관리에 초점이 잡혀있다. 이는 서버 상태와 클라이언트 상태가 다를 수 있다는 단점이 있다. 그래서 리액트 쿼리에서 추구하는 방향은 서버의 데이터 상태와 클라이언트의 데이터 상태를 동일하게 유지시켜주는 것이다.


SWR 대신 React Query를 도입을 결정한 가장 큰 요인이 존재하는 페이지이다.

여기서 우리는 사용자 관점에 생각을 해봐야한다.
화면에서 보이는 인터뷰 그룹에 속해 있는 크루라고 생각을 해보자. 모의 인터뷰를 진행하기 전 상황에서 보자면 모의 인터뷰를 시작하기 전에 레벨로그(인터뷰때 받고 싶은 질문)를 작성하지 않는다면 화면과 같이 버튼들이 disabled 된 상태를 확인할 수 있다. 그리고 레벨로그가 작성이 되어 있다면 버튼들은 active한 상태로 화면에 보여진다.

  • SWR을 사용한다면

    인터뷰에 참여하는 크루가 레벨로그를 작성했더라도 처음으로 보여지는 화면에서는 disabled된 버튼들을 화면에서 확인할 수 있다. 요즘은 하드웨어나 네트워크가 상당히 좋기 때문에 찰나의 순간에 active한 버튼들로 바뀐다. (그리고 이 서비스를 사용하는 주 사용자들을 살펴보면 네트워크 속도가 상당히 빠른 한국에서 서비스를 사용하고 우아한테크코스 크루이기 때문에 하드웨어 또한 일반 사용자에 비해 상당히 좋다.)
    이게 어떻게 보면 정말 사소한 화면 변화일 수도 있지만 사용자 관점에서 보면 다른 크루가 레벨로그를 작성하지 않았다고 첫 화면에서 인지하고 현재 페이지를 이탈할 수도 있다는 것을 생각해봐야 한다. 이것은 사용자에게 혼란을 줄 수 있는 요인이다.
    참고로 SWR에서 캐싱되어 있는 데이터를 첫화면 대신에 API 응답을 화면에 보여줄 수 있는 속성도 존재한다. 그렇다면 SWR에서 추구하는 캐싱되어 있는 데이터를 화면에 보여주고 재검증을 통해 변경된 데이터가 있다면 변경한다. 라는 방향성을 잃어버린채 라이브러리를 사용하는 것이다. 그렇기에 SWR은 레벨로그 서비스와 맞지 않다고 생각해서 도입하지 않았다.
    이는 서비스에 SWR을 빠르게 도입해보고 나온 결과물이다. 어떠한 기술이 추구하는 방향을 인지하고 빠른 도입과 빠른 결과물을 보는 것이 내가 추구하고자 하는 웹 서비스 개발이기 때문이다.

  • React Query를 사용한다면

    리액트쿼리는 화면에 보이는 페이지에 들어오면 queryClient나 useQuery에 옵션으로 cacaheTime, staleTime을 설정하여 캐시되어 있는 데이터를 화면에 보여주거나 캐시가 만료되었다면 API 요청을 통해 최신화된 데이터를 화면에 보여준다. 여기서 다시 사용자 관점에서 생각해보면 레벨로그 서비스는 사용하는 텀이 굉장히 짧다. 모의 인터뷰 그룹이 만들어지면 레벨로그를 작성하고 그에 대한 사전질문 작성하고 모의 인터뷰가 보통 하루에서 이틀안에 마무리되기 때문이다. 그러기에 cacheTime과 staleTime 짧게 가져가는 것이 좋다.


라이브러리 도입에 있어 팀원과 토론하며 알게 된 것이 있다.

  • 기술이 추구하는 방향을 인지하고 서비스가 추구하는 방향과 일치하는지 확인해보는 것이 중요하다.
  • 확인을 위해서는 빠른 도입과 빠른 결과를 보는 것이 웹 서비스 개발자에게 중요하다.

이제 코드로 살펴보자.


도입 전

리액트 쿼리를 도입하기 전 기존 코드에서 몇가지 좋지 않아보는 점을 찾을 수 있다.

  • 명령적인 프로그래밍과 반복적인 코드 작성

프론트엔드 개발자에게 비동기를 통해 API 요청에 대한 응답을 처리하는 데에 있어 굉장히 신경써야 하고 중요하다고 할 수 있다.
리액트 쿼리를 도입하기 전에는 API 요청을 try catch로 감싸고 API 응답값을 분기처리하여 성공/실패 후처리를 하였다.

// 피드백 get 요청하는 함수
const getFeedback = async () => {
 	try {
     	const res = await requestGetFeedback();
        
        return res;
    } catch(err) {
      // 에러처리
    }
}

const feedback요청후처리함수 = async () => {
 	const feedback = await getFeedback();
  
  	if (feedback) {
      	// 피드백이 존재하는 경우 후처리
     	setFeedback(feedback);
      	return;
    }
  	// 피드백이 없는 경우 후처리
}

이렇게 작성하니 모든 API 응답에 있어 반드시 분기처리를 통해 후처리를 해줘야하는 불편함과 코드가 명령적으로 작성될 수 밖에 없었다.


  • API 요청이 다른 API 응답값에 의존하는 경우

API 요청이 다른 API 결과값에 의존하는 케이스가 존재한다.
크루의 역할을 API 요청은 해당 크루의 고유 id를 알고 있어야 요청이 가능하다. 그리고 이 크루의 고유한 id 또한 레벨로그 id에 의존하기에 레벨로그를 get하는 API 요청을 통해 가져올 수 있다.

화면에서 레벨로그와 크루 역할을 보여주기 위해서는 레벨로그 API를 두번 요청해야 하는 문제가 발생한다. 물론 useLevellog 커스텀 훅을 useRole 커스텀훅에서 호출하면 레벨로그 API 요청은 한번만 보낼 수 있다. 하지만 그렇게 되면 useRole 커스텀 훅에서 레벨로그도 반환하게 된다. 그렇게 되면 useRole 커스텀 훅의 역할이 모호해진다. 이는 예상치 못한 곳에서 예상치 못한 API 요청이 발생하므로 개발자의 유지보수가 힘들어지는 결과를 초래한다. 한마디로 예상하기 어려운 코드가 탄생하는 것이다.
그래서 같은 요청이 두번 가더라도 개발자가 기대하는 대로 동작하는 커스텀 훅을 만들었다.

// 레벨로그 API 요청 -> 크루 역할 API 요청
const useRole = () => {
  // role 상태 로직...
  const getLevellog = () => {
    const res = requestGetLevellog();

    return res;
  }

  const getRole = (crewInfo) => {
    const res = requestRole(crewId);

    return res;
  }

  useEffect(() => {
      const init = async () => {
          const levellog = await getLevellog();
          const role = await getRole(levellog);
          // 후처리 로직
      }
      init();
  }, []);
  
  return {
    role
  }
}
 
const useLevellog = () => {
  // levellog 상태 로직...
  const getLevellog = () => {
    const res = requestGetLevellog();

    return res;
  }
  
  return {
   	levellog 
  }
}

도입 후

  • 선언적인 프로그래밍

리액트 쿼리에서는 API 요청후 성공/실패에 대한 후처리를 위한 옵션을 제공한다.

// 피드백 get 요청하는 함수
const { data } = useQuery(['feedback'], () => {
 	return  requestGetFeedback();
  }, 
  {
	onSuccess: () => {
      // API 요청 성공 후처리 로직
     setFeedback(feedback);
    },
  	onError: () => {
      // API 요청 실패 후처리 로직
    }
  }
);
  • onSuccess
    API 요청이 성공할때 마다 내부 로직이 실행된다.

  • onError
    API 요청이 실패할떄 마다 내부 로직이 실행된다.

onSuccessonError 옵션으로 인해 더 이상의 분기처리는 필요하지 않고 선언적으로 프로그래밍을 할 수 있다.


  • 같은 요청시 두번 보내더라도 하나의 요청으로 처리

리액트 쿼리는 GET 요청에 한해서 여러 요청을 보내더라도 하나의 요청으로 처리가 된다. 왜냐하면 GET 요청은 서버의 자원에 영향을 미치지 않고 그러기에 최적화가 효과적이다.

그래서 위 화면에서 리액트 쿼리를 사용하지 않는다면 중복된 요청이 두번 가지만 리액트 쿼리는 useQuery의 쿼리키에 같은 문자열을 넣는다면 리액트 쿼리는 같은 요청이라고 판단하고 두번째 요청은 가지않고 첫번째 요청의 결과를 가져와 사용한다.


  • 분기처리하지 않고 요청 보내기

만약 API 요청이 특정 값에 의존하는 경우가 있고 또 다른 API 응답값에 의존하는 경우가 있다. 그럴 때 보통 분기처리를 통해 API 요청을 보낼지 보내지 않을지 결정한다.
리액트 쿼리에서는 enabled 옵션을 제공해 쿼리를 비활성화 혹은 일시정지할 수 있다.

const { data: levellogInfo } = useQuery(
    [QUERY_KEY.LEVELLOG],
    () => {
      return requestGetLevellog();
    }
  );

  const author = levellogInfo?.author;

  const { data: feedbackWriterRole } = useQuery(
    [QUERY_KEY.ROLE, author],
    () => {
      return requestGetLoginUserRole({
        participantId: author?.id,
      });
    },
    {
      enabled: !!author,
    },
  );

useQuery를 통해 requestGetLevellog 응답값이 정상적으로 오면 변수 author는 string값을 갖게 된다. 그렇다면 다음 쿼리 requestGetLoginUserRole 의 enabled은 true가 되기에 다음 요청이 처리되는 것을 알 수 있다.
이로 인해 분기처리를 하지 않더라도 개발자가 쿼리를 보내고 싶을 때를 결정할 수 있다.


끝맺음

리액트 쿼리를 사용함으로함으로 API 관련 비동기 코드들이 좀 더 선언적이고 깔끔해진 것을 한 눈에 느낄 수 있다. 아직까지는 리액트 쿼리의 많은 기능들을 사용하지 못하고 있지만 점차 리액트 쿼리를 통해 좀 더 효율적이고 선언적인 코드들로 작성할 수 있게 되었다.

0개의 댓글