여러 쿼리 동시 실행
여러 쿼리를 하나로 결합하여 단일 작업으로 실행하고, 각각의 상태와 데이터를 재사용할 수 있습니다.
이것은 쿼리 배칭(query batching)과는 다릅니다. 쿼리 배칭에서도 GraphQL 서버는 단일 요청으로 여러 쿼리를 실행하지만, 해당 쿼리들은 서로 독립적으로 순서대로 실행될 뿐입니다.
이 기능은 성능을 향상시킵니다. 쿼리를 여러 요청으로 독립적으로 실행하는(먼저 GraphQL 서버에 작업을 실행하고, 응답을 기다린 후, 그 결과를 사용해 다른 작업을 수행하는) 방식 대신, 쿼리를 한꺼번에 실행할 수 있어 여러 요청으로 인한 레이턴시를 피할 수 있습니다.
Multiple Query Execution을 사용하면 GraphQL 쿼리를 더욱 체계적으로 구성할 수 있습니다. 서로 의존하고, 이전 작업의 결과에 따라 조건부로 실행되는 논리적 단위로 쿼리를 분리할 수 있습니다.
여러 쿼리 실행 사용 방법
로그인한 사용자의 이름이 언급된 모든 게시물을 검색하고 싶다고 가정해 보겠습니다. 일반적으로 이를 위해서는 두 개의 쿼리가 필요합니다.
먼저 사용자의 name을 가져옵니다:
query GetLoggedInUserName {
me {
name
}
}...그리고 첫 번째 쿼리를 실행한 후, 가져온 사용자의 name을 변수 $search로 전달하여 두 번째 쿼리에서 검색을 수행합니다:
query GetPostsContainingString($search: String!) {
posts(filter: { search: $search }) {
id
title
}
}Multiple Query Execution은 이 과정을 간소화하여 모든 데이터를 가져오고 필요한 로직을 단일 요청으로 실행할 수 있게 해 줍니다:
query GetLoggedInUserName {
me {
name @export(as: "search")
}
}
query GetPostsContainingString @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $search }) {
id
title
}
}Multiple Query Execution은 다음과 같은 특수 디렉티브를 사용하여 구현됩니다:
@depends(오퍼레이션 디렉티브): 작업(query또는mutation)이 먼저 실행되어야 하는 다른 작업을 지정합니다@export(필드 디렉티브): 한 작업의 필드 값을 내보내어 다른 작업의 필드 입력으로 주입합니다@deferredExport(필드 디렉티브): Multi-Field Directives와 함께 사용하는@export와 유사한 동작을 합니다
또한 @include와 @skip도 오퍼레이션 디렉티브로 사용할 수 있게 됩니다(일반적으로는 필드 디렉티브만 해당). 이를 통해 어떤 조건을 충족하는 경우 작업을 조건부로 실행할 수 있습니다.
GraphQL 서버는 각 @depends(on: ...)에서 수집하여 로드하고 실행할 작업 목록을 만들고, @export가 포함된 모든 필드의 값을 동적 변수(as 인수에 정의된 이름)로 내보내어 후속 작업의 입력으로 사용합니다.
이러한 디렉티브를 조합하면, 복잡한 기능을 중간 단계로 분리하고, query와 mutation 작업을 번갈아 사용하며, 필요한 순서로 의존성을 추가하고, 가장 바깥쪽 작업을 ?operationName=...로 정의하여 모든 것을 단일 요청으로 실행할 수 있습니다(위의 예에서는 ?operationName=GetPostsContainingString이 됩니다).
@depends를 통한 실행 작업 정의
GraphQL 문서에 여러 작업이 포함된 경우, URL 파라미터 ?operationName=...를 통해 서버에 실행할 작업을 지정합니다. 지정하지 않으면 마지막 작업이 실행됩니다.
이 초기 작업에서 시작하여, 서버는 depends(on: [...]) 디렉티브를 추가하여 정의된 실행해야 할 모든 작업을 수집하고, 의존성을 고려한 해당 순서로 실행합니다.
디렉티브 인수 operations는 작업 이름의 배열([String])을 받습니다. 단일 작업 이름(String)을 제공할 수도 있습니다.
이 쿼리에서 ?operationName=Four를 전달하면, 실행되는 작업(query 또는 mutation)은 ["One", "Two", "Three", "Four"]가 됩니다:
mutation One {
# Do something ...
}
mutation Two {
# Do something ...
}
query Three @depends(on: ["One", "Two"]) {
# Do something ...
}
query Four @depends(on: "Three") {
# Do something ...
}@export를 통한 쿼리 간 데이터 공유
디렉티브 @export는 필드(또는 필드 집합)의 값을 동적 변수로 내보내어, 다른 쿼리의 필드 입력으로 사용합니다.
예를 들어, 이 쿼리에서는 로그인한 사용자의 이름을 내보내고, 그 값을 사용해 해당 문자열이 포함된 게시물을 검색합니다(변수 $loggedInUserName은 동적 변수이므로 작업 FindPosts에서 정의할 필요가 없다는 점에 주목하세요):
query GetLoggedInUserName {
me {
name @export(as: "loggedInUserName")
}
}
query FindPosts @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $loggedInUserName }) {
id
}
}동적 변수 출력
@export는 다음의 조합에 따라 6가지 다른 출력을 생성할 수 있습니다:
type인수의 값(SINGLE,LIST, 또는DICTIONARY)- 디렉티브가 단일 필드에 적용되는지, 여러 필드에 적용되는지(Multi-Field Directives 모듈 경유)
6가지 가능한 출력은 다음과 같습니다:
SINGLE타입:- 단일 필드
- 멀티 필드
LIST타입:- 단일 필드
- 멀티 필드
DICTIONARY타입:- 단일 필드
- 멀티 필드
SINGLE 타입 / 단일 필드
파라미터 type: SINGLE(기본값으로 설정됨)을 전달하면, 출력은 단일 값이 됩니다.
이 쿼리에서:
query {
post(by: { id: 1 }) {
title @export(as: "postTitle", type: SINGLE)
}
}...동적 변수 $postTitle의 값은 다음과 같습니다:
"Hello world!"SINGLE이 엔티티 배열에 적용된 경우, 마지막 엔티티의 값이 내보내진다는 점에 주의하세요.
이 쿼리에서:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitle", type: SINGLE)
}
}...동적 변수 $postTitle은 ID 5인 게시물의 값을 가집니다:
"Everything good?"SINGLE 타입 / 멀티 필드
@export가 여러 필드에 적용된 경우(Multi-Field Directives 모듈이 제공하는 파라미터 affectAdditionalFieldsUnderPos를 추가함으로써), 동적 변수에 설정되는 값은 { key: 필드 별칭, value: 필드 값 }의 딕셔너리(JSONObject 타입)가 됩니다.
이 쿼리:
query {
post(by: { id: 1 }) {
title
content
@export(
as: "postData",
type: SINGLE,
affectAdditionalFieldsUnderPos: [1]
)
}
}...는 동적 변수 $postData를 다음 값으로 내보냅니다:
{
"title": "Hello world!",
"content": "Lorem ipsum."
}LIST 타입 / 단일 필드
파라미터 type: LIST를 전달하면, 동적 변수에는 쿼리된 모든 엔티티(내포 필드에서)의 필드 값을 담은 배열이 포함됩니다.
이 쿼리를 실행하면(쿼리되는 엔티티는 ID 1과 5인 게시물):
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitles", type: LIST)
}
}...동적 변수 $postTitles의 값은 다음과 같습니다:
[
"Hello world!",
"Everything good?"
]LIST 타입 / 멀티 필드
디렉티브가 적용된 필드의 값을 포함하는 딕셔너리(JSONObject 타입)의 배열을 얻습니다.
이 쿼리:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsData",
type: LIST,
affectAdditionalFieldsUnderPos: [1]
)
}
}...는 동적 변수 $postsData를 다음 값으로 내보냅니다:
[
{
"title": "Hello world!",
"content": "Lorem ipsum."
},
{
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
]DICTIONARY 타입 / 단일 필드
파라미터 type: DICTIONARY를 전달하면, 동적 변수에는 쿼리된 엔티티의 ID를 키로, 필드 값을 값으로 하는 딕셔너리(JSONObject 타입)가 포함됩니다.
이 쿼리:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postIDTitles", type: DICTIONARY)
}
}...는 동적 변수 $postIDTitles를 다음 값으로 내보냅니다:
{
"1": "Hello world!",
"5": "Everything good?"
}DICTIONARY 타입 / 멀티 필드
이 조합에서는 딕셔너리의 딕셔너리를 내보냅니다: { key: 엔티티 ID, value: { key: 필드 별칭, value: 필드 값 } }(JSONObject 타입의 항목을 포함하는 JSONObject 타입 사용).
이 쿼리:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsIDProperties",
type: DICTIONARY,
affectAdditionalFieldsUnderPos: [1]
)
}
}...는 동적 변수 $postsIDProperties를 다음 값으로 내보냅니다:
{
"1": {
"title": "Hello world!",
"content": "Lorem ipsum."
},
"5": {
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
}작업의 조건부 실행
Multiple Query Execution이 활성화되면, 디렉티브 @include와 @skip도 오퍼레이션 디렉티브로 사용할 수 있게 되어, 어떤 조건을 충족하는 경우 작업을 조건부로 실행하는 데 사용할 수 있습니다.
예를 들어, 이 쿼리에서는 작업 CheckIfPostExists가 동적 변수 $postExists를 내보내고, 그 값이 true인 경우에만 뮤테이션 ExecuteOnlyIfPostExists가 실행됩니다:
query CheckIfPostExists($id: ID!) {
# Initialize the dynamic variable to `false`
postExists: _echo(value: false) @export(as: "postExists")
post(by: { id: $id }) {
# Found the Post => Set dynamic variable to `true`
postExists: _echo(value: true) @export(as: "postExists")
}
}
mutation ExecuteOnlyIfPostExists
@depends(on: "CheckIfPostExists")
@include(if: $postExists)
{
# Do something...
}배열 또는 JSON 객체 반복 시 값 내보내기
@export는 이를 둘러싼 메타 디렉티브의 카디널리티를 존중합니다.
특히, @export가 배열 항목 또는 JSON 객체 속성을 반복하는 메타 디렉티브(즉 @underEachArrayItem과 @underEachJSONObjectProperty) 아래에 중첩된 경우, 내보내는 값은 배열이 됩니다.
이 쿼리:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...는 $contentAttributes를 다음 값으로 생성합니다:
[
"List Block",
"Columns Block",
"Columns inside Columns (nested inner blocks)",
"Life is so rich",
"Life is so dynamic"
]반면, 모든 항목을 반복하는 대신 배열의 특정 항목에 접근하는 동일한 쿼리(@underEachArrayItem을 @underArrayItem(index: 0)으로 교체)는 단일 값을 내보냅니다.
이 쿼리:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underArrayItem(index: 0)
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...는 $contentAttributes를 다음 값으로 생성합니다:
"List Block"디렉티브 실행 순서
@export 앞에 다른 디렉티브가 있는 경우, 내보내는 값은 해당 이전 디렉티브의 수정 사항을 반영합니다.
예를 들어, 이 쿼리에서는 @export가 @strUpperCase의 전후 중 어느 시점에 실행되느냐에 따라 결과가 달라집니다:
query One {
id
# First export "root", only then will be converted to "ROOT"
@export(as: "id")
@strUpperCase
again: id
# First convert to "ROOT" and then export this value
@strUpperCase
@export(as: "again")
}
query Two @depends(on: "One") {
mirrorID: _echo(value: $id)
mirrorAgain: _echo(value: $again)
}생성되는 결과:
{
"data": {
"id": "ROOT",
"again": "ROOT",
"mirrorID": "root",
"mirrorAgain": "ROOT"
}
}Multi-Field Directives
Multi-Field Directives 기능이 활성화되어 있고 여러 필드의 값을 딕셔너리로 내보내는 경우, 필드의 값을 내보내기 전에 관련된 모든 필드의 모든 디렉티브가 실행되었음을 보장하기 위해 @export 대신 @deferredExport를 사용하세요.
예를 들어, 이 쿼리에서는 첫 번째 필드에 디렉티브 @strUpperCase가 적용되고, 두 번째에는 @titleCase가 적용됩니다. @deferredExport를 실행하면, 내보내는 값에 이러한 디렉티브가 적용됩니다:
query One {
id @strUpperCase # Will be exported as "ROOT"
again: id @titleCase # Will be exported as "Root"
@deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
query Two @depends(on: "One") {
mirrorProps: _echo(value: $props)
}생성되는 결과:
{
"data": {
"id": "ROOT",
"again": "Root",
"mirrorProps": {
"id": "ROOT",
"again": "Root"
}
}
}GraphQL 사양
이 기능은 현재 GraphQL 사양의 일부가 아니지만, 요청이 제기되었습니다: