relay client-data

zmin·2022년 11월 15일
0

relay의 store는 서버로 요청한 데이터를 캐싱할 수 있는 것뿐만 아니라 클라이언트에서 정의한 데이터들도 저장할 수 있다.
전역 데이터를 생성하고 관리할 수 있다는 말이다.

  1. 서버의 스키마를 확장해서 사용할 수 있는 클라이언트 스키마 작성 가능
  2. 서버에 데이터를 요청하는 방식처럼 query를 이용하여 데이터 접근 가능
  3. 전역 데이터
"""server; local에 relay generate를 통해 생성되는 파일"""
type User {
	id
	name
    age
}

"""server; local에 relay generate를 통해 생성되는 파일"""
extend type User {
	school: String!
}

또는 클라이언트에서 새로 정의한 타입도 사용할 수 있다.

enum School {
	HighSchool
    MiddleSchool
    ElementarySchool
}
extend type School {
	school: School!
}

클라이언트 스키마를 새로 작성했다면 configuration 파일에 schemaExtensions을 추가해서 사용한다

schemaExtensions: ['~~~/~~~.graphql']

local data mutation은 commitLocalUpdate라는 함수를 이용하여 사용할 수 있다.

commitLocalUpdate는 relay environment와 updater를 파라미터로 가진다.

Query {
	users: [User!]!
}
// 아마도 따로 작성해뒀을 relay Environment
import Environment from '~~~/~~~'

useEffect(()=>{
  commitLocalUpdate(Environment, (store)=>{
    const root = store.getRoot();

    /* 아래는 어떤 데이터를 만드는 것이다.
      어떤 필드에 연결된 것이 아니라 그저
      '공중에 둥둥 떠다니는 User타입의 데이터'라고 이해하면 된다.*/
    const newData = store.create(`정말유니크한key`, 'User');

    newData.setValue('HighSchool', 'school');
    newData.setValue('zmin9', 'name');
    newData.setValue( 5, 'age');

    // 위에서 생성한 데이터를 실제 필드에 연결한다.
    root?.setLinkedRecords([newData], 'users');
  });
},[]);

전역 데이터를 첫 로드와 동시에 지정하고 싶다면 위와 같이 해줄 수 있겠다.

로직에 의해서 데이터를 추가해야하면 위와 같은 코드를 함수로 작성해두고 필요할 때마다 호출해서 데이터를 변경할 수 있다.

하지만 위와같이 작성하면 users에 작성된 데이터는 계속 덮어씌워지기 때문에 기존에 있던 데이터를 유지하면서 새로운 데이터를 이어 붙이고 싶다면 아래처럼 작성한다.

const data = store.getRoot().getLinkedRecord('users');

const newData = ... ;

root?.setLinkedRecords([...data, newData], 'users');

update도 create와 동일한데 한가지 다른점은 위에서 정의한 정말유니크한key의 dataID가 필요하다는 점이다.
dataID를 이용하여 데이터에 접근해서 setValue(primitive, primitive[] data), setLinkedRecord(s)(record data)를 해서 해당 필드에 대한 값을 업데이트한다.

const data = store.get(dataID);

delete도 dataID를 이용하여 진행한다.

store.delete(dataID)

해당 dataID를 가지는 데이터는 삭제했지만 이를 참조하고 있는 field가 있을 수 있기 때문에 해당 데이터를 필터링해서 field를 업데이트 해줘야한다.

이말은 delete를 문제없이 제대로 하려면 이 데이터가 어느 필드에 연결되어있는 데이터인지 알아야한다는 것이다.


저장되는 데이터들도 서버의 캐시와 동일하게 취급되기 때문에 참조하는 곳이 없으면 GC의 수거 대상이 된다. 컴포넌트에서 hook을 이용하여 렌더링되는 데이터들의 경우 참조하고 있는 것을 relay가 알고있기 때문에 retain되지만 그렇지 않은 데이터들은 사실상 참조되고 있지 않다고 판단하기 때문에 GC의 대상이 된다.
그래서 직접 retain 시켜줘야함

import {createOperationDescriptor, getRequest} from 'relay-runtime';

const localDataQuery = graphql`...`;

// 컴포넌트에서 여러 hook을 통해 요청을 보내는 것이 아니기 때문에 
// 수동으로 request를 설정하고 operation을 만들어서
// 직접 retain 시킨다
const request = getRequest(localDataQuery);
const operation = createOperationDescriptor(request, {});
const disposable = environment.retain(operation);

// 더이상 retain하지 않아도 되면 dispose한다.
disposable.dispose();

위와 같이 사용해서 전역 데이터를 사용하지 못하는 것은 아니다. 하지만 날 것의 데이터를 사용하게 되기 때문에 실제 우리가 편히 사용할 수 있도록 데이터를 한 번 걸러주는 것이 필요한데 이 역할을 하는 것이 resolver이다.

너무 졸리고 배고파서 일단 공식문서 예제를 그대로 가져왔다

/**
 * @RelayResolver
 *
 * @onType User
 * @fieldName greeting
 * @rootFragment UserGreetingResolver
 *
 * A greeting for the user which includes their name and title. 는 이제 idle 같은 곳에서 hover시 description으로 뜬다. 편리하게 이용가능
 */
export default function userGreetingResolver(userKey: UserGreetingResolver$key): string {
  const user = readFragment(graphql`
    fragment UserGreetingResolver on User {
      last_name
    }`, userKey);

  return `Hello ${user.last_name}!`;
}

resolver를 정의한 곳 위에는 공식문서의 예시처럼 docblock을 적어줘야한다. 그래야 complier가 이를 돌면서 resolver임을 캐치할 수 있다.

docblock에서 명시해준 필드명을 이용하여 클라이언트 데이터처럼 사용할 수 있다.

query Query {
	User {
    	greeting
    }
}

쿼리를 보내면 응답으로 Hello Choi가 올 것이다.

위에서 작성했듯이 fragment로 작성하기 때문에 argument를 선언하고 받을 수 있는데 이때 오직 하나의 arg만 받을 수 있다.

  1. The resolver function must accept a single argument, which is the key for its root fragment.
  2. The resolver function must be the default export of its module (only one resolver per module)
  3. The resolver must read its fragment using the special readFragment function.
  4. The resolver function must be pure
  5. The resolver’s return value must be immutable
/**
* @RelayResolver
* @filedName **my_resolver_field**
* @onType **MyType**
* @rootFragment myResolverFragment
*/
function myResolver(key) {
   const data = readFragment(graphql`
       fragment myResolverFragment on MyType
            @argumentDefinitions(**my_arg**: {type: "Float!"}) {
            field_with_arg(arg: $my_arg) {
               __typename
            }
       }
   `, key);

   return data.field_with_arg.__typename;
}

resolver function에 직접 arg를 넘겨줄 수도 있는데 이때는 docblock의 @fieldName 에 같이 작성해줘야한다.

/**
 * @filedName **my_resolver_field(my_arg: String, my_other_arg: Int)**
**/

function myResolver(key, arg){...}

resolver에서도 캐싱된 값을 이용하게 되는데 fragment가 그렇듯 내부 필드 값이 변경되면 알아서 reevaluate를 진행한다. 다시말해 데이터 변경을 알아서 감지하고 변경된 데이터를 알아서 반영한다는 소리다.

profile
308 Permanent Redirect

0개의 댓글