Published on

[번역] Relay - Graphql Queries for Interactions

Authors
  • avatar
    Name
    Kim, Dong-Wook
    Twitter

GraphQL Queries for Interactions

우리는 각각의 컴포넌트 안에서 어떻게 fragment들이 데이터 요구사항을 명시하는지 확인했다.(런타임에서 전체 화면에 대해서 단 하나의 query만 실행하는 경우에 한해서) 이 페이지에서 우리는 같은 화면에 대해서 두 번째 query를 실행하는 상황에 대해서 알아볼 것이다. 이를 통해 GraphQL query가 어떤 기능을 가지고 있는지 좀 더 탐색해볼 수 있을 것이다.

  • hovercard 컴포넌트를 제작할텐데, 이름 위에 마우스를 올리면 게시물에 대한 자세한 내용을 보여준다
  • hovercard는 유저가 마우스를 호버링 했을때만 추가적인 정보를 가져오는 query를 실행한다
  • 우리는 query 변수를 이용해서 어떤 게시물의 세부사항을 불러올지 결정할 것이다.
  • Preloaded query를 통해 성능을 향상시키는 방법에 대해서 알아볼 것이다.

위의 주제들을 살펴보고 난 뒤에, Fragment의 세부 기능들에 대해서 좀 더 살펴볼 것이다.

Contents

해당 섹션에서, PosterByline에 hovercard를 추가할 것이다. 이를 통해 게시물의 세부사항을 마우스를 올리는 것 만으로도 조회할 수 있게 할 것이다.

Deep Dive - 언제 secondary query를 사용해야할까? 이전에 언급했다싶이 Relay는 전체 화면을 그리기 위한 모든 데이터를 불러올 수 있도록 설계되었다. 그러나 이것을 일반화시키면 유저 인터렉션은 단 한 query를 통해 이뤄져야한다고 생각할 수 있다. 다른 화면으로 Navigating 해주는 것은 가장 흔한 유저 인터렉션이다.

화면 안에서, 몇몇 인터렉션은 처음 화면에 보였던 것과 다른 데이터를 추가적으로 요구할 수 있다. 만약 하나의 인터렉션이 비교적 가끔씩만 실행된다면(하지만 상당한 양의 추가적인 데이터를 요구한다면), 해당 데이터는 second query를 통해 불러오는 것이 더 좋을 것이다. 이렇게 함으로써 초기 로딩 속도를 더 빠르고 덜 expensive하게 만들 수 있다

그리고 불러올 데이터의 양이 명확하지 않은 인터렉션이 있다(예를 들면 Hovercard안에 hovercard가 있다면 통계적으로 얼마만큼의 데이터가 필요할지 가늠할 수 없다)

만약 데이터가 낮은 우선순위를 가지고 있고, 주요 데이터가 로드된 이후에 로드되어야한다면(하지만 추가적인 유저의 Input이 없이 자동으로 화면에 나타나야한다면), Relay는 deferred fragment라는 기능을 통해 원하는 기능을 만들 수 있다. 하지만 이는 추후에 다룰 것이다.

아래에 이미 hovercard 컴포넌트가 준비되어 있다. 그러나, 해당 컴포넌트가 ImageFragment를 사용하고 있기 때문에 컴파일 에러가 발생할 수 있어서 future 폴더에 넣어두었다. 이제 future 폴더에 있는 컴포넌트를 src/components 폴더로 옮겨보자

PosterByline 컴포넌트가 fragment를 사용하도록 실습을 해봤다면, PosterByline 컴포넌트는 아래와 같을 것이다.

export default function PosterByline({ poster }: Props): React.ReactElement {
  const data = useFragment(PosterBylineFragment, poster)
  return (
    <div className="byline">
      <Image image={data.profilePicture} width={60} height={60} className="byline__image" />
      <div className="byline__name" ref={hoverRef}>
        {data.name}
      </div>
    </div>
  )
}

hovercard 컴포넌트를 사용하기 위해서는 아래와 같이 변경해야한다.


import Hovercard from './Hovercard';
import PosterDetailsHovercardContents from './PosterDetailsHovercardContents';
const {useRef} = React;

...

export default function PosterByline({ poster }: Props): React.ReactElement {
  const data = useFragment(PosterBylineFragment, poster);
  const hoverRef = useRef(null);
  return (
    <div
      ref={hoverRef}
      className="byline">
      <Image image={data.profilePicture} width={60} height={60} className="byline__image" />
      <div className="byline__name">{data.name}</div>
      <Hovercard targetRef={hoverRef}>
        <PosterDetailsHovercardContents />
      </Hovercard>
    </div>
  );
}

이렇게 하면, 유저 이름에 마우스를 올리면 hovercard 컴포넌트에 유저의 상세정보가 표시되는 것을 확인할 수 있다. PosterDetailsHovercardContents.tsx 파일을 좀더 살펴보면, 해당 컴포넌트가 마운트 되었을 때 useLazyLoadQuery Hook을 통해 second query가 실행되면서 추가적인 정보를 불러오는 것을 확인할 수 있다.

chrisPbacon

Query Variables

우리가 데이터를 불러올 때, 서버에게 어떤 정보를 더 원하는지 알려줘야한다. GraphQL은 특정 field에 대해 인자로 넘겨줄 수 있는 query variable을 정의하고 있다. 해당 인자들은 서버에서 사용될 수 있다.

이전 섹션에서, field에 인자들을 어떻게 넣는지 살펴봤지만, hard-code되어져 있었다.(예를 들면 url(width:200, height: 200) 이런식으로 말이다) query variable을 이용하면 런타임에서 해당 값을 결정할 수 있다. query variable들은 쿼리 그 자체로 client에서 server로 전달된다. GraphQL variable들은 달러기호($)로 시작한다.

PosterDetailsHovercardContents.tsx를 자세히 살펴보면, 아래와 같은 쿼리문을 확인할 수 있다.

const PosterDetailsHovercardContentsQuery = graphql`
  query PosterDetailsHovercardContentsQuery {
    node(id: "1") {
      ... on Actor {
        ...PosterDetailsHovercardContentsBodyFragment
      }
    }
  }
`

node field은 unique한 ID를 기반으로 graph node를 가져오도록 하는 top-level field이다. Id를 인자로써 받으며, 위의 코드는 hard-code된 버전이다. 실습을 통해 hard-code된 ID를 UI 상태에 따라 변경되는 버전으로 바꿀 것이다. 흥미로운 점은 ... on Actor와 같이 사용해서 타입을 refine할 수 있다는 것이다. 다음 장에서 자세히 살펴볼 것이라 여기서는 넘어가도록 하겠다. 간단히 설명하자면, node field에 어떤 ID라도 넣을 수 있기 때문에, 통계적으로 어떤 타입의 노드를 선택하고 싶은지 알 길이 없다. type refinement는 우리가 어떤 타입을 예상하고 있는지 명시할 수 있으며, 이를 통해 Actor 타입에 대한 field를 사용할 수 있도록 해준다.

이를 통해, 우리가 보여주고 싶은 field들을 포함하고 있는 fragment를 "spread"할 수 있다. 아무튼 지금은 hard-code된 버전을 어떻게 개선하는지 살펴보자.

Step 1 -- define a query variable

먼저 작성한 query문이 query variable을 사용할 수 있도록 수정해야한다.

const PosterDetailsHovercardContentsQuery = graphql`
  query PosterDetailsHovercardContentsQuery($posterID: ID!) {
    node(id: "1") {
      ... on Actor {
        ...PosterDetailsHovercardContentsBodyFragment
      }
    }
  }
`
  • variable의 이름은 $posterID이다. 이 심볼은 해당 값이 UI로 부터 값이 들어올 것이라는 것을 의미한다.
  • variable들은 타입이 있다. 이 경우에는 ID!이다. ID 타입은 node ID를 가리키는 String 타입의 동의어이다(다른 string들과 구분하는걸 돕기 위해서 사용했다). 느낌표(!)는 해당 field가 nullable 하지 않다는걸 의미한다. GraphQL에서 field는 보통 nullable 하고, non-nullable이 예외적이다.

Step 2 — pass the variable in as a field argument

이제 hard-codeehls "1"을 우리의 새로운 변수로 교체해보자

const PosterDetailsHovercardContentsQuery = graphql`
  query PosterDetailsHovercardContentsQuery($posterID: ID!) {
    node(id: $posterID) {
      ... on Actor {
        ...PosterDetailsHovercardContentsBodyFragment
      }
    }
  }
`

주의: query variable들은 field 인자로 쓸 뿐만 아니라, framgnet들의 인자로도 사용할 수 있다.

Step 3 — provide the argument value to useLazyLoadQuery

이제 우리는 런타임에서 UI로 부터 받은 실제값을 넘겨줘야한다. useLazyLoadQuery Hook의 두번째 인자는 variable 값을 가진 객체이다. 우리는 새로운 prop을 컴포넌트에 넘기고, 컴포넌트 안에서 값을 넘겨줄 것이다.

export default function PosterDetailsHovercardContents({
  posterID,
}: {
  posterID: string
}): React.ReactElement {
  const data = useLazyLoadQuery<QueryType>(PosterDetailsHovercardContentsQuery, { posterID })
  return <PosterDetailsHovercardContentsBody poster={data.node} />
}

Step 4 — pass the ID in from the parent component

hovercard의 부모 컴포넌트(즉 PosterByline 컴포넌트)에서 posterID를 제공한다. 해당 파일을 넘겨주고 그리고 해당 fragment에 id를 추가한다. 그리고 ID를 prop으로 넘겨준다.


const PosterBylineFragment = graphql`
  fragment PosterBylineFragment on Actor {
    id
    ...
  }
`;

export default function PosterByline({ poster }: Props): React.ReactElement {
  ...
  return (
   ...
    <PosterDetailsHovercardContents
      posterID={data.id}
    />
   ...
  );
}

여기서 우리는 마우스를 카드 위에 올렸을때 적절한 정보가 보여지는 것을 확인할 수 있다.

detail-card

또한 브라우저의 Network 탭을 확인해보면, query 항목 옆에 variable 또한 적절하게 보내진 것을 확인할 수 있다.

variable-relay-network

또한 특정 포스터에 대한 요청에서 가장 첫번째 요청만 위의 결과가 표시되는 것을 확인할 수 있다. Relay가 query의 결과를 캐싱하고, 그리고 그 이후에는 재사용하기 때문이다.(단, 캐시가 만료되었을 경우에는 새로운 요청을 보낸다.)

Deep Dive - 캐싱 그리고 Relay Store

다른 대부분의 시스템과 대조적으로, Relay의 캐싱은 query를 기반으로 하지 않고 그래프 노드를 기반으로 합니다. Relay는 결과가 fetch 될때 모든 노드들을 캐싱합니다. Store에 존재하는 각 노드들은 ID를 기반으로 식별됩니다. 만약 같은 정보에 대해서 두 개의 쿼리가 요청하면, ID 기반으로 식별되기 때문에, 나중에 호출된 query는 캐쉬된 결과를 반환합니다. Relay는 어떤 쿼리를 사용하는 노드가 "reachable"하지 않다면 garbage-collect가 동작하게 됩니다.

Deep Dive - 왜 GraphQL은 Variable을 위한 문법이 필요할까?

왜 GraphQL은 값을 query string으로 변환하는 방식 대신에 variable이라는 개념을 가질까? 이전에 언급했다 싶이 GraphQL query string의 텍스트는 런타임에서 사용할 수 없다. 왜냐하면 Relay는 해당 값을 더 효율적인 데이터구조로 교체한다. 또한 Relay가 prepared queries를 사용하도록 설정해서, 빌드시 각각의 쿼리를 서버에 보내서 ID를 할당받는 방식을 사용할 수 있다. 이렇게 설정했을 경우, 런타임에서 Relay는 서버에서 "내 #1337 쿼리를 보내줘"라고 요청하는 방식으로 동작하기 때문에 문자열 변환이 불가능한 것이고, 그렇기에 variable이라는 방식이 나온 것이다. 정말 만약에 query string을 사용하는 방식이 가능했다고 해도 각각의 값과 escape 문자열을 serializing해야하는 문제가 발생했을 것이다(그것도 각각의 요청마다)

Preloaded Queries

아래에서 설명할 예제 어플리케이션은 매우 간단하다. 그래서 성능은 큰 문제가 되지 않는다.(사실 로딩 상태를 보여주기 위해서 인위적인 지연코드를 서버코드에 넣었다) 그렇지만, Relay의 가장 큰 관심사 중에 하나는 실제 앱에서 성능을 가능한 빠르게 만드는 것이다.

현재, hovercard는 useLazyLoadQuery Hook을 사용하고 있다(즉 컴포넌트가 렌더링 될 때 query 결과를 불러온다). 그래서 타임라인을 정리하자면 아래와 같은 사진이 나온다

Timeline

이상적인 환경에서 네트워크 fetch는 가능한 빠르게 되어야한다. 하지만 실제로는 React가 렌더링 과정을 끝내기전까지 fetch를 실행하지 않는다. 여기에 우리가 React.lazy를 사용한다면 hovercard 컴포넌트와 인터렉션을 하기 위해서 코드를 로드하는데 더 긴 시간이 걸릴 것이다. 해당 경우 아래와 같은 사진이 나온다.

Lazy-Timeline

Graphql query 결과를 불러오기도 전에 대기하고 있다는 점에 주목하자. React 컴포넌트가 렌더링 되기도 전에 query fetch가 시작하면 더 좋을 것 같다. 그렇게 된다면 타임라인은 아래와 같이 된다

Optim-Timeline

유저가 인터렉션을 할 때, 우리가 필요로 하는 query를 즉시 fetching 해야한다. 반면에 컴포넌트를 렌더링하기 시작해야한다. 두 비동기 작업이 완료되면, 우리는 query 결과를 가지고 컴포넌트를 렌더링하고 유저에게 보여줄 수 있는 것이다

Relay는 preloaded queries라는 기능을 통해 위의 작업을 할 수 있게 해준다

hovercard 컴포넌트를 다음과 같이 수정해보자

Step 1 — change useLazyLoadQuery to usePreloadedQuery

다시 말하자면, PosterDetailsHovercardContents 컴포넌트는 현재 데이터를 lazy하게 불러오고 있다.

export default function PosterDetailsHovercardContents({
  posterID,
}: {
  posterID: string
}): React.ReactElement {
  const data = useLazyLoadQuery<QueryType>(PosterDetailsHovercardContentsQuery, { posterID })
  return <PosterDetailsHovercardContentsBody data={data.node} />
}

해당 컴포넌트는 useLazyLoadQuery를 호출한다.(변수를 두번째 인자로 받는다). 우리는 이걸 usePreloadedQuery로 바꿀 것이다. 그렇지만, preloaded query는 variable들은 실제로 query가 fetch될 때 결정된다(컴포넌트가 렌더링되어지기 전에). 그래서 variable들 대신, 해당 hook은 query 결과를 불러오기 위해 필요한 정보를 포함하는 query reference를 가져온다. Query reference는 Step 2에서 query fetch할 때 생성된다.

바뀐 컴포넌트는 다음과 같다


import {usePreloadedQuery} from 'react-relay';
import type {PreloadedQuery} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as QueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';

export default function PosterDetailsHovercardContents({
  queryRef,
}: {
  queryRef: PreloadedQuery<QueryType>,
}): React.ReactElement {
  const data = usePreloadedQuery(
    PosterDetailsHovercardContentsQuery,
    queryRef,
  );
  ...
}


Step 2: export the query for access from the parent component

우리는 부모 컴포넌트인 PosterByline를 수정해서 PosterDetailsHovercardContentsQuery 쿼리를 초기화할 것이다. 해당 작업을 하기 위해서는 query에 대한 reference를 생성해야한다. 그래서 아래와 같이 export 한다


export const PosterDetailsHovercardContentsQuery = graphql`...

Step 3: Call useQueryLoader in the parent component

이제 PosterDetailsHovercardContetns는 query ref를 바라본다. 우리는 해당 query ref를 생성하고 부모컴포넌트(PosterByline)로부터 내려보내야한다. 우리는 useQueryLoader Hook을 통해서 query ref를 생성한다. 이 Hook은 query fetch를 발생시킬 수 있는 함수를 반환한다.


import {useQueryLoader} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as HovercardQueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
import {PosterDetailsHovercardContentsQuery} from './PosterDetailsHovercardContents';

export default function PosterByline({ poster }: Props): React.ReactElement {
  ...
  const [
    hovercardQueryRef,
    loadHovercardQuery,
  ] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
  return (
   ...
    <PosterDetailsHovercardContents
      queryRef={hovercardQueryRef}
    />
   ...
  );
}


useQueryLoader Hook은 다음 두가지를 반환한다.

  • query ref는 usePreloadedQuery가 query 결과를 가져오기 위한 불분명한 정보의 조각이다.
  • loadHovercardQuery는 요청을 초기화하는 함수이다

Step 4: Fetch the query in the event handler

마침내, 우리는 카드가 보여질때 이벤트 핸들러가 발생되는 loadHovercardQuery를 호출한다. 운이좋게도 Hovercard 컴포넌트는 onBeginHover 이벤트가 존재한다.


export default function PosterByline({ poster }: Props): React.ReactElement {
  ...
  const [
    hovercardQueryRef,
    loadHovercardQuery,
  ] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
  function onBeginHover() {
    loadHovercardQuery({posterID: data.id});
  }
  return (
    <div className="byline">
      ...
      <Hovercard
        onBeginHover={onBeginHover}
        targetRef={hoverRef}>
        <PosterDetailsHovercardContents queryRef={hovercardQueryRef} />
      </Hovercard>
    </div>
  );
}

주의해야할 점은 query variable들이 요청을 초기화하는 곳에서 넘겨진다는 것이다

이 부분에서, 이전에 봤던 동작과 같다는 것을 알 수 있지만, 이 방식이 조금 더 빠르다는 것을 알 수 있을 것이다(Relay가 조금 더 일찍 query를 가져올 수 있기 때문이다)

Tip: 단순화하기 위해서 useLazyLoadQuery를 통해 query 사용법을 알려주었다. 그렇지만 실제로는 preloaded query가 보통 Relay에서 query를 사용하는 보편적인 방식이다. 왜냐하면 실사용에 있어서 성능을 극명하게 높일 수 있는 방법이기 때문이다. 적절한 방식으로 당신의 서버와 라우터 시스템에 통합이 완료되었다면, 클라이언트 코드를 다운로드하거나 실행하기도 전에 서버사이드에서 메인 query를 미리 로드할 수 있을 것이다.

요약

  • 초기 화면을 보여주기 위해서 필요한 모든 데이터는 하나의 쿼리로 합쳐져야하지만, 유저 상호작용에 필요한 후속 데이터는 secondary query를 통해서 다뤄질 수 있다
  • Query variable들은 query를 통해 서버로 정보를 넘겨준다
  • Query variable들은 field 인자들을 통해 넘겨진다
  • Preloaded query들은 항상 최선의 방식이다. 유저 상호작용을 위한 query라면, 이벤트 핸들러에서 fetch를 초기화하면 된다. 화면을 그리기 위한 초기 query를 위해서라면, 특정 라우팅 시스템 안에서 가능한 빨리 fetch를 초기화하면 된다. lazy-loaded query는 오직 빠른 프로토타이핑이 필요할때만 사용하고, 그렇지 않다면 절대 사용하지 말아야한다

다음 시간에는 다른 타입의 hover card를 핸드링 함으로써 기존 hovercard 컴포넌트를 강화시키는 방식을 알아볼 것이다. 그 후, 초기 query가 추후 다른 variable을 이용해서 업데이트 및 refetch될 수 있도록 하는 방법을 알아볼 것이다.