Rendering List Data and Pagination (part 2)

Relay 02/18

Updating Connections

connection์„ ๋ Œ๋”๋ง ํ•˜๋ ค๊ณ  ํ• ๋•Œ, ์‚ฌ์šฉ์ž์˜ ๋™์ž‘์— ๋”ฐ๋ผ ์•„์ดํ…œ์„ connection์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ๋‹ค. ์ด์ „ Updating Data ์„น์…˜์—์„œ Relay๋Š” ์ •๊ทœํ™”๋œ ๋กœ์ปฌ in-memory ์ €์žฅ์†Œ๋ฅผ ๊ฐ–๊ณ  ์žˆ๊ณ , ๊ทธ ์ €์žฅ์†Œ์—๋Š” ID๋กœ ๊ตฌ๋ถ„๋˜๋ฉฐ ์ •๊ทœํ™”๋œ GraphQL ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜์—ˆ๋‹ค.

Relay๋ฅผ ํ†ตํ•ด mutation, subscription ํ˜น์€ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•  ๊ฒฝ์šฐ์—” ๋ฌด์กฐ๊ฑด ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๊ณ  ์ฝ๊ณ  ์“ฐ๋Š” ๋‚ด์šฉ์„ ํฌํ•จํ•˜๋Š” updater ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ด ์ œ๊ณตํ•ด์•ผ ํ–ˆ๋‹ค. ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๊ฒฝ์šฐ, ์—…๋ฐ์ดํŠธ๋œ ๋ฐ์ดํ„ฐ์— ์˜ํ–ฅ์„ ๋ฐ›๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์€ ์ „๋ถ€ ์ด๋ฅผ ์•Œ์•„์ฐจ๋ ค ๋‹ค์‹œ ๋ Œ๋”๋ง๋œ๋‹ค.

Connection Records

Relay์—์„œ @connection ์œผ๋กœ ๋งˆํ‚น๋œ connection ํ•„๋“œ๋Š” ์ €์žฅ์†Œ ๋‚ด๋ถ€์— ํŠน๋ณ„ํ•œ ๋ ˆ์ฝ”๋“œ๋กœ์„œ ์ €์žฅ๋˜๊ณ , ์—ฌํƒœ๊นŒ์ง€ fetch๋œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋“ค์„ ์ €์žฅํ•ด๋‘”๋‹ค. connection์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” connection key ๋ฅผ ํ†ตํ•ด connection ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด key๋Š” @connection ์„ ์–ธ์„ ํ•  ๋•Œ ์ œ๊ณต๋œ๋‹ค. ํŠนํžˆ, key ๋Š” ConnectionHandler API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ updater ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ connection์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜์˜ fragment๋Š” @connection ์„ ์–ธ์ด ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— updater ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ connection ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

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

const storyFragment = graphql`
  fragment StoryComponent_story on Story {
    comments @connection(key: "StoryComponent_story_comments_connection") {
      nodes {
        body {
          text
        }
      }
    }
  }
`;

Accessing connections using __id

connection์˜ __id ํ•„๋“œ๋ฅผ ์ฟผ๋ฆฌํ•œ ํ›„, ์ด๋ฅผ ํ†ตํ•ด ์ €์žฅ์†Œ์˜ ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

const fragmentData = useFragment(
  graphql`
    fragment StoryComponent_story on Story {
      comments @connection(key: "StoryComponent_story_comments_connection") {
        # __id ํ•„๋“œ๋ฅผ ์ฟผ๋ฆฌํ•œ๋‹ค.
        __id

        # ...
      }
    }
  `,
  props.story,
);

// connection์˜ ๋ ˆ์ฝ”๋“œ id๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
const connectionID = fragmentData?.comments?.__id;

์ดํ›„ connectionID ๋ฅผ ํ†ตํ•ด ์ €์žฅ์†Œ์˜ ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

function updater(store: RecordSourceSelectorProxy) {
  // connectionID is passed as input to the mutation/subscription
  const connection = store.get(connectionID);

  // ...
}

์ฃผ์˜ : GraphQL API๊ฐ€ __id ํ•„๋“œ๋ฅผ ๋…ธ์ถœ์‹œํ‚ฌ ํ•„์š”๋Š” ์—†๋‹ค. __id ๋Š” Relay๊ฐ€ connection ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•œ ๊ฒƒ์ด๋‹ค.

Accessing connections using ConnectionHandler.getConnectionID

๋งŒ์•ฝ connection์„ ๊ฐ–๊ณ  ์žˆ๋Š” ๋ถ€๋ชจ ๋ ˆ์ฝ”๋“œ์˜ connection ID์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ConnectionHandler.getConnectionID API๋ฅผ ํ†ตํ•ด connection ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  // Get the connection ID
  const connectionID = ConnectionHandler.getConnectionID(
    storyID, // passed as input to the mutation/subscription
    'StoryComponent_story_comments_connection',
  );

  // Get the connection record
  const connectionRecord = store.get(connectionID);

  // ...
}

Accessing connections using ConnectionHandler.getConnection

๋งŒ์•ฝ connection์„ ๊ฐ–๊ณ  ์žˆ๋Š” ๋ถ€๋ชจ ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ConnectionHandler.getConnection API์™€ ๊ทธ ๋ถ€๋ชจ ๋ ˆ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด connection ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  // Get parent story record
  // storyID is passed as input to the mutation/subscription
  const storyRecord = store.get(storyID);

  // Get the connection record from the parent
  const connectionRecord = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
  );

  // ...
}

Adding edges

connection์— edge๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ช‡๊ฐ€์ง€ ๋Œ€์•ˆ๋“ค์ด ์žˆ๋‹ค.

Using declarative directives

๋ณดํ†ต mutation์ด๋‚˜ subscription์˜ ํŽ˜์ด๋กœ๋“œ๋Š” ๋‹จ์ผ edge๋‚˜ edge๋“ค์˜ ๋ฆฌ์ŠคํŠธ์™€ ํ•จ๊ป˜ ์„œ๋ฒ„์—์„œ ์ถ”๊ฐ€๋œ ์ƒˆ๋กœ์šด edge๋“ค์„ ๋…ธ์ถœ์‹œํ‚จ๋‹ค. ๋งŒ์•ฝ mutation ํ˜น์€ subscription์ด response์—์„œ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” edge ํ˜น์€ edge๋“ค์„ ๋…ธ์ถœ์‹œํ‚จ๋‹ค๋ฉด, ์ƒˆ๋กœ ์ƒ์„ฑ๋œ edge๋ฅผ ์ง€์ •๋œ connection์— ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด @appendEdge ํ˜น์€ @prependEdge ์„ ์–ธ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋˜๋Š” mutation์ด๋‚˜ subscription์˜ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ํ•˜๋‚˜์˜ node๋‚˜ node๋“ค์˜ ๋ฆฌ์ŠคํŠธ์™€ ํ•จ๊ป˜ ์„œ๋ฒ„์—์„œ ํ•„๋“œ๋กœ ์ถ”๊ฐ€๋œ ์ƒˆ๋กœ์šด ๋…ธ๋“œ๋“ค์„ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ๋‹ค. ์œ„์—์„œ edge๋ฅผ ๋…ธ์ถœ์‹œํ‚จ๊ฒƒ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, mutation์ด๋‚˜ subscription์ด ์‘๋‹ต์—์„œ ์ƒˆ๋กœ์šด ๋…ธ๋“œ๋‚˜ ๋…ธ๋“œ๋“ค์˜ ํ•„๋“œ๋ฅผ ๋…ธ์ถœ์‹œํ‚จ๋‹ค๋ฉด @appendNode ํ˜น์€ @prependNode ์„ ์–ธ์„ ํ†ตํ•ด ์ƒˆ๋กœ ์ง€์ •๋œ ๋…ธ๋“œ๋“ค์„ ์ง€์ •๋œ connection์— ์ถ”๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

์ด ์„ ์–ธ์€ connections ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋Š”๋ฐ, ์ด๋Š” connection ID๋“ค์„ ๋ฐฐ์—ด๋กœ ๊ฐ–๊ณ  ์žˆ๋Š” GraphQL ๋ณ€์ˆ˜์ด๋‹ค. Connection ID๋“ค์€ connection์˜ __id ํ•„๋“œ์—์„œ ์–ป์–ด์ง€๊ฑฐ๋‚˜ ConnectionHandler.getconnectionID API๋ฅผ ํ†ตํ•ด ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

@appendEdge / @prependEdge

์ด ์„ ์–ธ๋“ค์€ ํ•˜๋‚˜์˜ edge ํ˜น์€ ์—ฌ๋Ÿฌ edge๋“ค์ด ๋‹ด๊ธด ๋ฆฌ์ŠคํŠธ์—์„œ ๋™์ž‘ํ•œ๋‹ค. @prependEdge ๋Š” ์„ ํƒ๋œ edge๋“ค์„ ๊ฐ connection๋“ค์˜ ๋งจ ์•ž์— ์ถ”๊ฐ€์‹œํ‚ค๊ณ , @appendEdge ๋Š” ์„ ํƒ๋œ edge๋“ค์„ ๊ฐ connection๋“ค์˜ ๋งจ ๋’ค์— ์ถ”๊ฐ€์‹œํ‚จ๋‹ค.

  • ์ธ์ž

    • connections : connection ID๋“ค์˜ ๋ฐฐ์—ด.

    • edgeTypeName : node๋ฅผ ํฌํ•จํ•˜๋Š” edge์˜ ํƒ€์ž… ์ด๋ฆ„. ConnectionHandler.createEdge ์˜ edge ํƒ€์ž… ์ธ์ž์™€ ๋™์ผํ•˜๋‹ค.

// Get the connection ID using the `__id` field
const connectionID = fragmentData?.comments?.__id;

// Or get it using `ConnectionHandler.getConnectionID()`
const connectionID = ConnectionHandler.getConnectionID(
  '<story-id>',
  'StoryComponent_story_comments_connection',
);

// ...

// Mutation
commitMutation<AppendCommentMutation>(environment, {
  mutation: graphql`
    mutation AppendCommentMutation(
      # Define a GraphQL variable for the connections array
      $connections: [ID!]!
      $input: CommentCreateInput
    ) {
      commentCreate(input: $input) {
        # Use @appendNode or @prependNode on the node field
        feedbackCommentNode @appendNode(connections: $connections, edgeTypeName: "CommentsEdge") {
          id
        }
      }
    }
  `,
  variables: {
    input,
    // Pass the `connections` array
    connections: [connectionID],
  },
});

Manually adding edges

์œ„์˜ ์„ ์–ธ๋ฌธ์€ connection์—์„œ ํ•ญ๋ชฉ๋“ค์„ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€ ํ˜น์€ ์ œ๊ฑฐํ•  ํ•„์š”์„ฑ์„ ํฌ๊ฒŒ ์ œ๊ฑฐํ•˜์ง€๋งŒ, ์„ฌ์„ธํ•œ ์ œ์–ด๋Š” ๋ถˆ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์‚ฌ๋ก€๋ฅผ ์ถฉ์กฑ์‹œํ‚ค์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ๋‹ค.

connection์„ ์ˆ˜์ •ํ•˜๋Š” updater๋ฅผ ์ž‘์„ฑํ•˜๋ ค๋ฉด connection ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. connection ๋ ˆ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด connection์— ์ถ”๊ฐ€ํ•˜๋ ค๋Š” ์ƒˆ edge์— ๋Œ€ํ•œ ๋ ˆ์ฝ”๋“œ๋„ ํ•„์š”ํ•˜๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ mutation ํ˜น์€ subscription์˜ ํŽ˜์ด๋กœ๋“œ์—๋Š” ์ถ”๊ฐ€๋œ ์ƒˆ๋กœ์šด edge๊ฐ€ ํฌํ•จ๋œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ ์ƒˆ edge๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ mutation์˜ response์—์„œ ์ƒˆ๋กœ ์ƒ์„ฑ๋œ edge๋ฅผ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

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

const createCommentMutation = graphql`
  mutation CreateCommentMutation($input: CommentCreateData!) {
    comment_create(input: $input) {
      comment_edge {
        cursor
        node {
          body {
            text
          }
        }
      }
    }
  }
`;
  • ์ƒˆ๋กœ์šด edge๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด cursor ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค๋Š” ์ ์„ ์ฃผ๋ชฉํ•ด์•ผ ํ•œ๋‹ค. ๋ฌด์กฐ๊ฑด ํ•„์š”ํ•œ ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ cursor ๊ธฐ๋ฐ˜์˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ด๋ผ๋ฉด ํ•„์š”ํ•˜๋‹ค.

updater ๋‚ด๋ถ€์—์„œ๋Š” Relay์˜ ์ €์žฅ์†Œ API์™€ mutation ์‘๋‹ต์„ ์ด์šฉํ•ด edge์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);
  const connectionRecord = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
  );

  // Get the payload returned from the server
  const payload = store.getRootField('comment_create');

  // Get the edge inside the payload
  const serverEdge = payload.getLinkedRecord('comment_edge');

  // Build edge for adding to the connection
  const newEdge = ConnectionHandler.buildConnectionEdge(
    store,
    connectionRecord,
    serverEdge,
  );

  // ...
}
  • mutation ํŽ˜์ด๋กœ๋“œ๋Š” store.getRootField ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์ฝ์–ด์™€ ์œ„ ์ €์žฅ์†Œ์—์„œ ๋ฃจํŠธ ํ•„๋“œ๋กœ์จ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค. ์ด ์˜ˆ์‹œ์—์„œ๋Š” comment_create ๋ฅผ ์ฝ๊ณ  ์žˆ๋‹ค.

  • ์œ„ ์˜ˆ์‹œ์—์„œ๋Š” ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ ๋ฐ›์•„์˜จ edge์— ConnectionHandler.buildConectionEdge ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ์ƒˆ edge๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์ƒˆ edge๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด์„œ ConnectionHandler.createEdge ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);
  const connectionRecord = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
  );

  // Create a new local Comment record
  const id = `client:new_comment:${randomID()}`;
  const newCommentRecord = store.create(id, 'Comment');

  // Create new edge
  const newEdge = ConnectionHandler.createEdge(
    store,
    connectionRecord,
    newCommentRecord,
    'CommentEdge', /* GraphQl Type for edge */
  );

  // ...
}

ํ•œ๋ฒˆ ์ƒˆ edge์˜ ๋ ˆ์ฝ”๋“œ๋ฅผ ์–ป์–ด์˜จ ์ดํ›„๋กœ๋Š” ConnectionHandler.insertEdgeAfter ํ˜น์€ ConnectionHandler.insertEdgeBefore ๋ฅผ ์ด์šฉํ•ด ์ƒˆ edge๋ฅผ connection์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);
  const connectionRecord = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
  );

  const newEdge = (...);

  // Add edge to the end of the connection
  ConnectionHandler.insertEdgeAfter(
    connectionRecord,
    newEdge,
  );

  // Add edge to the beginning of the connection
  ConnectionHandler.insertEdgeBefore(
    connectionRecord,
    newEdge,
  );
}

Removing edges

Using the declarative deletion directive

edge๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š”๊ฒƒ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ @deleteEdge ์„ ์–ธ์„ ํ†ตํ•ด edge๋ฅผ connection์œผ๋กœ๋ถ€ํ„ฐ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค. mutation์ด๋‚˜ subscription์ด response์—์„œ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์‚ญ์ œ๋œ ๋…ธ๋“œ์˜ ID๊ฐ€ ์žˆ๋Š” ํ•„๋“œ๋ฅผ ๋…ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ @deleteEdge ์ง€์‹œ๋ฌธ์„ ์ด์šฉํ•ด connection์—์„œ ๊ฐ edge๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

@deleteEdge

ID ํ˜น์€ [ID] ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” GraphQL ํ•„๋“œ์—์„œ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋™์ผํ•œ id ๋ฅผ ๊ฐ€์ง„ ์—์ง€๋ฅผ ํฌํ•จํ•˜๋Š” ๋…ธ๋“œ๋“ค์„ connections ๋ฐฐ์—ด์—์„œ ์‚ญ์ œํ•œ๋‹ค.

  • ์ธ์ž

    • connections : connection ID๋“ค์˜ ๋ฐฐ์—ด

// Get the connection ID using the `__id` field
const connectionID = fragmentData?.comments?.__id;

// Or get it using `ConnectionHandler.getConnectionID()`
const connectionID = ConnectionHandler.getConnectionID(
  '<story-id>',
  'StoryComponent_story_comments_connection',
);

// ...

// Mutation
commitMutation<DeleteCommentsMutation>(environment, {
  mutation: graphql`
    mutation DeleteCommentsMutation(
      # Define a GraphQL variable for the connections array
      $connections: [ID!]!
      $input: CommentsDeleteInput
    ) {
      commentsDelete(input: $input) {
        deletedCommentIds @deleteEdge(connections: $connections)
      }
    }
  `,
  variables: {
    input,
    // Pass the `connections` array
    connections: [connectionID],
  },
});

Manually removing edges

ConnectionHandler ๋Š” ConnectionHandler.deleteNode ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด connection์œผ๋กœ๋ถ€ํ„ฐ์˜ edge ์‚ญ์ œ๋ฅผ ์ง€์›ํ•œ๋‹ค.

const {ConnectionHandler} = require('RelayModern');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);
  const connectionRecord = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
  );

  // Remove edge from the connection, given the ID of the node
  ConnectionHandler.deleteNode(
    connectionRecord,
    commentIDToDelete,
  );
}
  • ์—ฌ๊ธฐ์„œ ConnectionHandler.deleteNode ๋Š” ์ฃผ์–ด์ง„ node ์˜ ID๋ฅผ ํ†ตํ•ด edge๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ์ฆ‰, ์–ด๋–ค node๊ฐ€ ์ฃผ์–ด์ง„ id์— ํ•ด๋‹นํ•˜๋Š” edge๋ฅผ ๊ฐ–๊ณ  ์žˆ๋Š”์ง€๋ฅผ ํ›‘์–ด๋ณด๊ณ , ๊ทธ edge๋ฅผ ์‚ญ์ œํ•œ๋‹ค.

Connection identity with filters

connection์„ ์„ ์–ธํ•  ๋•Œ filter ์ธ์ž๋ฅผ ์ „๋‹ฌํ•˜๋ฉด filter์— ์‚ฌ์šฉ๋œ ๊ฐ’๋“ค์€ connection ์‹๋ณ„์ž์˜ ์ผ๋ถ€๊ฐ€ ๋œ๋‹ค. ์ฆ‰, Relay ์ €์žฅ์†Œ์—์„œ connection์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ comments ํ•„๋“œ๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธ์ž๋ฅผ ๋ฐ›๋Š”๋‹ค๊ณ  ํ•˜์ž.

const {graphql} = require('RelayModern');

const storyFragment = graphql`
  fragment StoryComponent_story on Story {
    comments(
      order_by: $orderBy,
      filter_mode: $filterMode,
      language: $language,
    ) @connection(key: "StoryComponent_story_comments_connection") {
      edges {
        nodes {
          body {
            text
          }
        }
      }
    }
  }
`;

comments ํ•„๋“œ๋Š” $orderBy, $filterMode, $language ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์œผ๋ฉฐ comments ๋ฅผ ์ฟผ๋ฆฌํ•  ๋•Œ ์‹๋ณ„์ž๋กœ์„œ ๋™์ž‘ํ•œ๋‹ค. ์ดํ›„ connection ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์œ„ ๊ฐ’๋“ค์„ ๋„˜๊ฒจ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

๊ฐ™์€ ์ด์œ ๋กœ ConnectionHandler.getConnection ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•  ๋•Œ๋„ ์„ธ๋ฒˆ์งธ ์ธ์ž๋กœ ์œ„ ๊ฐ’๋“ค์„ ๋„˜๊ฒจ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

const {ConnectionHandler} = require('RelayModern');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);

  // Get the connection instance for the connection with comments sorted
  // by the date they were added
  const connectionRecordSortedByDate = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
    {order_by: '*DATE_ADDED*', filter_mode: null, language: null}
  );

  // Get the connection instance for the connection that only contains
  // comments made by friends
  const connectionRecordFriendsOnly = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
    {order_by: null, filter_mode: '*FRIENDS_ONLY*', langugage: null}
  );
}

์ธ์ž๋กœ ๋„˜๊ฒจ์ค€ ๊ฐ ๋ณ€์ˆ˜๋“ค์˜ ์กฐํ•ฉ์ด ํ•„ํ„ฐ๋กœ ์ž‘์šฉํ•ด ๋‹ค๋ฅธ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•”์‹œํ•œ๋‹ค.

connection์„ ์—…๋ฐ์ดํŠธ ํ•˜๋ ค๊ณ  ํ•  ๊ฒฝ์šฐ ๊ทธ ์—…๋ฐ์ดํŠธ์— ์˜ํ–ฅ์„ ๋ฐ›๋Š” ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๋“ค์„ ์—…๋ฐ์ดํŠธ ํ•ด์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ƒˆ ๋Œ“๊ธ€์„ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•˜๋ ค ํ•  ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์˜ ์นœ๊ตฌ๋กœ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด์ง„ ๋Œ“๊ธ€์ด ์•„๋‹ˆ๋ผ๋ฉด FRIENDS_ONLY connection์— ๋Œ“๊ธ€์ด ์ถ”๊ฐ€ ๋˜์–ด์„œ๋Š” ์•ˆ๋œ๋‹ค.

const {ConnectionHandler} = require('relay-runtime');

function updater(store: RecordSourceSelectorProxy) {
  const storyRecord = store.get(storyID);

  // Get the connection instance for the connection with comments sorted
  // by the date they were added
  const connectionRecordSortedByDate = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
    {order_by: '*DATE_ADDED*', filter_mode: null, language: null}
  );

  // Get the connection instance for the connection that only contains
  // comments made by friends
  const connectionRecordFriendsOnly = ConnectionHandler.getConnection(
    storyRecord,
    'StoryComponent_story_comments_connection',
    {order_by: null, filter_mode: '*FRIENDS_ONLY*', language: null}
  );

  const newComment = (...);
  const newEdge = (...);

  ConnectionHandler.insertEdgeAfter(
    connectionRecordSortedByDate,
    newEdge,
  );

  if (isMadeByFriend(storyRecord, newComment) {
    // Only add new comment to friends-only connection if the comment
    // was made by a friend
    ConnectionHandler.insertEdgeAfter(
      connectionRecordFriendsOnly,
      newEdge,
    );
  }
}

์—ฌ๋Ÿฌ ํ•„ํ„ฐ๋“ค์„ ์ด์šฉํ•ด connection๋“ค์„ ๊ด€๋ฆฌํ•œ๋‹ค. ๊ทธ์น˜๋งŒ ๋‹จ์ˆœํžˆ ๋ช‡๊ฐœ์˜ ํ•„ํ„ฐ ์กฐํ•ฉ๋งŒ์œผ๋กœ ๋ณต์žก๋„๊ฐ€ ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•˜๋Š” ๋ฌธ์ œ์ ์ด ์žˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Relay๋Š” ๋‘๊ฐ€์ง€ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค.

  1. ์–ด๋–ค ํ•„ํ„ฐ๊ฐ€ connection ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉ๋˜๋Š”์ง€ ์ •ํ™•ํ•˜๊ฒŒ ํŠน์ •ํ•œ๋‹ค.

    • ํŽ˜์ด์ง€๋„ค์ด์…˜์— ์“ฐ์ด๋Š” ํ•„ํ„ฐ๋งŒ @connection ์„ ์–ธ์„ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•œ๋‹ค.

    const {graphql} = require('relay-runtime');
    
    const storyFragment = graphql`
      fragment StoryComponent_story on Story {
        comments(
          order_by: $orderBy
          filter_mode: $filterMode
          language: $language
        )
          @connection(
            key: "StoryComponent_story_comments_connection"
            filters: ["order_by", "filter_mode"]
          ) {
          edges {
            nodes {
              body {
                text
              }
            }
          }
        }
      }
    `;
    • language๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์— ์‚ฌ์šฉ๋˜๋Š” ํ•„ํ„ฐ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— @connection ๋‚ด๋ถ€์— ๋„ฃ์–ด์ฃผ์ง€ ์•Š์•˜๋‹ค.

    • ๊ฐœ๋…์ ์œผ๋กœ๋Š” ์–ด๋–ค ์ธ์ž๋งŒ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ connection์˜ ๊ฒฐ๊ณผ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š”์ง€ ์ •ํ•ด์ค€ ๊ฒƒ์ด๋‹ค. ๋งŒ์•ฝ ์–ด๋–ค ์ธ์ž๊ฐ€ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ connection์˜ ๊ฒฐ๊ณผ๋‚˜ ์ •๋ ฌ๋ฐฉ์‹์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋นผ๋„ ๊ดœ์ฐฎ์€ ์ธ์ž๋ผ๋Š” ๋œป์ด๋‹ค. ์œ„ ์˜ˆ์‹œ์—์„œ๋Š” language ๊ฐ€ ๊ทธ๋ ‡๋‹ค.

    • ์•ฑ์ด ์‹คํ–‰๋จ์— ์žˆ์–ด ์–ด๋–ค ๋ถ€๋ถ„๋„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ์ธ์ž๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•„ํ„ฐ์—์„œ ์ง€์›Œ๋„ ์•ˆ์ „ํ•˜๋‹ค๋Š” ๋œป์ด ๋œ๋‹ค.

  2. ๋” ์‰ฌ์šด ๋Œ€์•ˆ์€ ์•„์ง ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค.

Advanced Pagination

์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” usePaginationFragment ๋ฅผ ์ด์šฉํ•œ ๊ณ ๊ธ‰ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณธ๋‹ค.

Pagination Over Multiple Connections

๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ connection๋“ค์„ ์ด์šฉํ•ด ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” usePaginationFragment ๋ฅผ ์—ฌ๋Ÿฌ๋ฒˆ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

import type {CombinedFriendsListComponent_user$key} from 'CombinedFriendsListComponent_user.graphql';
import type {CombinedFriendsListComponent_viewer$key} from 'CombinedFriendsListComponent_viewer.graphql';

const React = require('React');

const {graphql, usePaginationFragment} = require('react-relay');

type Props = {
  user: CombinedFriendsListComponent_user$key,
  viewer: CombinedFriendsListComponent_viewer$key,
};

function CombinedFriendsListComponent(props: Props) {

  const {data: userData, ...userPagination} = usePaginationFragment(
    graphql`
      fragment CombinedFriendsListComponent_user on User {
        name
        friends
          @connection(
            key: "CombinedFriendsListComponent_user_friends_connection"
          ) {
          edges {
            node {
              name
              age
            }
          }
        }
      }
    `,
    props.user,
  );

  const {data: viewerData, ...viewerPagination} = usePaginationFragment(
    graphql`
      fragment CombinedFriendsListComponent_user on Viewer {
        actor {
          ... on User {
            name
            friends
              @connection(
                key: "CombinedFriendsListComponent_viewer_friends_connection"
              ) {
              edges {
                node {
                  name
                  age
                }
              }
            }
          }
        }
      }
    `,
    props.viewer,
  );

  return (...);
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋˜๊ธฐ๋Š” ํ•˜์ง€๋งŒ Relay์—์„œ๋Š” ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋‹น ํ•˜๋‚˜์˜ connection์„ ์ด์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.

Bi-directional Pagination

Pagination ์„น์…˜์—์„œ ์–ด๋–ป๊ฒŒ usePaginationFragment ๋ฅผ ์‚ฌ์šฉํ•ด ์ผ๋ฐฉ์ ์ธ ๋ฐฉํ–ฅ(forward)์˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์•˜๋‹ค. ํ•˜์ง€๋งŒ connection์€ ์—ญ๋ฐฉํ–ฅ(backward) ํŽ˜์ด์ง€๋„ค์ด์…˜๋„ ์ง€์›ํ•œ๋‹ค. forward์™€ backward๋Š” connection์˜ ๋‚ด์šฉ์ด ์–ด๋–ป๊ฒŒ ์ •๋ ฌ๋˜๋Š”์ง€๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์ž๋ฉด โ€œforwardโ€๋Š” ์ตœ์‹ ์ˆœ, โ€œbackwardโ€๋Š” ์˜ค๋ž˜๋œ ์ˆœ์ด ๋  ๊ฒƒ์ด๋‹ค.

๋ฐฉํ–ฅ์˜ ์˜๋ฏธ์™€๋Š” ๋ณ„๊ฐœ๋กœ, Relay๋Š” usePaginationFragment ๋ฅผ ์ด์šฉํ•ด ์—ญ๋ฐฉํ–ฅ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์œ„ํ•œ API๋ฅผ ๋ณ„๋„๋กœ ์ง€์›ํ•˜๊ธฐ๋„ ํ•œ๋‹ค. before, last ๋Š” after, first ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋œ๋‹ค.

import type {FriendsListComponent_user$key} from 'FriendsListComponent_user.graphql';

const React = require('React');
const {Suspense} = require('React');

const {graphql, usePaginationFragment} = require('react-relay');

type Props = {
  userRef: FriendsListComponent_user$key,
};

function FriendsListComponent(props: Props) {
  const {
    data,
    loadPrevious,
    hasPrevious,
    // ... forward pagination values
  } = usePaginationFragment(
    graphql`
      fragment FriendsListComponent_user on User {
        name
        friends(after: $after, before: $before, first: $first, last: $last)
          @connection(key: "FriendsListComponent_user_friends_connection") {
          edges {
            node {
              name
              age
            }
          }
        }
      }
    `,
    userRef,
  );

  return (
    <>
      <h1>Friends of {data.name}:</h1>
      <List items={data.friends?.edges.map(edge => edge.node)}>
        {node => {
          return (
            <div>
              {node.name} - {node.age}
            </div>
          );
        }}
      </List>

      {hasPrevious ? (
        <Button onClick={() => loadPrevious(10)}>
          Load more friends
        </Button>
      ) : null}

      {/* Forward pagination controls can go simultaneously here */}
    </>
  );
}
  • ๋‹จ์ˆœํžˆ ๋ช…๋ช…ํ•˜๋Š” ๋ฐฉ์‹๋งŒ ๋‹ค๋ฅธ๊ฒƒ์ด๊ณ  โ€œforwardโ€, โ€œbackwardโ€๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋ฐ”๋Š” after, first๋ฅผ ์ด์šฉํ•œ ๋ฐฉ์‹๊ณผ ์™„์ „ํžˆ ๊ฐ™๋‹ค.

  • โ€œforwardโ€, โ€œbackwardโ€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ์ „์ œ๋Š” usePaginationFragment ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ โ€œforwardโ€์™€ โ€œbackawardโ€๋Š” ๋™์‹œ์— ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ๋‹ค.

Custom Connection State

๊ธฐ๋ณธ์ ์œผ๋กœ usePaginationFragment ์™€ @connection ์„ ์‚ฌ์šฉํ•  ๋•Œ, Relay๋Š” โ€œforwardโ€์ธ ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ connection์— ๋’ค์— ์ถ”๊ฐ€ํ•˜๊ณ , โ€œbackwardโ€์ธ ๊ฒฝ์šฐ ์ƒˆ ํŽ˜์ด์ง€๋ฅผ ์•ž์— ์ถ”๊ฐ€ํ•œ๋‹ค. ์ฆ‰, ์ปดํฌ๋„ŒํŠธ๋Š” ํ•ญ์ƒ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ†ตํ•ด ์ถ•์ ๋œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์™€, mutation์ด๋‚˜ subscription์„ ํ†ตํ•ด ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์ˆ˜์ •๋œ ๊ฒฐ๊ณผ๋กœ์„œ์˜ connection์„ ๋ Œ๋”๋งํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋ณ‘ํ•ฉํ•˜๊ณ  ์ถ•์ ์‹œํ‚ฌ ๋•Œ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ฐ”๋ž„ ์ˆ˜๋„ ์žˆ๊ณ , ๋กœ์ปฌ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ connection์— ๋ฐ˜์˜์‹œํ‚ค๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ๋‹ค.

  • connection์˜ visible slice๋‚˜ window๊ฐ€ ๋‹ฌ๋ผ์ง€๋Š” ๊ฒƒ์„ ์ถ”์ ํ•˜๋ ค ํ•  ๋•Œ

  • ์‹œ๊ฐ์ ์œผ๋กœ ํŽ˜์ด์ง€๋ฅผ ๋ถ„๋ฆฌํ•˜๋ ค ํ•  ๋•Œ. ์ด ๋•Œ๋Š” ๊ฐ ํŽ˜์ด์ง€์— ์ •ํ™•ํžˆ ์–ด๋–ค ์•„์ดํ…œ๋“ค์ด ๋“ค์–ด๊ฐ€์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์ง€์‹์ด ํ•„์š”ํ•˜๋‹ค.

  • ๋™์ผํ•œ connection์˜ ์„œ๋กœ ๋‹ค๋ฅธ โ€œ๋"(๋งˆ์ง€๋ง‰ ๋…ธ๋“œ)์„ ํ‘œ์‹œํ•˜๋ฉด์„œ ๊ทธ๋“ค ์‚ฌ์ด์˜ gap์„ ์ถ”์ ํ•˜๊ณ , gap ์‚ฌ์ด์˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๊ฒฐ๊ณผ๋ฅผ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

    • ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋Œ“๊ธ€์ด ๋งจ ์œ„์— ํ‘œ์‹œ๋˜๋Š” ๋Œ“๊ธ€ ๋ชฉ๋ก์„ ๋ Œ๋”๋งํ•˜๋ ค ํ•˜๊ณ , ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋งค๊ธฐ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ƒํ˜ธ์ž‘์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” gap์ด ํ•„์š”ํ•˜๋‹ค.

์ด๋Ÿฐ ๋ณต์žกํ•œ ์‚ฌ์šฉ์‚ฌ๋ก€๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Relay์—์„œ๋Š” ๊ฐœ๋ฐœ์ด ๊ณ„์† ์ง„ํ–‰์ค‘์ด๋‹ค.

Refreshing connections

Prefetching pages of a Connection

Rendering One Page of Items at a Time

Last updated