relay 공식문서 읽으면서 이것저것 메모🥕

Relay 는?

Relay Compiler 라는놈이 있음

// UserComponent_user.graphql.ts
import { FragmentRefs } from "relay-runtime";

export type UserComponent_user$data = {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly " $fragmentType": "UserComponent_user";
};

export type UserComponent_user = UserComponent_user$data;
export type UserComponent_user$key = {
  readonly " $data"?: UserComponent_user$data;
  readonly " $fragmentRefs": FragmentRefs<"UserComponent_user">;
};

const node: ReaderFragment = {
  "argumentDefinitions": [],
  "kind": "Fragment",
  "metadata": null,
  "name": "UserComponent_user",
  "selections": [
    {
      "alias": null,
      "args": null,
      "kind": "ScalarField",
      "name": "id",
      "storageKey": null
    },
    {
      "alias": null,
      "args": null,
      "kind": "ScalarField",
      "name": "name",
      "storageKey": null
    },
    {
      "alias": null,
      "args": null,
      "kind": "ScalarField",
      "name": "email",
      "storageKey": null
    }
  ],
  "type": "User"
};

어떤식으로 데이터를 정규화하나용?

// 정규화되지 않은 구조
{
  user: {
    id: "1",
    name: "John",
    posts: [
      { id: "1", title: "Post 1", authorName: "John" },
      { id: "2", title: "Post 2", authorName: "John" }
    ]
  }
}

// 정규화된 구조
{
  users: {
    "1": { id: "1", name: "John" }
  },
  posts: {
    "1": { id: "1", title: "Post 1", authorId: "1" },
    "2": { id: "2", title: "Post 2", authorId: "1" }
  }
}

그래서 Relay 가 어떻게 다른데~

// UserProfile 컴포넌트
fragment UserProfile_user on User {
  name
  avatar
}

// UserPosts 컴포넌트
fragment UserPosts_posts on User {
  posts {
    title
    content
  }
}
// 컴파일러가 만드는 최종 쿼리
query UserPageQuery {
  user(id: $id) {
    ...UserProfile_user
    ...UserPosts_posts
  }
}

apollo 와 비교를 해보자!

간단히말하면 apollo 는 top-down 이고, relay 는 bottom-up 이라고 봐도 되는걸까?!
apollo는..

relay 는

cc. Fragment-Driven-Development

쿼리는 static 하다. (relay compiler 동작방식은..?!)

// 우리가 작성한 원본 쿼리
query UserQuery {
  user(id: "123") {
    name
    email
  }
}
// 실제 네트워크 요청시
{
  "id": "Query_USER_123",  // 쿼리 전체 텍스트 대신 ID만 전송
  "variables": {
    "userId": "123"
  }
}

Fragment 적용하기

// ParentComponent
const ParentComponent = () => {
  const data = useLazyLoadQuery(graphql`
    query ParentQuery {
      user {
        id
        name
        email      # 부모가 가진 모든 필드가
        avatar     # 자식에게 그대로 전달됨
        location
      }
    }
  `);

  return <ChildComponent data={data.user} />;
}

// ChildComponent
const ChildComponent = ({ data }) => {
  // data 안에 모든 필드가 있어서
  // 실제로 어떤 필드를 사용하는지 불명확
  return <div>{data.name}</div>;
}
// ParentComponent
const ParentComponent = () => {
  const data = useLazyLoadQuery(graphql`
    query ParentQuery {
      user {
        id
        ...ChildComponent_user
      }
    }
  `);

  return <ChildComponent user={data.user} />;
}

// ChildComponent
const ChildComponent = ({ user }) => {
  const data = useFragment(graphql`
    fragment ChildComponent_user on User {
      name    # 자신이 실제로 필요한 필드만
      avatar  # 명시적으로 선언
    }
  `, user);

  return <div>{data.name}</div>;
}

Fragment 에 대해서 조금만 더...

# 서버의 스키마 정의
type User {
  id: ID!
  name: String
  posts: [Post]
}

type Post {
  id: ID!
  title: String
  content: String
}

# 클라이언트의 Fragment 정의
fragment UserInfo on User {   # User 타입에 대한 Fragment
  name
}

fragment PostInfo on Post {   # Post 타입에 대한 Fragment
  title
  content
}

useFragment 훅은 어떤 역할을 하나?

// 컴파일 전
const UserProfile = () => {
  const data = useFragment(profileFragment, user);
  return <div>{data.name}</div>;
}

// 컴파일 후 (개념적 예시)
const UserProfile = () => {
  // fragment가 이미 최적화되고 ID로 변환됨
  const data = useFragment("Profile_fragment_id", user);
  return <div>{data.name}</div>;
}

fragment 에 인수 주입하기

const ImageFragment = graphql`
  fragment ImageFragment on Image
    @argumentDefinitions(
      width: {
        type: "Int",
        defaultValue: null
      }
      height: {
        type: "Int",
        defaultValue: null
      }
    )
  {
    url(
      width: $width,
      height: $height
    )
    altText
  }
`;
const StoryFragment = graphql`
  fragment StoryFragment on Story {
    title
    summary
    postedAt
    poster {
      ...PosterBylineFragment
    }
    thumbnail {
      ...ImageFragment @arguments(width: 400)
    }
  }
`;

https://github.com/moonheekim0118/relay-examples/commit/2a35d6eb2693ac336ca332347a7fa3c7b2add6b1
^ 자세한건 위의 예시를 보기를,,

GraphQL 에서의 배열 처리

type User {
  name: String        # 단일 값
  friends: [User]     # 배열 값 (대괄호로 표시)
}
{
  user {
    name      # 단일 값 조회
    friends   # 배열 값 조회
  }
}

캐싱 & Relay Store

missing field hanlder란?

// Query 1

query UserQuery {
  user(id: 4) {
    name
  }
}

// Query 2

query NodeQuery {
  node(id: 4) {
    ... on User {
      name
    }
  }
}
// 내부적으로 이런 식으로 저장
{
  "4": {  // ID 기반 캐싱
    "__typename": "User",
    "name": "John"
  }
}
// 첫 번째 쿼리로 데이터를 가져옴
query UserQuery {
  user(id: 4) { ... }  // 'user' 필드로 접근
}

// 나중에 다른 쿼리로 같은 데이터에 접근하려 함
query NodeQuery {
  node(id: 4) { ... }  // 'node' 필드로 접근
}

usePreloadQuery

useQueryLoader 라는 놈과 함께 사용됨

function ProfilePageWrapper() {
  const [queryRef, loadQuery] = useQueryLoader(ProfileQuery);

  // 라우트 진입 시점에 데이터 로드 시작
  useEffect(() => {
    loadQuery({id: '123'});
  }, []);

  if (!queryRef) return null;

  return (
    <Suspense fallback={<Loading />}>
      <ProfilePage queryRef={queryRef} />
    </Suspense>
  );
}

근데 이러면 부모-자식의 결합도가 높아지지 않나? 🤥

인터페이스와 다형성

# Actor는 인터페이스 (공통 특성을 정의)
interface Actor {
  name: String
  profilePicture: Image
  joined: DateTime
}

# Person과 Organization은 Actor를 구현한 구체적인 타입
type Person implements Actor {
  name: String            # Actor의 필드
  profilePicture: Image   # Actor의 필드
  joined: DateTime        # Actor의 필드
  location: Location      # Person만의 필드
}

type Organization implements Actor {
  name: String                    # Actor의 필드
  profilePicture: Image           # Actor의 필드
  joined: DateTime                # Actor의 필드
  organizationKind: OrgKind       # Organization만의 필드
}
# 기본적인 방법 (Actor의 공통 필드만 가져옴)
fragment BasicActorInfo on Actor {
  name
  joined
  profilePicture {
    ...ImageFragment
  }
}

# 타입별 특수 필드도 가져오고 싶을 때
fragment DetailedActorInfo on Actor {
  name
  joined
  profilePicture {
    ...ImageFragment
  }
  # Organization인 경우의 특수 필드
  ... on Organization {
    organizationKind
  }
  # Person인 경우의 특수 필드
  ... on Person {
    location {
      name
    }
  }
}

(기본문법) 쿼리문 내의 ...... on 의 차이점

query {
  node(id: "123") {
    ... on User {    # Type Refinement: 노드가 User 타입일 때만 적용
      name
      email
    }
    ... on Organization {
      name
      memberCount
    }
  }
}
fragment ActorInfo on Actor {
  name
  joined
}

query {
  node(id: "123") {
    ... on Actor {              # Type Refinement: 노드가 Actor일 때
      ...ActorInfo             # Fragment Spread: ActorInfo의 모든 필드를 포함
      ... on Organization {    # Type Refinement: Actor 중에서도 Organization일 때
        memberCount
      }
    }
  }
}

(기본문법) 쿼리문에 붙는 node 의 의미는!?

# 기본적인 Node 인터페이스
interface Node {
  id: ID!  # 모든 노드는 글로벌 고유 ID를 가짐
}

type User implements Node {
  id: ID!
  name: String
}

type Post implements Node {
  id: ID!
  title: String
}
# ID만 있으면 어떤 타입의 데이터든 조회 가능
query {
  node(id: "User:123") {
    id
    ... on User {
      name
    }
  }
  
  node(id: "Post:456") {
    id
    ... on Post {
      title
    }
  }
}
// 1. 백엔드에서 데이터를 처음 받아올 때
query {
  posts {
    edges {
      node {
        id    // 백엔드가 이미 "Post:1" 형태로 ID를 제공
        title
      }
    }
  }
}

// 2. 이 ID를 posterID로 사용
function PostHovercard({ posterID }: { posterID: string }) {
  // posterID는 이미 "Post:1" 형태
  const data = useLazyLoadQuery(PosterDetailsHovercardContentsQuery, {
    posterID // "Post:1"
  });
  // ...
}

(기본문법) 쿼리 문법

// 클라이언트에서 자유롭게 정할 수 있는 부분
query WhateverNameYouWant {    // 👈 이 쿼리 이름은 자유롭게 정할 수 있음
  topStories {                 // 👈 이건 백엔드 스키마에 있어야 함!
    id                         // 👈 이것도 백엔드 스키마에 있어야 함!
    title                      // 👈 이것도!
  }
}

// 다른 이름으로도 같은 쿼리 가능
query GetMainPageStories {     // 👈 다른 이름으로 해도 됨
  topStories {                 // 👈 하지만 이 구조는 백엔드와 일치해야!
    id
    title
  }
}