teklog

Relay는 왜 사용할까?

2022/10/19

n°18

category : Relay

img


Relay는 react, graphql을 개발한 meta(facebook)에서 출시한 graphql 라이브러리이다. graphql의 데이터 상태관리를 위해 사용한다. 여러가지 강력한 기능과 편의성을 제공한다. 앞서 살펴본 graphql을 프론트 환경에서 실질적으로 구현하기 위해 필요하다. 장단점을 통해 relay 사용의 이점을 간략히 파악하고, 기본적인 개념들을 잡아보자.


글 마지막엔 처음 relay를 공부하면서 봤던 포스트들의 링크를 달아둘 예정이니, 혹여나 이 글을 보시는 분들은 더 훌륭한 글들을 먼저 보시길 추천드린다.


목차


  • 장점
  • 단점
  • Relay의 기본 요소, 훅
  • 읽을 거리들.



Relay의 장점


처음 사용하긴 했지만, 사용하면서 느낀 좋은 점들이 정말 많았다. 순수 js, html, css를 사용하다 리액트를 처음 접했을 때만큼의 임팩트가 있었다. 특히 react와 함께 사용하면 rest api를 사용할 때 발생할 수 있는 많은 문제를 방지할 수 있을 것 같다.


  • 컴파일러가 있다. graphql 템플릿 리터럴의 에러를 미리 잡을 수 있고, 컴파일 에러 메시지가 구체적이다.
  • 컴파일러를 통해 artifact를 생성하고, 그곳에 저장된 타입을 import하여 데이터 타입을 거의 자동으로 지정할 수 있다.
  • 성능 최적화와 관련된 강력한 훅이 많다. (캐싱 관련)
  • 비동기 함수로 가져오는 데이터의 렌더링 방식을 세밀하게 설정할 수 있다 (optimistic UI, preload, lazyload 등..)
  • graphql의 설계의도에 충실할 수 있다. (예를 들면 useFragment훅을 이용한 data masking)



Relay의 단점


  • 진입 장벽이 확실히 높다.
  • 레퍼런스가 부족한 편이다.


graphql과 마찬가지로 아직 많은 단점을 찾지는 못했다. 비슷한 기능을 공유하는 Apollo나 SWR을 graphql과 함께 사용해본다면 그 차이를 더 분명히 느낄 수 있을 것같다. 한가지 첨언하자면, apollo client나 swr보다 확실히 진입장벽이 있다는 점. 처음 적응할 때 새로운 프레임워크를 학습하는 정도의 난이도가 있었다.


학습에 큰 비용이 들 수 있으나, 그럼에도 relay를 사용하는 것은 엄청나다. 프론트 개발에서 undefined, null 에러가 지겹고, api 명세서를 일일히 봐가며 데이터의 타입 지정해야하는 것이 귀찮고, useState를 이용해 요청을 보내고, 응답을 받아 또다시 state로 ui를 업데이트 해야하는 게 불완전하게 느껴진다면 훌륭한 대안이 될 수 있다. 성능을 개선하는 것 뿐만 아니라, 불필요한 코드를 상당 부분 줄여줄 수 있다.



Relay의 기본 요소 & Hooks


  • Relay Compiler


The Relay Compiler will analyze any graphql literals inside your Javascript code, and produce a set of artifacts that are used by Relay at runtime, when the application is running on the browser. - Relay docs


릴레이를 통해 graphql을 사용하기 위해선 런타임 이전에 릴레이 컴파일을 해야한다. 그 결과로 query에 대응하는 artifact 파일들이 생성된다. "릴레이 컴파일러는 js 코드 내의 모든 graphql 리터럴을 분석하고, 이를 바탕으로 브라우저가 실행될 때 런타임에서 사용할 artifact 세트를 생성한다". (일반적으로 __generated__ 폴더에 저장된다.) artifact는 릴레이 컴파일러가 실행될 때마다 생성되어 프로젝트에서 변경된 gql을 반영한다. graphql 태그 자체가 런타임에서 실행되는 것이 아니다. 컴파일러가 생성한 artifact를 기반으로 relay-runtime이 실행되어 graphql이 작동할 수 있다.


개인적으로 컴파일러를 실행하여 쿼리 내부 타입을 미리 검증하고, 컴파일 이후 생성된 artifact에서 데이터의 타입을 바로 가져올 수 있는 점이 상당히 혁신적으로 다가왔다. TS를 사용한다면 types.ts에 무수히 많은 인터페이스와 타입 등을 작성했어야 했을 것이다. 반복적일 뿐만 아니라, ts가 던지는 컴파일 에러가 무시되고 런타임이 실행되는 경우도 종종 있기에 이 작업이 무의미해질 때도 있다.



  • Relay Store


The Relay Store can be used to programmatically update client-side data inside updater functions. The following is a reference of the Relay Store interface.


릴레이 스토어는 일반적인 상태관리툴의 저장소 store와 유사하다. 일반적인 상태관리 툴에서 상태를 변경하는 dispatch 기능이 updater 함수를 통해 구현된다. 다만 중요한 차이점은 릴레이 스토어는 모든 요청과 응답에 대한 데이터가 저장된다는 점이다. 또한 데이터의 상태를 변경하기 위해 복잡한 dispatch를 요구하지 않는다. 릴레이에서 모든 쿼리는 스토어를 거치기 때문이다. 이를 통해 불필요한 round trip을 방지할 수 있다.


덕분에 다양한 성능 최적화를 이룰 수 있다. 예를 들어 optimistic response와 usePreloadedQuery 같은 다양한 훅이 이 스토어를 기반으로 작동한다. optimistic response을 활용하면, 동일한 쿼리로 요청을 보내 스토어에 있는 값과 동일한 응답을 받을 것이라 예상된다면 아예 요청을 보내지 않는다. (Optimistic UI) 혹은 Suspense을 함께 사용하여 render as you fetch 방식으로 페이지를 로드할 수 있다. 즉 렌더링과 데이터 페칭을 동시에 구현할 수 있다. 뿐만 아니라 스토어에는 다양한 정보가 저장된다. 이를테면 query에 입력된 argument를 불러오거나 할 수 있다. 이처럼 스토어는 컴파일러와 더불어 릴레이의 강력한 기능의 기반이 된다고 할 수 있다.



  • Relay Environment


A Relay Environment instance to execute the request on. If you're starting this request somewhere within a React component, you probably want to use the environment you obtain from using useRelayEnvironment.


한마디로 relay environment는 요청을 실행할 instance이다. 이 때문에 일일히 요청 body에 무언가를 기입하지 않아도 된다.


1) RelayEnvironmentProvider


This component is used to set a Relay environment in React Context. Usually, a single instance of this component should be rendered at the very root of the application, in order to set the Relay environment for the whole application: - relay docs


relay를 사용하기 위해선 다른 상태관리 툴이 그렇듯, root 컴포넌트를 provider가 감싸주어야 한다. Next.js를 사용 중이라면 _app.tsx를 provider로 감싸주면 된다. 바로 import 하여 사용할 수 있는 swr과 차이가 나는 지점인 것 같다. provider, environment, store를 통해 조금 더 '상태관리'가 강조되는 느낌이다.


2) useRelayEnvironment


Hook used to access a Relay environment that was set by a RelayEnvironmentProvider:


environment에 직접 접근하기 위해 사용하는 훅이다. 생각보다 사용할 일이 많지 않았던 것 같다. environment는 relay의 초기 세팅이나 훅의 argument로 넣어서 더 자주 사용된 것 같다. (environment를 더 자세하게 알고 싶은데 공식 문서의 설명이 너무 부실하다 ㅠㅠ)

const React = require('React');


const {useRelayEnvironment} = require('react-relay');


function MyComponent() {
  const environment = useRelayEnvironment();


  const handler = useCallback(() => {
    // For example, can be used to pass the environment to functions
    // that require a Relay environment.
    commitMutation(environment, ...);
  }, [environment])


  return (...);
}


module.exports = MyComponent;


  • fetchQuery


If you want to fetch a query outside of React, you can use the fetchQuery function from react-relay:


query를 리액트 컴포넌트 밖에서 미리 호출하고 싶다면 fetchQuery를 사용할 수 있다. fetch-then-render (fetching을 끝내고 렌더) 방식에 적절한 것으로 보인다. Next.js의 SSR을 적용하고 있다면 서버 사이드에 fetchQuery를 작성해준다.


인자로는 environment (relay), query (graphql tag), variables(non-nullable 이기 때문에 argument가 필요없는 요청엔 빈 객체를 넣어야한다), networkCacheConfig(optional)이 들어간다.


fetchQuery will automatically save the fetched data to the in-memory Relay store, and notify any components subscribed to the relevant data.


결과로 instance를 반환한다. 이 instance는 응답 데이터를 담은 것으로 예상했으나, fetchQuery의 응답이 바로 JSON 형태의 res는 아니다. res 데이터를 얻기 위해선 fetchQuery의 반환값에 반드시 subscribe 혹은 toPromise 메서드를 호출해주어야 한다. 그리고 이 반환 값은 relay store에 저장된다.


To start the requestsubscribe or toPromise must be called on the observable. Exposes the following methods:


둘 중 하나가 호출되지 않으면, 실질적으로 요청이 실행되지 않는다고 한다. toPromise는 간단히 promise res 객체를 반환하는데 비해, subscribe는 store와 연관이 있는 것 같다. (GraphQL Subscriptions are a mechanism which allow clients to subscribe to changes in a piece of data from the server, and get notified whenever that data changes.) 더 자세히 알아봐야 할 것 같으니 우선 추후 업데이트 예정.


물론 클라이언트 사이드에서 바로 이 반환 값을 사용하진 않는다. useEnvironment 훅을 조합하여 사용할 수도 있으나, 클라이언트에선 useQueryLoader + loadQuery가 더 적절할 것 같다. SSR을 사용 중이라면, 굳이 response를 prop으로 내려 줄 필요가 없어진다. (진짜 신기함)



  • useQueryLoader


relay에서 기본적으로 요청을 보내기 위해 사용되는 훅이다. 처음엔 gql에 익숙하지 않아 엔드포인트가 하나이고, req body가 없는 이 요청이 무척 혼란스러웠다. ( loadQuery와 useQueryLoader가 불편하게 느껴진다면, 'relay-hooks'의 useQuery를 사용하는 것도 좋은 방법일 것 같다. )


인자로는 query가 들어간다. optional로 initialQueryRef가 들어가는데, 이는 usePreloadedQuery 훅을 사용하기 위해서 필요하다. preload 훅은 다음에 알아보기로 하고 useQueryLoader의 반환값을 살펴보자. 참고로 반환값은 배열 형태로 오기 때문에 이에 맞춰서 구조분해할당을 하여 사용한다.


Return Value of useQueryLoader


  • queryReference : object

useQueryLoader의 인자로 들어갔던 query에 대한 결과가 반환된다. nullable이다.


  • loadQuery : callback

useQuryLoader가 반환하는 상당히 중요한 콜백함수이다. 이 콜백함수를 통해 쿼리에 새로운 variable을 넣어 다시 요청할 수 있다. 예를 들어 검색 창에서 글자를 입력하여 새로운 결과를 얻고자 한다면 loadQuery의 인자로 새로운 값을 넣어 다시 요청을 날릴 수 있다. relay에서는 요청을 보내어 결과를 받는 과정을 query load라는 개념으로 접근하는 것 같다. loadQuery의 인자는 3가지가 있다.


  • variables!

non-nullable이다. query의 argument로 들어갈 값들을 객체 형태로 넣어준다. non-nullable이기 때문에 argument가 없는 query이라면 빈 객체를 전달해야 한다.


  • options : nullable


fetchPolicy :

Determines if cached data should be used, and when to send a network request based on the cached data that is currently available in the Relay store (for more details, see our Fetch Policies and Garbage Collection guides):


networkCacheConfig:

Default value: {force: true}. Object containing cache config options for the network layer. Note that the network layer may contain an additional query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely (which is the default behavior), pass {force: true} as the value for this option.


캐시 데이터의 재사용에 관한 설정이다. relay store에 저장된 값을 바탕으로 요청을 보낼지, 언제 보낼지 등 자세한 설정을 줄 수 있다. optimistic response 등 최적화 관련하여 활용해야 하기 때문에 공식문서를 읽어보시길 추천드린다.


  • disposeQuery : callback


queryReference에 있는 값을 null로 만드는 콜백함수이다. 검색을 예로 들면, 검색 결과를 비우고 싶을 때 이벤트 핸들러로 이 함수를 넣어주면 결과가 모두 사라질 것이다.



useFragment, usePreloadedQuery 훅의 사용법은 추후에 업데이트 예정입니다.



Relay에 관해 읽을 거리들


한국어 자료 위주로 추천드리는 글들.


목차 구성이 특이하게 되어있어서 파도파도 뭔가 계속 나온다 ㅎㅎ 설명은 부실하나 필요한 부분의 snippet이 있어서 빠르게 이해할 수 있다. 한 줄짜리 설명을 보면 좀 너무한다 생각이 들긴 한다.. (어떤 분께서 'relay 공식문서는 역순으로 되어있어요'라고 하셨는데 너무 deep해서 인상깊었다 ㅎㅎ)



relay 공식문서에 있는 graphql에 관한 글을 번역해주셨다. relay, graphql 모두 facebook에서 개발했기 대문에 더 공신력이 생기는 문서같다. 이 글을 읽으면서 react-gql-relay는 삼위일체가 아닐까 생각했다. 번역도 정말 잘해주셔서 귀한 자료다.



첫꼭지와 data masking 부분 밖에 못읽어봤지만 역시 좋은 글이다. 내가 번역해볼까도 싶다.



이 글보다 훨씬 좋은 relay 소개글. 개인적으로 공부한 내용 정리 + 공식문서 바탕으로 작성한 글이라, xiniha님이 작성한 이 글을 보시는 게 더 좋을 것 같다. 굳이 relay를 쓸 이유를 못찾으시겠다면 정말 추천드린다.



나처럼 문서만 보고 이해하지 못하는, 직접 예제를 작성해야 감을 잡으시는 분들에게 정말 추천드린다. 처음에 전혀 감을 잡지 못하고 있었을 때 가이드가 되준 글이다. 특히 이 글에서는 graphql + relay의 초기설정 부분을 언급하고 있지 않기 때문에, 이 글을 보시는 걸 추천드린다.



에러는 없으나, 그닥 좋은 코드는 아닙니다! 참고 바랍니다.



공부하면서 작성한 글이라 잘못된 내용이 있을 것 같다. leetekwoo@gmail로 피드백 주시면 감사하겠습니다!