Relay with Next.js: setting

Copied!
June 02, 2021 ·   17 mins to readRelay with Next.js: setting article
Notice

이 글은 독자가 GraphQL과 React에 대한 기본적인 이해가 있다고 가정한다.

작년 GraphQL 서버를 도입하면서 web application에서 사용할 client를 고민했었다. Apollo ClientRelay가 그 고민의 대상들이었는데, 최종적으로 Apollo Client를 선택했다.

Relay 자체를 학습하고 이해하는데 예상보다 많은 시간이 걸릴 것 같다는 생각이 들었고, GraphQL 조차 생소하게 받아들였던 팀원들이 이를 부담스러워한 탓도 있었다. 결정적으로는 Relay의 사용 당위성과 정해진 시간 안에 끝낼 수 있겠다는 확신이 없었기 때문에 그분2를 설득하지 못한 본인의 탓이 컸다.

그렇게 Apollo Client를 이용해 web application에서 GraphQL API를 사용할 수 있게 만들었고, 이 경험을 토대로 새로운 프로젝트에도 잘 사용할 수 있었다. 하지만 사용하면서 뭔가 한마디의 말로 설명하기 힘든 아쉬움이 있었다. GraphQL을 사용하고 있지만 GraphQL답게 사용하지 못하고 있는 듯한 묘한 감정이었는데, Thinking in Relay라는 글을 읽어보고 다시 Relay에 대한 호기심이 생겼다. 그 호기심은 어쨌든 간단하게라도 직접 써보자 라는 생각까지 발전했다.

하지만 예상과 다르게 Next.js에 Relay를 사용할 수 있게 만들기까지가 쉽지 않았다. Relay라는 framework가 생소하기도 했고, 생소한 만큼 Next.js와 사용할 수 있게 초기 설정을 하는 데 시간이 조금 걸렸다. 생각보다 예제가 많이 없었고, 참고할만한 자료들도 조금 부족한 느낌이었다(검색을 잘못한 거라면 할 말은 없지만).

뒤돌아 생각해보면 크게 어려운 부분이 있지는 않았으나, 처음 프로젝트를 설정할 때의 난감함을 잊지 않기 위해 글을 남기려고 한다. 혹시나 Relay와 Next.js를 함께 도입하려는 분들이 있으시면 이 글이 조금이나마 도움이 되었으면 좋겠다.

이 글에서는 Relay, Next.js와 GitHub API v4를 이용해 간단한 화면을 만들어 보고자 한다.

Thinking in Relay?

Thinking in Relay의 도입부에서 data fetching에 대한 Relay에 접근방식은 Facebook이 React를 통해 얻은 경험에서 영감을 받았음을 밝히고 있다. 그들이 어떤 고민을 하였고, 어떤 노력을 하였는지 GraphQL, Relay 등에 관심이 없더라도 한 번쯤 읽어보면 좋을 것 같다.

1. Relay?

Facebook에서 만든, data 기반의 React application을 구축하기 위한 JavaScript framework이다. Relay의 GitHub repository에 소개된 Relay의 특징은 다음과 같다.

  • Declarative: Never again communicate with your data store using an imperative API. Simply declare your data requirements using GraphQL and let Relay figure out how and when to fetch your data.
  • Colocation: Queries live next to the views that rely on them, so you can easily reason about your app. Relay aggregates queries into efficient network requests to fetch only what you need.
  • Mutations: Relay lets you mutate data on the client and server using GraphQL mutations, and offers automatic data consistency, optimistic updates, and error handling.

먼저, 필요한 데이터를 요청하는 것을 명령하지 말고 선언하라 고 한다. 말 그대로 “어떻게” 데이터를 가져올 것인지 표현하지 말고, “어떠한” 데이터가 필요한지만 명시하라는 뜻이다.

또한, 선언된 query가 관계있는 view와 가까이 위치하기 때문에 각 component의 기능을 쉽게 추론할 수 있는 장점이 생긴다고 한다. 딱 여기까지만 생각했을 때는 Apollo Client와 별 차이가 없는 듯 보이지만, Relay compiler의 존재가 그 차이를 만들어 낸다. Relay compiler가 만들어낸 아티팩트를 통해, query를 효율적으로 모아서 필요한 데이터만 가져올 수 있다.

2. Next.js application 생성

우선 Next.js 공식문서에 있는 create-next-app을 이용해 새로운 Next.js application을 만든다. 새로운 프로젝트가 문제없이 생성되었다면 TypeScript 설정을 해준다. Next.js에 TypeScript를 설정하는 방법 역시 문서에 잘 나와 있다.

$ yarn create next-app relay-next-test
$ cd relay-next-test
$ yarn add -D typescript @types/react @types/node
$ touch tsconfig.json
$ yarn dev

3. Relay 설정

1. Relay 의존성 설치

Relay를 활용하는데 필요한 패키지들을 모두 설치해준다.

$ yarn add react-relay relay-runtime
$ yarn add -D relay-compiler relay-config babel-plugin-relay graphql @types/react-relay @types/relay-runtime

각 패키지의 역할은 다음과 같으며, 자세한 설명은 Relay:Architecture Overview에 나와 있다.

  • react-relay

    • Relay와 React를 연결해주는 integration layer 모듈
  • relay-runtime

    • Relay core 모듈
  • relay-compiler

    • 사전 컴파일을 위한 모듈
  • babel-plugin-relay

    • GraphQL을 런타임 아티팩트로 만들어 주기 위한 babel plugin
  • relay-config

    • babel-plugin-relay 및 relay-compiler에서 설정 파일을 사용할 수 있게 도와주는 모듈
  • graphql

2. Relay config

Relay compiler를 위한 설정을 추가한다. 위의 단계에서 relay-config를 설치했기 때문에 project root에 relay.config.js 파일을 만들어 주면 된다.

$ touch relay.config.js

그리고 기본적으로 아래와 같은 옵션을 설정해준다. 더 많은 옵션은 여기에서 확인할 수 있다.

// relay.config.js
module.exports = {
  src: '.',
  schema: './schema.graphql',
  exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/.next/**'],
  artifactDirectory: '__generated__',
}

src에는 컴파일 대상인 파일들의 경로를 설정해준다. Next.js의 기본적인 directory 구성을 따르기 위해서 현재 경로로 설정했다.

schema는 컴파일에 필요한 GraphQL API서버의 schema 파일이다. 보통 GraphQL 서버에 Introspection query를 요청한 결과로 자동 생성해야하기 때문에 직접 작성할 일은 없다. 아직 schema 파일을 만들지는 않았지만, 다음 단계에서 만들어 줄 것이기 때문에 미리 설정해 주었다.

exclude는 컴파일 시 제외해야 할 파일들의 경로, artifactDirectory는 컴파일된 아티팩트들을 모아주는 경로에 대한 설정이다.

그다음, GraphQL과 관련한 요소들을 런타임 아티팩트로 만들어 주기 위해 babel plugin을 설정해준다. Next.js의 babel 설정을 override 하기 위해 우선 .babelrc를 만들어 준 후, 다음과 같이 간단하게 설정하면 된다.

$ touch .babelrc
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": ["relay"]
}

3. Generate schema file

이제 relay.config.js에 설정한 대로 schema.graphql 파일을 자동으로 만들 수 있게 해야 한다. 일단 nodejs환경에서 GraphQL 서버에 요청을 보내야 하므로 isomorphic-fetch 패키지를 설치한다.

$ yarn add isomorphic-fetch
$ yarn add -D @types/isomorphic-fetch

위에 쓰여있듯, GitHub API v4를 이용할 것이기 때문에 일단 GitHub personal access token을 발급받아야 한다. 발급받는 방법은 이 문서를 참고하면 된다.

토큰을 정상적으로 발급받았다면 원하는 경로에 아래의 코드를 작성한다. 이 예시에서는 <PROJECT_ROOT>/scripts/generateSchema.js에 작성한다고 가정한다.

// <PROJECT_ROOT>/scripts/generateSchema.js
const fs = require('fs')
const path = require('path')
const fetch = require('isomorphic-fetch')
const { getIntrospectionQuery, buildClientSchema, printSchema } = require('graphql')

const generateSchema = async () => {
  try {
    const response = await fetch('https://api.github.com/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `bearer ${GITHUB_TOKEN}`,
      },
      body: JSON.stringify({
        query: getIntrospectionQuery(), // #1
      }),
    })

    const res = await response.json()
    const sdl = printSchema(buildClientSchema(res.data)) // #2
    const parentPath = path.join(__dirname, '../')
    fs.writeFileSync(`${parentPath}/schema.graphql`, sdl) // #3
  } catch (e) {
    console.error(e)
  }
}

generateSchema()

GitHub API에 Introspection query를 보내 서버가 지원하는 모든 schema에 대한 정보를 요청한다(#1). 그다음 buildClientSchema()함수를 이용해 서버에서 응답한 schema 정보로 GraphQLSchema instance를 만들고, printSchema()함수를 이용해 SDL로 바꿔준다(#2). 그리고 relay.config.jsschema property에 설정한 경로에 파일이 생성되도록 한다(#3).

4. Add npm scripts

이제 Relay를 사용하기 위한 기반설정이 어느 정도 끝났기 때문에, package.jsonscripts를 아래와 같이 설정한다.

// package.json
{
  "scripts": {
    "generate:schema": "node ./scripts/generateSchema.js",
    "relay": "relay-compiler",
    "dev": "yarn run relay && next dev",
    "build": "yarn run relay && next build",
    "start": "next start"
  }
}

그리고 정해진 경로에 schema.graphql파일이 잘 생성되는지 확인해본다.

$ yarn run generate:schema
yarn run v1.22.10
$ node ./scripts/generateSchema.js
✨  Done in 6.52s.

Project root에 schema.graphql 파일이 생성되었고, 내부를 확인해 보면 다음과 같은 schema 들이 만들어져 있을 것이다. 여기까지 확인했다면 거의 다 설정한 것과 마찬가지이다.

schema.graphql
"""
Autogenerated input type of AcceptEnterpriseAdministratorInvitation
"""
input AcceptEnterpriseAdministratorInvitationInput {
  """
  The id of the invitation being accepted
  """
  invitationId: ID!

  """
  A unique identifier for the client performing the mutation.
  """
  clientMutationId: String
}

# ...

4. Next.js에서 Relay 사용하기

이제 본격적으로 Next.js와 Relay를 연결할 차례가 되었다. 순서는 크게 다음과 같다.

  1. 실제로 GraphQL 서버와의 통신을 담당할 fetch function을 작성
  2. 작성한 fetch function을 이용해 RelayEnvironment를 설정
  3. RelayEnvironmentProvider를 통해 Next.js와 Relay를 연결

작성하는 코드 중 Relay와 관련한 코드들은 우선 <PROJECT_ROOT>/relay/ 아래에 모아두기로 하였다.

1. Set RelayEnvironment - fetch helper

GraphQL 서버에 request를 보낼 fetch helper를 작성한다.

// <PROJECT_ROOT>/relay/fetchGraphQL.ts
import fetch from 'isomorphic-fetch'
import type { Variables } from 'relay-runtime'

const fetchGraphQL = async (query: string, variables: Variables) => {
  const response = await fetch('https://api.github.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `bearer ${GITHUB_TOKEN}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  })

  return await response.json()
}

export default fetchGraphQL

전혀 복잡한 부분이 없는 코드이다. relay-runtime 패키지의 Network.create() 함수의 paramter type을 참고하여 작성하였다. GraphQL은 기본적으로 POST method를 이용하여 query와 variables를 body에 담아 요청하게 되어있다.

2. Set RelayEnvironment - Environment

그리고 relay-runtime 패키지의 Environment 클래스를 이용해 RelayEnvironment를 작성한다. Relay는 Environment instance를 생성할 때, 실제로 GraphQL 서버에 접근하는 방법을 개발자가 명시해 주어야 한다. 위에서 작성한 fetchGraphQL.ts를 Environment instance를 생성할 때 network property로 전달한다.

// <PROJECT_ROOT>/relay/relayEnvironment.ts
import { useMemo } from 'react'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
import type { FetchFunction } from 'relay-runtime'

import fetchGraphQL from './fetchGraphQL'

let relayEnvironment: Environment

const fetchRelay: FetchFunction = async (params, variables) => {
  console.log(`fetch query ${params.name} with ${JSON.stringify(variables)}`)
  return fetchGraphQL(params.text, variables)
}

const createEnvironment = () => {
  return new Environment({
    network: Network.create(fetchRelay),
    store: new Store(new RecordSource()),
  })
}

type InitialRecords = ConstructorParameters<typeof RecordSource>[number]
export const initEnvironment = (initialRecords?: InitialRecords) => {
  const environment = relayEnvironment ?? createEnvironment() // #1

  if (initialRecords) {
    environment.getStore().publish(new RecordSource(initialRecords)) // #2
  }

  if (typeof window === 'undefined') return environment // #3

  if (!relayEnvironment) {
    relayEnvironment = environment // #4
  }

  return relayEnvironment
}

export const useEnvironment = (initialRecords: InitialRecords) => {
  const relayEnvironment = useMemo(() => initEnvironment(initialRecords), [initialRecords])
  return relayEnvironment
}

이 코드는 상당 부분 vercel에서 제공하는 예제의 일부를 참고하여 작성하였다(사실 말이 참고지 거의 그대로 옮겨왔다). Apollo Client와 Next.js를 사용하는 예제와 크게 다른 부분은 없었다.

Application 전체적으로 하나의 RelayEnvironment를 유지하기 위해 let relayEnvironment: Environment 변수를 선언하고, initEnvironment() 함수를 통해 RelayEnvironment를 초기화하거나, 기존의 RelayEnvironment store에 새로운 값을 할당한다.

이미 만들어진 RelayEnvironment가 있는지 확인하여 있다면 그 값을 그대로 사용하고, 없다면 createEnvironment()로 새로운 RelayEnvironment를 만들어 준다(#1). 그러고 나서 Store에 초기화할 값이 있다면 store에 그 값을 할당한다(#2). 만약 Next.js application이 server side에서 빌드되고 있는 상황이라면 만들어진 RelayEnvironment를 그대로 return 한다(#3). 이미 만들어진 RelayEnvironment가 없다면 지금 시점에 만들어진 RelayEnvironment를 relayEnvironment변수에 할당한다(#4).

useEnvironment() hooks는 initEnvironment()함수의 실행 결과를 useMemo로 감싸 React component에 memoize된 RelayEnvironment를 전달하는 역할을 한다.

3. Add RelayEnvironment to _app.tsx

다음은 _app.tsx에 약간의 설정을 추가해야 한다. _app.tsx는 각 페이지를 초기화할 때 사용된다. 모든 페이지가 render 될 때 마다 실행되기 때문에 각 페이지에서 공통으로 적용되어야 하는 설정이나 동작을 설정할 수 있다. _app.tsx에 대한 자세한 설명은 이 문서를 참고하면 된다.

// <PROJECT_ROOT>/pages/_app.tsx
import '../styles/globals.css'
import { RelayEnvironmentProvider } from 'react-relay/hooks'
import { useEnvironment } from '../relay/relayEnvironment'

import type { AppProps } from 'next/app'

function App({ Component, pageProps }: AppProps) {
  const environment = useEnvironment(pageProps.initialRecords)
  return (
    <RelayEnvironmentProvider environment={environment}>
      <Component {...pageProps} />
    </RelayEnvironmentProvider>
  )
}

export default App

Preload 된 페이지로부터 relay store에 할당할 값을 initialRecords라는 이름으로 전달받아 RelayEnvironment를 초기화하고 이를 RelayEnvironmentProvider에 전달하는 간단한 로직이다.

4. GitHub API 붙여보기

이제 Next.js에서 Relay를 사용할 수 있는 준비가 모두 끝났기 때문에, 실제로 GraphQL API를 호출해 볼 것이다. 여기서는 GitHub API v4에서 제공하는 marketplaceListings query를 사용해볼 것이다.

1. index.tsx 작성

먼저 아래와 같이 index.tsx를 작성한다. 주의할 점 하나는, Relay는 operation name convention이 있고, 이를 어겼을 경우 컴파일되지 않는다. 예를 들어 아래처럼 코드를 작성하고 컴파일을 하게 되면,

// <PROJECT_ROOT>/pages/index.tsx
import { graphql } from 'react-relay'

const query = graphql`
  query testQuery($first: Int) {
    marketplaceListings(first: $first) {
      edges {
        node {
          id
          app {
            name
          }
          fullDescription
        }
      }
    }
  }
`

아래와 같은 에러 메시지를 보게 된다.

ERROR:

Parse error: Error: RelayFindGraphQLTags: Operation names in graphql tags must be prefixed with the module name and end in “Mutation”, “Query”, or “Subscription”. Got “testQuery” in module “pages”. in “pages/index.tsx”

Operation name은 <MODULE_NAME><원하는 이름><Mutation | Query | Subscription>이 되어야 한다는 뜻이다. index.tsx파일은 pages/index.tsx에 있기 때문에 pages_index_MarketplaceListings_Query로 작성했다. 여기서는 각각의 구분을 위해 중간에 _를 사용했다.

// <PROJECT_ROOT>/pages/index.tsx
import { fetchQuery, graphql } from 'react-relay'
import { initEnvironment } from '../relay/RelayEnvironment'

export default function Home() {
  return <div></div>
}

const query = graphql`
  query pages_index_MarketplaceListings_Query($first: Int) {
    marketplaceListings(first: $first) {
      edges {
        node {
          id
          app {
            name
          }
          fullDescription
        }
      }
    }
  }
`

export const getStaticProps = async () => {
  const environment = initEnvironment()
  try {
    const queryProps = await fetchQuery<any>(environment, query, {
      first: 20,
    }).toPromise()
    const initialRecords = environment.getStore().getSource().toJSON() // #1

    return {
      // #2
      props: {
        ...queryProps,
        initialRecords, // #3
      },
    }
  } catch (e) {
    console.error(e)
    throw e
  }
}

선언한 query를 getStaticProps() 내부에서 GitHub API 서버에 전송해야 하는데, 이때 fetchQuery 함수를 이용한다. fetchQuery 함수는 response 결과를 자동으로 relay store에 저장한다. Response에 이상이 없다면, 실제로 페이지가 rendering 될 때 relay store에 할당할 값을 만들기 위해 fetch 한 결과를 relay store에서 꺼내어 JSON으로 만든다(#1). 이 함수의 동작은 문서에서 확인하면 된다.

fetchQuery: behavior with .toPromise()에 보면 .toPromise()의 사용을 지양하라고 나와 있기 때문에 추후 .subscribe()를 이용하도록 코드를 수정해야 한다.

그렇게 query를 fetch 한 결과와 relay store에 할당할 값을 props란 이름의 object로 묶어서 return 한다(#2). 이때 relay store에 할당할 값의 property name을 initialRecords로 해야 _app.tsx에서 props로 받아서 사용할 수 있다(#3).

2. TypeScript로 Relay compile하기

이제 Relay compiler를 실행 시켜 런타임 아티팩트를 만들어야 한다.

$ yarn run relay

하지만 아무 일도 일어나지 않는다. 왜냐하면 기본적으로 Relay는 JavaScript - Flow로 프로젝트가 구성되어있음을 전제로 동작하기 때문이다. 그래서 TypeScript를 이용하려면 relay-compiler-language-typescript 패키지를 설치하고, relay.config.jslanguage: 'typescript'를 추가해 주어야 한다.

$ yarn add -D relay-compiler-language-typescript
// relay.config.js
module.exports = {
  src: '.',
  schema: './schema.graphql',
  exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/.next/**'],
  artifactDirectory: '__generated__',
  language: 'typescript',
}

다시 컴파일하면 정상적으로 잘 동작하는 것을 확인할 수 있다.

$ yarn run relay
yarn run v1.22.10
$ relay-compiler

Writing ts
Created:
 - pages_index_MarketplaceListings_Query.graphql.ts
Unchanged: 0 files
✨  Done in 2.63s.

relay.config.js에 설정한 artifactDirectory의 경로에 가서 확인해 보면 다음과 같은 파일이 만들어진 것을 확인할 수 있다.

generated 결과
// <PROJECT_ROOT>/__generated__/pages_index_MarketplaceListings_Query.graphql.ts
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck

import { ConcreteRequest } from 'relay-runtime'
export type pages_index_MarketplaceListings_QueryVariables = {
  first?: number | null
}
export type pages_index_MarketplaceListings_QueryResponse = {
  readonly marketplaceListings: {
    readonly edges: ReadonlyArray<{
      readonly node: {
        readonly app: {
          readonly name: string
        } | null
        readonly fullDescription: string
      } | null
    } | null> | null
  }
}
export type pages_index_MarketplaceListings_Query = {
  readonly response: pages_index_MarketplaceListings_QueryResponse
  readonly variables: pages_index_MarketplaceListings_QueryVariables
}

/*
query pages_index_MarketplaceListings_Query(
  $first: Int
) {
  marketplaceListings(first: $first) {
    edges {
      node {
        app {
          name
          id
        }
        fullDescription
        id
      }
    }
  }
}
*/

const node: ConcreteRequest = (function () {
  var v0 = [
      {
        defaultValue: null,
        kind: 'LocalArgument',
        name: 'first',
      },
    ],
    v1 = [
      {
        kind: 'Variable',
        name: 'first',
        variableName: 'first',
      },
    ],
    v2 = {
      alias: null,
      args: null,
      kind: 'ScalarField',
      name: 'name',
      storageKey: null,
    },
    v3 = {
      alias: null,
      args: null,
      kind: 'ScalarField',
      name: 'fullDescription',
      storageKey: null,
    },
    v4 = {
      alias: null,
      args: null,
      kind: 'ScalarField',
      name: 'id',
      storageKey: null,
    }
  return {
    fragment: {
      argumentDefinitions: v0 /*: any*/,
      kind: 'Fragment',
      metadata: null,
      name: 'pages_index_MarketplaceListings_Query',
      selections: [
        {
          alias: null,
          args: v1 /*: any*/,
          concreteType: 'MarketplaceListingConnection',
          kind: 'LinkedField',
          name: 'marketplaceListings',
          plural: false,
          selections: [
            {
              alias: null,
              args: null,
              concreteType: 'MarketplaceListingEdge',
              kind: 'LinkedField',
              name: 'edges',
              plural: true,
              selections: [
                {
                  alias: null,
                  args: null,
                  concreteType: 'MarketplaceListing',
                  kind: 'LinkedField',
                  name: 'node',
                  plural: false,
                  selections: [
                    {
                      alias: null,
                      args: null,
                      concreteType: 'App',
                      kind: 'LinkedField',
                      name: 'app',
                      plural: false,
                      selections: [v2 /*: any*/],
                      storageKey: null,
                    },
                    v3 /*: any*/,
                  ],
                  storageKey: null,
                },
              ],
              storageKey: null,
            },
          ],
          storageKey: null,
        },
      ],
      type: 'Query',
      abstractKey: null,
    },
    kind: 'Request',
    operation: {
      argumentDefinitions: v0 /*: any*/,
      kind: 'Operation',
      name: 'pages_index_MarketplaceListings_Query',
      selections: [
        {
          alias: null,
          args: v1 /*: any*/,
          concreteType: 'MarketplaceListingConnection',
          kind: 'LinkedField',
          name: 'marketplaceListings',
          plural: false,
          selections: [
            {
              alias: null,
              args: null,
              concreteType: 'MarketplaceListingEdge',
              kind: 'LinkedField',
              name: 'edges',
              plural: true,
              selections: [
                {
                  alias: null,
                  args: null,
                  concreteType: 'MarketplaceListing',
                  kind: 'LinkedField',
                  name: 'node',
                  plural: false,
                  selections: [
                    {
                      alias: null,
                      args: null,
                      concreteType: 'App',
                      kind: 'LinkedField',
                      name: 'app',
                      plural: false,
                      selections: [v2 /*: any*/, v4 /*: any*/],
                      storageKey: null,
                    },
                    v3 /*: any*/,
                    v4 /*: any*/,
                  ],
                  storageKey: null,
                },
              ],
              storageKey: null,
            },
          ],
          storageKey: null,
        },
      ],
    },
    params: {
      cacheID: 'fbc2c680e8078ef83e32b6227543aba2',
      id: null,
      metadata: {},
      name: 'pages_index_MarketplaceListings_Query',
      operationKind: 'query',
      text: 'query pages_index_MarketplaceListings_Query(\n  $first: Int\n) {\n  marketplaceListings(first: $first) {\n    edges {\n      node {\n        app {\n          name\n          id\n        }\n        fullDescription\n        id\n      }\n    }\n  }\n}\n',
    },
  }
})()
;(node as any).hash = 'db038a4ad6787233e5d1b2a65efccf55'
export default node

3. index.tsx에 type 선언하기

이제 정상적으로 index.tsx에 타입을 선언해 줄 수 있다. 먼저 getStaticProps 내부의 fetchQuery에 generic을 이용해 타입을 선언한다.

import type { pages_index_MarketplaceListings_Query } from '../__generated__/pages_index_MarketplaceListings_Query.graphql'

const queryProps = await fetchQuery<pages_index_MarketplaceListings_Query>(
  environment,
  query,
  {}
).toPromise()

그리고 Next.js에서 기본으로 제공하는 InferGetStaticPropsType을 이용하면 getStaticProps가 return 하는 값에 대한 타입 추론을 자동으로 할 수 있다.

import type { InferGetStaticPropsType } from 'next'

export default function Home({ marketplaceListings }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    // ...
  )
}

수정한 index.tsx의 코드는 아래와 같다.

import { fetchQuery, graphql } from 'react-relay'
import { initEnvironment } from '../relay/RelayEnvironment'

import type { InferGetStaticPropsType } from 'next'
import type { pages_index_MarketplaceListings_Query } from '../__generated__/pages_index_MarketplaceListings_Query.graphql'

export default function Home({
  marketplaceListings,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <div>
      <ul>
        {marketplaceListings.edges.map(({ node }) => (
          <li key={node.id}>
            <div>App name: {node.app?.name}</div>
            <div>Description: {node.fullDescription}</div>
          </li>
        ))}
      </ul>
    </div>
  )
}

const query = graphql`
  query pages_index_MarketplaceListings_Query($first: Int) {
    marketplaceListings(first: $first) {
      edges {
        node {
          id
          app {
            name
          }
          fullDescription
        }
      }
    }
  }
`

export const getStaticProps = async () => {
  const environment = initEnvironment()
  try {
    const queryProps = await fetchQuery<pages_index_MarketplaceListings_Query>(environment, query, {
      first: 20,
    }).toPromise()
    const initialRecords = environment.getStore().getSource().toJSON()

    return {
      props: {
        ...queryProps,
        initialRecords,
      },
    }
  } catch (e) {
    console.error(e)
    throw e
  }
}

이제 실행해보면 문제없이 동작할 것이다.

$ yarn run dev

5. Conclusion

Relay와 Next.js를 설치하고 두 framework를 사용해 간단히 GitHub API를 호출까지 해보았다. 위에도 쓰여 있듯 전부 해보고 돌아보니 그렇게 어려운 점은 없었다. 설정 자체는 올해 초 만들었던 서비스에서 Next.js와 Apollo Client를 사용했을 때와 비슷했기 때문에 크게 복잡하게 느껴지는 부분은 없었다.

아직 초기설정만 진행한 정도이기 때문에 Relay에 대한 소감을 말하기는 어려운 부분이 있다. 다만 Relay compiler가 Apollo Client를 사용했을 때 수동으로 관리했던 상당 부분을 자동으로 관리해 준다는 것이 좋았다.

다음 글에서는 Relay의 hooks와 GraphQL fragment를 활용하여 중요한 개념 중 하나인 data masking에 대해 알아보고자 한다.


참고


© 2024, Built with Gatsby