아폴로 캐시 업데이트하기

마데슾 : My Dev Space·2020년 7월 21일
5

목차

  1. 개요
  2. 캐시 업데이트 방법
    1. polling
    2. refetching
    3. useMutation Hooks에 update 기능 추가

개요

현재 저는 팀원들과 함께 개구리라는 앱을 만들고 있습니다.
이 앱은 개발자, 기획자, 웹/앱 디자이너 등등의 직업을 가진 사람들이 사이드 프로젝트를 함께 진행할 수 있도록 도와주는 앱입니다.

이 프로젝트에서는 데이터패칭을 하기 위한 수단으로 Apollo를 사용하고 있습니다.

아폴로 클라이언트는 불필요한 네트워크 호출을 피하고 쿼리 결과를 브라우저에 저장하기 위해 캐시를 사용합니다.

이를 위해 아폴로 클라이언트에는 Fetch Policies라는 것이 존재하는데요, Fetch Policies 설정값에 따라 캐시에서 빠르게 데이터를 가져올지 서버에서 최신 데이터를 가져올지가 결정되는데 기본값이 cache-first으로 되어있어서 캐시에 데이터가 있으면 캐시에서 데이터를 가져오고 없으면 서버에서 데이터를 가져옵니다.

cache-first가 적용되었을때 데이터를 가져오는 순서는 아래와 같습니다.
1. 아폴로가 데이터 캐시를 확인합니다.
2. 데이터가 캐시에 존재하면 바로 데이터가 반환됩니다.
3. 데이터가 캐시에 존재하지 않는다면 아폴로는 서버에 데이터를 요청합니다.
4. 아폴로는 서버에서 응답받은 데이터를 사용하여 캐시를 업데이트 합니다.
5. 요청된 데이터가 반환됩니다.

위와 같은 특징때문에 만약 새로운 데이터가 추가되어도 서버에는 반영이되지만 캐시는 업데이트가 되지 않습니다. 캐시에 데이터가 존재하면 서버에 데이터 요청을 하지 않고 원래 캐시에 저장되어 있던 데이터를 반환하기 때문이죠.

위와같은 문제를 해결하기위해 캐시를 업데이트 하는 방법에 대해 조사를 해보았습니다.

(Fetch Policies에 대해서는 해당 블로그에 자세하게 나와있습니다.)

Polling

첫번째 방법으로 Polling이라는 방법을 소개합니다.

여러분 Polling의 뜻이 무엇인지 알고 계시나요?
저는 궁금해서 구글에 검색을 해보았는데요. 위키백과에서는 polling을 아래와 같이 정의하고 있습니다.

폴링(polling)이란 하나의 장치또는 프로그램이 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치 또는 프로그램의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식을 말한다.
(출처 : 위키백과)

이러한 특징을 사용해 아폴로에서도 클라이언트서버일정한 주기를 가지고 응답을 주고받는 Polling 방식을 사용하고 있나봅니다.

Polling의 의미를 알아보았으니 이제 Polling의 특징을 알아봅시다.

  • 지정된 간격으로 쿼리가 주기적으로 실행되도록하여 서버와 거의 실시간으로 동기화합니다.
  • startPollingstopPolling함수를 사용하여 동적으로 폴링을 시작 및 중지 할 수도 있습니다.

저는 위와같은 특징을 사용하여 프로젝트 리스트 데이터가 업데이트될때마다 메인화면에 렌더링해주려 합니다.

아래에 메인화면이 준비되어 있는데요, 메인화면에는 현재 로그인한 유저가 참여하지 않은 프로젝트 리스트가 보여지게 됩니다.

메인화면 진입시 프로젝트 리스트 데이터를 불러와주는 아폴로 클라이언트 쿼리는 아래와같이 작성되어 있습니다.

  const { loading, error, data } = useQuery(GET_PROJECT, {
    pollInterval: 500,
  });

이렇게 useQuerypollInterval:500 옵션을 사용하게 되면 0.5 초마다 서버에서 현재 프로젝트의 리스트를 가져올 수 있습니다.

자, 이제 어떠한 방법으로 메인 페이지에 프로젝트 리스트가 불러와지는지 확인했으니 한번 직접 확인해볼까요?
프로젝트 리스트중 하나를 선택해 마음에 드는 프로젝트방으로 이동해 봅시다.

마음에 드는 방하나를 고르고 참여요청 버튼을 클릭하게되면 미리 만들어놓은 참여요청 뮤테이션이 실행되고 채팅화면으로 이동됩니다.

채팅화면에 들어간 후 뒤로가기 버튼을 누르게되면

다시 메인화면으로 나오게 됩니다. 이 때 프로젝트 리스트를 보시면 달라진게 보이시나요?

오른쪽은 참여요청 전 프로젝트 리스트 목록이고 왼쪽은 참여요청 후 프로젝트 리스트 목록입니다. 보시면 방금전 참여요청기업 단기 프로젝트 멤버 급구 방이 사라진것을 보실수가 있습니다.

메인페이지 진입시 실행되는 GET_PROJECT는 자신이 참여하지 않은 프로젝트 리스트 목록을 가져오기때문에 방금전에 참여한 방 목록은 프로젝트 리스트에서 사라지게됩니다.

그럼.. 내가 참여한 프로젝트 방은 어디서 확인하냐구요?..

화면에서 확인 가능합니다. 다만 해당 글에서 언급할 일이 없을거 같기때문에 존재만 알리고 떠나겠습니다..ㅎㅎ

자, 이제 다시 본론으로 돌아왔습니다.

아래의 글을 보시면 제가 참여요청을 누르기 전과 후의 GET_PROJECT 쿼리 결과값을 비교해 놓았는데요, 한번 보실까요?

제가 75번 방에 참여요청 버튼을 누르기 전의 쿼리 결과값입니다

자 이제 참여요청 버튼을 누른 다음에 쿼리 결과값을 보시면 75번 방은 목록에 존재하지 않습니다.

아! 그리고 추가로 위에서 한번 언급했던 화면에서 알람 버튼을 클릭하게되면 알람 리스트 페이지로 이동하게 되는데 이때 알람페이지에서도 Polling 방법을 사용해 데이터를 가져오고 있습니다.

const { data, loading } = useQuery(GET_ALRAMLIST, { pollInterval: 200 });

GET_ALRAMLIST 쿼리를 주기적으로 실행해줘서 유저는 알람이 실시간으로 추가되는 것을 확인해볼 수 있습니다.

refetching

두번째 방법으로 refetching이라는 방법을 소개합니다.

먼저, refetching의 특징입니다.

  • 사용자 action에 대한 응답으로 쿼리 결과를 새로 고칠 수 있습니다.

현재 진행중인 프로젝트에 사용은 못해보았지만, 사용자의 특정 액션에 반응하여 쿼리 결과를 새로 고칠 수 있는 refetch를 이 프로젝트에서 프로젝트 리스트 데이터를 불러오는 또는 메인 화면에서 사용할 수 있지 않을까 라는 생각을 해보았습니다.

예를 들어..

인스타페이스북을 사용하다보면 최신글에서 스크롤을 아래로 당기면 새로운 데이터를 불러오는 것을 확인할 수가 있습니다. 이러한 기능을 보면서 현재 진행중인 프로젝트의 메인 또는 홈 화면에서도 제일 최근에 불러온 프로젝트 리스트의 최상단에서 스크롤을 아래로 당길때 refetching을 실행시켜 새로운 데이터를 불러올 수 있다면 좋을거 같다는 생각을 했습니다.

사용자가 원하지 않는데 polling 기능으로 서버에 계속 데이터를 요청해 가져오는 것보다는 사용자가 업데이트된 데이터를 확인하고 싶을때 스크롤을 아래로 당기면 그떄 refetching을 통해 서버에서 새로운 데이터를 가져와 보여주는것이 좋지 않을까? 하는 생각을 했습니다.

아래는 제가 예시로 말씀드렸던 부분에 대한 이해를 돕기위한 이미지입니다.

이론 설명이 끝났으니.. 코드로 어떻게 구현되는지 보도록 하겠습니다.
refetching 기능은 현재 프로젝트에서 사용해보지 않았기때문에 예시 코드는 아폴로 공식문서에 올라왔는 코드를 사용하겠습니다. codesandbox에서도 확인해 볼 수 있어서 이해하는데 도움이 많이 되었습니다.

function DogPhoto({ breed }) {
  const { loading, error, data, refetch, networkStatus } = useQuery(
    GET_DOG_PHOTO,
    {
      variables: { breed },
      notifyOnNetworkStatusChange: true
    }
  );

  if (networkStatus === 4) return <p>Refetching!</p>;

  if (loading) return null;
  if (error) return `Error!: ${error}`;

  return (
    <div>
      <div>
        <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
      </div>
      <button onClick={() => refetch()}>Refetch!</button>
    </div>
  );
}

useQuery를 사용하면 loading 값이 true일때를 캐치해서 로딩바를 노출시킬수가 있는데 refetching시에는 loading 값에 아무런 변화가 없는것을 확인하실 수 있습니다.

그럼 refetching시 사용자는 refetching 중이라는 것을 인지하지 못하기 때문에 refetching이 길어질 경우 아무일도 일어나지 않는다고 판단해버릴지도 모릅니다..!

이 문제를 해결하기 위해서는..!
1. notifyOnNetworkStatusChange 옵션을 true로 설정해준다
2. networkStatus === 4일때 로딩바를 노출시킨다. refetch가 호출되었을때 networkStatus 값이 4가 된다고 합니다. => networkStatus 참고

if (networkStatus === 4) return <p>Refetching! 또는 로딩바</p>

이렇게 해주면 1번 코드로 인해 refetching 이 진행되는 동안 컴포넌트가 다시 렌더링되고, 2번 코드로 인해 refetching이 진행되는 동안 로딩바를 노출시켜 데이터를 가져오는 중이라는 것을 사용자에게 알릴 수가 있습니다.

이렇게 아폴로 클라이언트 쿼리 사용시 캐시를 업데이트 방법에 대해 알아보다 보니.. 개인적으로 현재 프로젝트 홈, 메인 페이지에서 사용하고있는 folling 기능을 refetching 기능으로 바꾸는게 더 좋을거 같다는 생각이 듭니다.

다음 글에서는 뮤테이션 후 캐시 업데이트하는 방법에 대해 알아보도록 하겠습니다.

useMutation Hooks에 update 기능 추가

세번째 방법으로는 뮤테이션 후 캐시를 업데이트하는 방법을 소개하겠습니다.

Mutaion 후 캐시를 업데이트하는 방법은 새로운 프로젝트를 만든후 현재 내 프로젝트 리스트를 업데이트하는 부분에서 사용되었습니다.

프로젝트가 만들어지는 과정을 먼저 설명드리겠습니다.

위에서 보여드린 단계를 프로젝트 화면을 보면서 설명드리도록 하겠습니다.

먼저 프로젝트 리스트 생성 페이지에서 필요한 정보를 입력하고 완료 버튼을 누릅니다.(참고로 저는 [테스트]뮤테이션 캐시 업데이트라는 방을 만들었습니다)

완료 버튼을 클릭하면 화면으로 이동하게 되고, 방금전 만든 프로젝트방도 추가된 것을 확인해 보실 수 있습니다.

위와 같이 구현하기 위해 저는 어떻게 코드를 작성하였을까요? 바로 아래에 코드가 준비되어 있습니다.

const [createNewProject] = useMutation(CREATE_PROJECT, {
    update(cache, { data }) {
      const newProject = data?.createNewProject?.newProject;
      
      // 캐시에서 프로젝트 목록을 가져옵니다 
      let existingProejects = cache.readQuery({
        query: GET_PROJECT_STATUS_FILTER,
      });
  	
      let project = existingProejects.getMyProjectListwithStatus.statusProject;

      console.log('===========================');
      console.log('[전] existingProejects ? ', existingProejects);

      console.log('newProject ? ', newProject);

      console.log('[전] project.onGoing ? ', project.onGoing);
		
      // 프로젝트 목록에 새로운 프로젝트를 추가합니다.
     // 추가한 프로젝트는 `project.onGoing`에 추가되어야합니다.
      project.onGoing = [newProject, ...project.onGoing];

      console.log('[후] project.onGoing ? ', project.onGoing);
	  
      // 캐시에 "프로젝트 목록 + 새로운 프로젝트"를 추가합니다.
      cache.writeQuery({
        query: GET_PROJECT_STATUS_FILTER,
        data: {
          getMyProjectListwithStatus: existingProejects?.getMyProjectListwithStatus,
        },
      });
    },
  });

위의 코드에서 사용한 console.log 값입니다.

확실히 pollingrefetching보다는 코드가 긴거 같습니다..ㅎㅎ

코드를 설명해보겠습니다.

  1. 먼저 cache.readQuery를 사용하여 GET_PROJECT_STATUS_FILTER 쿼리를 실행합니다. 그리고 실행된 쿼리의 결과를 existingProejects 변수에 담아줍니다.
  2. 쿼리결과가 담긴 existingProejects 변수를 [createNewProject 뮤테이션 결과값, ...기존 쿼리 결과값]으로 재정의해줍니다. (생성된 방 + 기존 방 리스트의 형태로 쿼리결과를 불러오기 위함입니다)
  3. writeQuery를 사용해 화면에 GET_PROJECT_STATUS_FILTER가 실행되었을때 [프로젝트 방 생성 뮤테이션 결과값, ...기존 쿼리 결과값]를 결과로 받아볼수 있도록 data에 담아줍니다.
  4. 화면으로 이동했을때 GET_PROJECT_STATUS_FILTER 쿼리가 실행된 모습을 보면 뮤테이션 결과값이 추가되어 있는것을 확인할 수 있습니다.

프로젝트가 잘 만들어졌는지 확인하기위해 화면에서 실행되는 GET_PROJECT_STATUS_FILTER 쿼리의 결과값을 콘솔에 찍어보았습니다.

< 프로젝트 생성 뮤테이션 전 >

< 프로젝트 생성 뮤테이션 후 >

화면에도 방금전 만든 프로젝트방이 업데이트 되었습니다. 뮤테이션 후 업데이트 성!공!

느낀점

redux 사용시에는 아래와같이 여러 단계를 거쳐야 가능했던 것이 readQuerywriteQuery 만으로 해결이되니 신기하다..

  1. REQUEST_ADD_PROJECT 액션 디스패치

  2. 리덕스 사가에서 해당 액션이 발생하면 add porject post api 요청

  3. 요청에 성공하면 리덕스 사가에서 SUCCESS_ADD_PROJECT 액션 디스패치

      // 예시
      yield put({
        type: SUCCESS_ADD_PROJECT,
        data: result.data,
      });
  4. 서버에서 응답받은 데이터를 이용해 리덕스 스토어 업데이트

  5. 스테이트가 변경되면서 컴포넌트 리렌더링

    참고글

profile
👩🏻‍💻 🚀

1개의 댓글

comment-user-thumbnail
2020년 8월 13일

작성글 잘보았습니다! 개구리 앱 또한 너무 기대됩니다.
배포되면 꼭 다시 업로드해주세요ㅎㅎ

답글 달기