작성일 댓글 남기기

GraphQL의 프래그먼트

GraphQL **프래그먼트(Fragment)**는 쿼리에서 공통 필드를 재사용할 수 있도록 도와주는 기능입니다. 큰 쿼리나 여러 쿼리에서 동일한 필드를 반복적으로 요청할 때 코드 중복을 줄이고, 유지보수성을 높일 수 있는 중요한 기능입니다.

프래그먼트를 사용하면 여러 쿼리나 뮤테이션에서 자주 사용하는 필드들을 하나의 프래그먼트로 정의하고, 그 프래그먼트를 다른 쿼리 내에서 호출함으로써 일관성을 유지할 수 있습니다. 특히 큰 애플리케이션에서 여러 곳에서 동일한 데이터를 요청할 때 매우 유용합니다.

1. 프래그먼트 기본 개념

프래그먼트는 GraphQL 쿼리나 뮤테이션에서 중복되는 필드를 하나로 묶어, 여러 곳에서 재사용할 수 있도록 하는 일종의 모듈화된 쿼리 블록입니다.

기본 구조:

fragment FragmentName on TypeName {
field1
field2
field3
}
  • FragmentName: 프래그먼트의 이름입니다.
  • on TypeName: 프래그먼트를 적용할 GraphQL 타입을 명시합니다. 예를 들어, 특정 객체 타입(User, Post 등)에만 해당 필드를 사용할 수 있도록 지정합니다.
  • field1, field2, field3: 재사용할 필드 목록입니다.

2. 프래그먼트 사용 예시

1) 기본 프래그먼트 정의

다음은 User 타입에 대한 필드를 프래그먼트로 정의하는 예시입니다. id, name, email 같은 필드가 여러 쿼리에서 공통으로 사용될 경우, 이를 프래그먼트로 정의하고 재사용할 수 있습니다.

fragment UserInfo on User {
id
name
email
}

2) 프래그먼트 사용하기

프래그먼트는 쿼리에서 재사용할 수 있습니다. 예를 들어, 사용자 목록을 요청하는 쿼리와 특정 사용자를 요청하는 쿼리에서 동일한 id, name, email 필드를 사용하고 싶다면, 프래그먼트를 다음과 같이 적용할 수 있습니다.

사용자 목록을 가져오는 쿼리:

query {
users {
...UserInfo
}
}

특정 사용자를 가져오는 쿼리:

query {
user(id: 1) {
...UserInfo
}
}

두 쿼리 모두 ...UserInfo를 통해 id, name, email 필드를 요청합니다. 프래그먼트를 사용하면 코드 중복을 줄일 수 있고, 필드가 변경되었을 때 프래그먼트만 수정하면 모든 쿼리가 함께 반영됩니다.


3. 중첩 프래그먼트

프래그먼트는 다른 프래그먼트 안에서 사용할 수도 있습니다. 이를 통해 더 복잡한 구조에서도 재사용성을 극대화할 수 있습니다.

예시:

사용자(User)와 그 사용자가 작성한 게시물(Post)를 요청한다고 가정해 보겠습니다. 각 사용자는 여러 개의 게시물을 가지고 있고, 게시물 정보에도 공통 필드가 있을 수 있습니다.

UserInfo 프래그먼트:

fragment UserInfo on User {
id
name
email
posts {
...PostInfo
}
}

PostInfo 프래그먼트:

fragment PostInfo on Post {
id
title
content
}

쿼리:

query {
users {
...UserInfo
}
}

이 경우, UserInfo 프래그먼트 안에 PostInfo 프래그먼트를 중첩하여, 사용자 정보와 함께 게시물 정보를 가져옵니다.


4. 프래그먼트의 장점

1) 코드 중복 제거

프래그먼트를 사용하면 쿼리에서 중복되는 필드를 하나로 정의해 재사용할 수 있습니다. 동일한 필드를 여러 쿼리에서 중복 작성하는 대신, 프래그먼트를 한 번 정의해두고 모든 곳에서 재사용할 수 있기 때문에 코드가 간결해집니다.

2) 유지보수성 향상

프래그먼트를 사용하면 특정 필드가 변경되었을 때, 각 쿼리에서 필드를 일일이 수정할 필요 없이 프래그먼트만 수정하면 됩니다. 필드가 변경되면 그 프래그먼트를 사용하는 모든 쿼리에 자동으로 반영됩니다.

3) 큰 쿼리의 가독성 개선

대규모 애플리케이션에서는 쿼리가 복잡하고 커질 수 있습니다. 이때 프래그먼트를 사용하면 필드들을 모듈화하여 쿼리의 가독성을 높일 수 있습니다. 필요한 부분을 프래그먼트로 분리해 관리함으로써, 쿼리의 길이가 길어지는 문제를 해결할 수 있습니다.


5. 프래그먼트의 단점 및 고려사항

1) 오버페칭 가능성

프래그먼트는 재사용성을 극대화하기 위한 도구이지만, 때로는 필요하지 않은 데이터를 포함할 수 있습니다. 즉, 쿼리에서 오버페칭(over-fetching) 문제가 발생할 수 있습니다. 프래그먼트가 포함된 모든 필드를 항상 요청하는 구조이기 때문에, 일부 쿼리에서 불필요한 필드를 가져올 수 있습니다.

이를 방지하기 위해, 필요한 필드만 정확하게 가져오는지를 신경 써서 프래그먼트를 설계해야 합니다. 너무 많은 필드를 프래그먼트로 묶어두면, 각 쿼리에서 실제 필요한 데이터보다 많은 데이터를 요청할 수 있기 때문입니다.

2) 타입 의존성

프래그먼트는 특정 GraphQL 타입에 종속적입니다. 즉, on TypeName으로 정의된 타입에서만 프래그먼트를 사용할 수 있습니다. 만약 타입이 변경되거나 다른 타입에서 동일한 필드를 사용해야 한다면, 새로운 프래그먼트를 정의해야 할 수 있습니다.


6. 실습: 프래그먼트 사용 예시

예시 시나리오

  1. 사용자 정보와 그 사용자가 작성한 게시물 정보를 가져오는 쿼리에서 프래그먼트를 사용합니다.
  2. 사용자는 id, name, email 정보를 가지고 있고, 게시물은 id, title, content를 가지고 있습니다.

프래그먼트 정의:

graphql코드 복사# User 정보를 위한 프래그먼트
fragment UserInfo on User {
  id
  name
  email
}

# Post 정보를 위한 프래그먼트
fragment PostInfo on Post {
  id
  title
  content
}

쿼리 사용 예시:

query {
users {
...UserInfo
posts {
...PostInfo
}
}
}

이 쿼리는 각 사용자와 그 사용자가 작성한 게시물을 프래그먼트를 사용해 가져옵니다. ...UserInfo...PostInfo는 각각의 프래그먼트를 사용하여 정보를 요청합니다.


7. 프래그먼트가 유용한 상황

  • 반복적인 데이터 구조: 동일한 데이터를 여러 쿼리에서 반복해서 요청해야 할 때.
  • 공통 필드 관리: 데이터 모델이 바뀌었을 때, 여러 쿼리에서 중복된 필드를 수정할 필요 없이 프래그먼트만 수정할 수 있어 일관성을 유지할 수 있습니다.
  • 모듈화 필요: 큰 쿼리를 여러 프래그먼트로 분리해 가독성을 개선하고, 모듈화된 쿼리 구조를 만들 때 유용합니다.

결론

프래그먼트는 GraphQL 쿼리에서 코드 중복을 줄이고, 일관성을 유지하며, 쿼리의 가독성과 유지보수성을 높이는 중요한 도구입니다. 그러나, 너무 광범위한 필드를 프래그먼트로 묶어버리면 오버페칭 문제가 발생할 수 있으니, 실제로 필요한 필드를 신중하게 선택해 프래그먼트를 설계하는 것이 중요합니다.

작성일 댓글 남기기

GraphQL의 페이징

페이징(Pagination)은 많은 양의 데이터를 효율적으로 가져오기 위해 데이터를 여러 페이지로 나누어 클라이언트에 제공하는 방법입니다. GraphQL에서도 대량의 데이터를 다룰 때 페이징을 적용하여 서버와 클라이언트 간의 성능을 최적화할 수 있습니다.

GraphQL에서는 두 가지 주요 페이징 방법을 지원합니다:

  1. Offset-based pagination (오프셋 기반 페이징)
  2. Cursor-based pagination (커서 기반 페이징)

두 방식 모두 클라이언트가 데이터를 나누어 요청하고, 필요한 만큼만 받아올 수 있게 합니다. 각각의 방식에 대해 자세히 설명해볼게요.


1. Offset-based Pagination (오프셋 기반 페이징)

Offset-based pagination은 전통적인 REST API에서 흔히 사용하는 방식입니다. 이 방식은 페이지당 몇 개의 데이터를 가져올지 결정하고, 특정 **오프셋(offset)**을 기준으로 데이터를 요청하는 방식입니다.

1) 기본 개념

  • offset: 데이터를 몇 번째부터 가져올지 결정하는 숫자입니다.
  • limit: 한 번에 몇 개의 데이터를 가져올지 결정하는 숫자입니다.

2) 예시

사용자 목록을 페이지 단위로 가져오는 예를 들어볼게요.

GraphQL 쿼리:

query getUsers($offset: Int!, $limit: Int!) {
users(offset: $offset, limit: $limit) {
id
name
email
}
}

이 쿼리는 offsetlimit 값을 변수로 받아, 해당 페이지의 사용자 목록을 반환합니다. 예를 들어, offset: 0limit: 10이면 첫 번째 페이지의 10명의 사용자를 가져옵니다.

3) 서버 리졸버 예시 (Node.js)

const resolvers = {
Query: {
users: async (parent, { offset, limit }) => {
return await User.find().skip(offset).limit(limit);
}
}
};

이 리졸버에서는 skip()limit() 메서드를 사용해 MongoDB에서 해당 범위의 데이터를 가져옵니다.

장점:

  • 간단하고 이해하기 쉬운 방식입니다.
  • 기존의 SQL 데이터베이스 또는 NoSQL 데이터베이스에서 쉽게 사용할 수 있습니다.

단점:

  • 데이터가 많아질수록 오프셋이 커질 경우, 성능이 떨어질 수 있습니다. 특히 오프셋이 클 경우, 쿼리가 느려질 수 있습니다.
  • 데이터가 변경될 경우, 클라이언트가 정확한 페이지의 데이터를 보장받지 못할 수 있습니다.

2. Cursor-based Pagination (커서 기반 페이징)

Cursor-based pagination은 더 효율적이고 성능이 좋은 방식으로, 특히 대량의 데이터를 다룰 때 적합합니다. **커서(cursor)**는 특정 데이터를 식별할 수 있는 고유 값(예: 데이터베이스의 고유 ID)을 사용하여, 해당 커서를 기준으로 다음 페이지 데이터를 가져옵니다.

1) 기본 개념

  • cursor: 현재 위치를 나타내는 값(예: 데이터의 ID).
  • first/last: 몇 개의 데이터를 가져올지 결정하는 값입니다. first는 처음부터 데이터를 가져오고, last는 끝에서부터 데이터를 가져옵니다.

2) 예시

사용자 목록을 커서 기반으로 가져오는 예를 살펴볼게요.

GraphQL 쿼리:

query getUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}

이 쿼리에서는 first로 데이터를 몇 개 가져올지 결정하고, after로 특정 커서(이전 데이터의 마지막 아이디)를 기준으로 데이터를 요청합니다. 서버는 해당 커서 이후의 데이터를 반환하게 됩니다.

3) 서버 리졸버 예시 (Node.js)

javascript코드 복사const resolvers = {
  Query: {
    users: async (parent, { first, after }) => {
      const cursorCondition = after ? { _id: { $gt: after } } : {};
      const users = await User.find(cursorCondition).limit(first);
      
      const edges = users.map(user => ({
        node: user,
        cursor: user._id // 커서를 ID로 사용
      }));

      const hasNextPage = users.length === first;
      const endCursor = hasNextPage ? users[users.length - 1]._id : null;

      return {
        edges,
        pageInfo: {
          endCursor,
          hasNextPage
        }
      };
    }
  }
};

이 예시에서 after 값이 있는 경우 해당 커서(ID) 이후의 데이터를 가져오고, first 값에 따라 데이터를 제한합니다. pageInfo 필드에서는 다음 페이지가 있는지 여부를 hasNextPage로 반환하며, 마지막 커서 값을 endCursor로 반환합니다.

장점:

  • 데이터가 추가되거나 삭제되어도 페이징에 문제가 발생하지 않습니다. 커서를 기준으로 하므로, 데이터가 변경되더라도 올바른 페이지를 가져올 수 있습니다.
  • 오프셋 기반보다 성능이 뛰어납니다. 특히 오프셋이 큰 경우 성능 저하 문제를 피할 수 있습니다.

단점:

  • 커서를 관리해야 하므로 구현이 더 복잡할 수 있습니다.
  • 데이터베이스에 커서로 사용할 고유 값(예: ID)이 필요합니다.

3. 페이징 선택: Offset vs Cursor

언제 Offset-based를 사용할까?

  • 간단한 애플리케이션에서 데이터 양이 적고, 페이징의 성능이 크게 중요하지 않은 경우.
  • 이미 SQL 기반의 전통적인 데이터베이스와 연동하고 있고, 오프셋 방식을 쉽게 사용할 수 있을 때.

언제 Cursor-based를 사용할까?

  • 대량의 데이터를 처리하거나, 데이터가 자주 변경되는 경우.
  • 성능이 중요한 애플리케이션에서 데이터의 정확성과 성능을 보장해야 할 때.

4. Relay 스타일 페이징

Relay는 GraphQL 클라이언트 라이브러리 중 하나로, Cursor-based pagination을 사용할 때 권장되는 페이징 방식입니다. Relay 스타일 페이징은 edgespageInfo 구조를 사용하며, GraphQL API 설계 시 모범 사례로 많이 사용됩니다.

Relay 스타일 페이징 구조:

  • edges: 데이터를 담고 있는 배열이며, 각 데이터는 nodecursor로 구성됩니다.
  • pageInfo: 페이지 정보를 담고 있으며, 다음 페이지가 있는지 여부(hasNextPage)와 마지막 커서(endCursor) 등을 포함합니다.

Relay 스타일 쿼리:

query getUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}

5. 페이징을 더 효율적으로 관리하는 방법

  • 서버에서 페이징 제한 설정: 서버에서 limit 값을 제한하여 클라이언트가 너무 많은 데이터를 요청하는 것을 방지할 수 있습니다. 예를 들어, 최대 100개의 데이터만 한 번에 가져올 수 있도록 설정할 수 있습니다.
  • 필터링과 페이징 결합: 페이징과 함께 데이터 필터링 기능을 제공하면 클라이언트가 더 유용하게 데이터를 요청할 수 있습니다. 예를 들어, 특정 조건에 맞는 데이터를 페이징하여 가져올 수 있도록 합니다.
  • 캐싱: 페이징된 데이터를 캐시하여 같은 요청이 반복될 때 서버 자원을 아끼고 응답 시간을 줄일 수 있습니다.

결론

  • Offset-based pagination은 단순한 구현으로 시작하기 좋지만, 대량의 데이터를 다룰 때는 성능 문제가 발생할 수 있습니다.
  • Cursor-based pagination은 데이터의 정확성을 유지하면서 더 나은 성능을 제공합니다. 특히 데이터가 자주 변경되는 애플리케이션에서 유리합니다.
  • Relay 스타일 페이징은 GraphQL API에서 많이 사용하는 패턴이며, 성능과 확장성을 모두 고려한 좋은 선택입니다.

페이징은 애플리케이션의 데이터 처리 성능을 최적화하는 중요한 기법이므로, 사용하려는 데이터의 양과 특성에 맞게 페이징 방식을 선택하는 것이 중요합니다.