https://facebook.com/groups/graphql-kr logo
#질문
Title
# 질문
u

배휘동

11/06/2021, 9:28 AM
안녕하세요. 저는 회사에서 relay는 안써봤고 apollo client(v2)와 react-query만 써봤는데요. apollo가 가지는 normalized cache가 편할 때도 많았지만 mutation 후 invalidate할 때 캐시를 잘 관리하는 게 쉽지 않았습니다. 수정은 그나마 쉽지만 추가나 삭제에서는 정렬이나 페이지네이션 때문에 보통 전체 목록을 다시 가져오는 전략을 취했는데, 여기서 오는 성능 문제도 가끔 있었습니다. 아폴로나 릴레이로 normalized cache를 쓰시는 분들은 실무에서 캐시 무효화를 다들 어떻게 하시는지 궁금합니다.
👍 1
t

Tony Won

11/06/2021, 12:42 PM
릴레이에서는 Mutation 후에 리스트 앞 뒤로 붙여야할때 @appendEdge, @prependEdge, 혹은 리스트에서 삭제해야할때 @deleteEdge 등 기능을 지원합니다 ㅎㅎ
h

Hyeseong Kim

11/06/2021, 5:42 PM
Apollo 도 Pagination Util 이 있긴 합니다. https://www.apollographql.com/docs/react/pagination/core-api/
t

Tony Won

11/08/2021, 8:35 AM
@배휘동 답변이 되셨을까요??
mutation 후 invalidate할 때 캐시를 잘 관리하는 게 쉽지 않았습니다.
요거 정확한 시나리오가 궁금한데 혹시 알려주실수있을까요? 왠지 오해가 있을거같아서 자세히 설명드리고 싶습니다
u

배휘동

11/08/2021, 8:42 AM
앗 제가 알림이 안와서 답변 달아주신 걸 방금 알았습니다. 😅 시나리오 좀 정리해서 추가 코멘트 달아보겠습니다.
@Tony Won @Hyeseong Kim 저는 페이지네이션 자체보다는 목록 안의 이이템을 추가/삭제하는 mutation 후 목록을 효율적으로 갱신하는 문제에 대해 다른 분들의 경험이 궁금했습니다. (1년도 전에 고민한 주제이고 지금은 react-query가 메인이라.. 기억을 좀 되살려볼게요 ㅎㅎ) mutation 후 되도록이면 graphql 쿼리 요청을 줄이고 유저에게 즉시 결과를 보여주고 싶기 때문에, 목록 쿼리를 refetch하기보다는 응답을 캐시에 바로 업데이트하고 싶었습니다. (refetch 호출도 귀찮고) 그런데 apollo client는 업데이트 mutation은 result의 응답 결과를 자동으로 캐시에 업데이트해주지만 add, delete는 자동 업데이트를 안 해줍니다. 이 때는 cache를 manual update 하는 방식을 apollo가 제시하지만, 문법이 아주 직관적이지는 않고 목록이 깊은 위치에 존재할 때는 정확히 업데이트를 하는 것 자체가 힘들었습니다. 또한 유저에게 마지막으로 캐시된 목록에서 diff만 캐시에서 반영해주는 게 두 부분에서 부정확할 수 있다고 봤습니다. 1. 다른 유저의 행위에 의해 목록의 구성이 바뀌었을 수 있음 2. 아이템을 추가했을 때는, mutation 결과를 어디에 붙여넣을지 클라이언트가 정렬 순서를 정확히 알고 있어야 함(목록 맨 앞? 맨 뒤? 만약 정렬이 시간순이 아니라면?) 그래서, 대신 mutation의 응답 결과에서 목록 전체를 다시 쿼리해오는 방식을 사용했습니다. 그러나 이 방식 또한 한계가 있었는데, 1. 전체 목록을 다시 가져오기 때문에, 목록을 쿼리해오는 게 무겁다면 유저가 mutation 응답을 받아 결과를 보는 것 자체가 오래 걸린다 a. -> optimistic UI를 고려는 했었지만 실제 적용까지는 안했습니다 2. 페이지네이션이 이미 되어있는 목록을 이 방식으로 가져오기가 까다롭다. (mutation에서 리페치해올 오프셋 지정을 안 하면 첫 페이지 크기만큼만 가져올텐데, 그렇다고 오프셋을 지정하자니 mutation의 책임이 아니고..) a. -> 이 문제는 실제로 자주 발생하지 않았기 때문에 무시했고, react-query에서는 InfiniteQuery가 애초에 일반 쿼리와 구분되어 있어서 invalidate하면 기존 캐시에 있는 것들을 순차적으로 업데이트해주길래 편했습니다. 이런 일을 겪고 나니 normalized cache 가 편하기도 하지만 불편하기도 해서, 지금은 react-query를 메인으로 쓰고 있습니다. 써보니 API 호출 횟수를 줄이는 것보다는 캐시 관리가 편한 게 더 낫다는 생각이 들었습니다.
Copy code
query {
  articles {
    id
    title
  }
}

mutation UpdateArticleTitle($id: ID!, $title: String!) {
  updateArticleTitle(input: { articleId: $id, title: $title }) {
    article { // 아폴로 캐시의 articles에 해당 id 값의 title이 자동 업데이트됨
      id
      title
    }
  }
}

mutation RemoveArticle($id: ID!) {
  removeArticle(input: { articleId: $id }) {
    query { // mutation 응답에서 목록을 다시 불러옴
      articles {
        id
        title
      }
    }
  }
}
h

Hyeseong Kim

11/08/2021, 11:34 AM
InfiniteQuery가 애초에 일반 쿼리와 구분되어 있어서 invalidate하면 기존 캐시에 있는 것들을 순차적으로 업데이트해주길래 편했습니다.
네 이게 Pagination 이 대표적으로 데이터 그래프 모델링할 때 다루지 않고 클라이언트에서 추상화되어야 하는 순수 UI 로직이라서 그런데요. 말씀하신거처럼 유틸리티를 만들어서 대응하고 Apollo Client 에서는 주로 Type Policy 에서 정책을 재사용하는 식으로 다룹니다. 저 같은 경우는 전에 Apollo Client 2 쓸 때 요청 크게 나가는게 싫어서 Offset 맥락을 따로 관리했던 기억이 있네요.
Pagination은 다른 라이브러리 쓸 때도 대체로 비슷할거에요 ㅋㅋ 라이브러리가 직접 지원하면 좀 애매한게 사례가 딱 맞으면 편하긴 한데 보통 커스터마이징이 살짝씩 들어가기도해서 아폴로 쓸 때도 굳이 편하게 만드려고 노력은 안했었네요.
u

배휘동

11/08/2021, 11:58 AM
그렇군요. 답변 감사합니다. 🙂 저도 아폴로 v3가 너무 안나와서 오랫동안 v2까지만 썼었는데 혹시 이제 더 괜찮아졌나, 하는 기대도 있었는데 그건 아닌가보네요. (그나저나 쓰레드 알림이 안올까요..ㅠ 알림 설정에는 문제가 없는데..)
h

Hyeseong Kim

11/08/2021, 12:18 PM
아 그래도 오프셋 기반 커넥션 기반 각각이 페이지네이션 기본 유틸이 있어서 Apollo 2 때보단 많이 낫습니다 :)
👍 1
💡 1
u

배휘동

11/09/2021, 5:51 AM
페이지네이션 관련해서는 혜성님이 잘 답변해주셔서 의문이 좀 풀렸는데요. 목록을 mutation 후 갱신하는 프랙티스에 대해서는 relay에서, 또는 다른 회사에서 어떤지는 아직 궁금하네요. 혹시 @Tony Won 답변해주실 수 있을까요?
t

Tony Won

11/09/2021, 6:38 AM
Copy code
mutation PageReviewsDeleteReviewMutation(
        $reviewId: ID!
        $connections: [ID!]!
      ) {
        deleteReview(input: {
          reviewId: $reviewId,
        }) {
          __typename

          ...on DeleteReviewOutput_Result {
            result {
              reviewEdge {
                node {
                  id @deleteEdge(connections: $connections)
                }
              }
              widget {
                ...ProfileReviews_widget
              }
            }
          }
          ...on DeleteReviewOutput_WidgetNotFoundError {
            widgetNotFoundError {
              message
            }
          }
          ...on DeleteReviewOutput_ReviewNotFoundError {
            reviewNotFoundError {
              message
            }
          }
          ...on UnauthorizedError {
            unauthorizedError {
              message
            }
          }
          ...on UnknownError {
            unknownError {
              message
            }
          }
        }
      }
delete 할때는 이렇게 하구요 ㅎㅎ append, prepend할때는 마찬가지로
@appendEdge
@prependEdge
directive를 쓰면 relay가 자동으로 붙여줘요.
u

배휘동

11/09/2021, 9:07 AM
append, prepend가 아닌 방식으로 업데이트해야 할 때는 없으셨나요? 제가 위에 썼던 부분을 다시 적어보면: 유저에게 마지막으로 캐시된 목록에서 diff만 캐시에서 반영해주는 게 두 부분에서 부정확할 수 있다고 봤습니다. 1. 다른 유저의 행위에 의해 목록의 구성이 바뀌었을 수 있음 2. 아이템을 추가했을 때는, mutation 결과를 어디에 붙여넣을지 클라이언트가 정렬 순서를 정확히 알고 있어야 함(목록 맨 앞? 맨 뒤? 만약 정렬이 시간순이 아니라면?)
t

Tony Won

11/10/2021, 9:53 AM
다른 유저의 행위에 의해 목록의 구성이 바뀌었을 수 있음
이건 해당 유저에게는 중요한 변경사항이 아닐수있어요. 유저가 "바라보는 세상"의 일들만 Consistency가 맞으면 됩니다 ㅎㅎ (그게 DB와 100% 싱크를 맞출수는 없어요. 만약에 그걸 원하시면 Subscription을...) 만약에 그정도로 중요한 실시간 변경사항이면 구현하셨던것처럼 refetch나 Subscription으로 받는게 맞을거같네요. 근데 이건 Normalize된 캐시라서 더 어려운 문제는 아닐거같아요.
아이템을 추가했을 때는, mutation 결과를 어디에 붙여넣을지 클라이언트가 정렬 순서를 정확히 알고 있어야 함
넵 이건 맞습니다. 다만, 정렬 순서랑 관계없이, 일반적으로 유저가 작성한 콘텐츠는은 맨 위 (ex: 페이스북 피드) 또는 맨 아래 (ex: 채팅) 에 가는게 일반적입니다. 페이스북 피드는 시간 순서가 아니지만 유저가 글을 쓰면 맨 위로 올려주죠. 정렬보다는 유저의 탐험 경험과 연관이 더 많을거같아요. 리스트 중간에 추가한다던지 하는 경우는 저는 여태까지 경험을 못해봤네요;; 지금 당장 떠오르는 Use case는 상단 고정 게시글이 있다고 했을때 그 밑에 꼽는건데요. 위쪽 고정 게시물은 따로 처리하고, 그 아래부터 connection으로 간주하면 마찬가지로 쉽게 구현할 수 있을거같아요.
👍 1
u

배휘동

11/10/2021, 10:01 AM
답변 감사합니다. 🙂
유저가 "바라보는 세상"의 일들만 Consistency가 맞으면 됩니다
이 말씀이 잘 와닿네요. "정렬보다는 유저의 탐험 경험" 이라는 말씀과도 잘 연결이 되는군요.