메타 디렉티브를 통한 스크립팅 기능
쿼리 내 필드에 적용할 수 있는 디렉티브 @strTitleCase가 있다고 가정해 보겠습니다. 이 디렉티브는 필드의 값을 "hello world!"에서 "Hello World!"로 변환하므로, String 타입의 필드에만 적용하는 것이 합리적입니다.
다음 쿼리를 실행하면:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...다음과 같은 결과가 반환됩니다:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}이제 필드 타입이 [String](또는 [String!])인 경우, 예를 들어 다음과 같은 케이스를 생각해 보겠습니다:
type Post {
categoryNames: [String!]
}이 쿼리를 실행할 때 필드 categoryNames에 디렉티브 @strTitleCase를 적용하면 어떻게 되어야 할까요?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}이상적으로는 배열 내의 모든 String 값이 변환된 결과가 반환되어야 합니다:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}이를 구현하려면 @strTitleCase의 디렉티브 리졸버가 입력이 배열인지 확인하고 그에 따라 처리해야 합니다(이 PHP 코드는 예시이며, 플러그인의 실제 메서드와는 다릅니다):
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}이것은 그리 어렵지 않습니다. 하지만 필드가 String의 배열의 배열, 즉 [[String]]이라면 어떻게 될까요? 조금 더 어렵긴 하지만, 디렉티브는 이것도 처리할 수 있습니다:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to title case
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(ucwords(...), $array),
$value
);
}
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}그렇다면 [[[String]]]이나 [[[[String]]]]이라면 어떨까요? 구현이 점점 더 어려워집니다.
더 나쁜 것은, 이러한 추가적인 로직 보일러플레이트를 배열에 적용될 수 있는 모든 디렉티브에 구현해야 한다는 것입니다. 예를 들어, 디렉티브 @strUpperCase를 구현할 때도 이 추가 로직이 필요합니다:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to uppercase
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(strtoupper(...), $array),
$value
);
}
// Convert each item in an array to uppercase
if ($schemaDef['isArray']) {
return array_map(strtoupper(...), $value);
}
// Convert the String value to uppercase
return strtoupper($value);
}그다지 보기 좋지 않죠?
해결책: 다른 디렉티브를 통해 디렉티브의 입력을 변경하기
여기서 하나의 디렉티브를 적용하여 다른 디렉티브의 동작을 변경하는 것이 유용해집니다.
필드의 배열 깊이(즉, String, [String], [[String]], [[[String]]] 등)에 모두 대응하는 대신, @strTitleCase는 기본 케이스인 String만 처리하도록 할 수 있습니다:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// The input will always be `String`
// Convert the String value to title case
return ucwords($value);
}그런 다음 또 다른 디렉티브 @underEachArrayItem이 그 동작을 다음과 같이 변경합니다:
[String]타입의 단일 입력을String타입의 입력 배열로 변환합니다- 이 배열의 항목을 반복하며, 각 항목에 대해 하위 디렉티브(
@strTitleCase)를 호출하여 적용합니다. 이때String타입의 입력을 받습니다 String값의 배열을 다시 단일[String]값으로 변환합니다
그러면 다음 쿼리를 실행할 수 있습니다:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}이 gif는 @underEachArrayItem의 동작을 보여줍니다:

이 해결책의 장점은 배열의 깊이와 디렉티브의 구현을 분리할 수 있다는 것입니다. 입력 타입이 [[String]]인 경우, 추가적인 @underEachArrayItem을 하나 더 추가하기만 하면 됩니다. 이것이 의도한 디렉티브를 변경하는 @underEachArrayItem을 변경합니다:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...다음과 같은 결과를 생성합니다:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}이처럼 하나의 디렉티브가 다른 디렉티브를 변경하는 것은 디렉티브 파이프라인에서도 발생할 수 있습니다. 이 경우 하나의 디렉티브가 하위 디렉티브에 영향을 주고, 그 자신도 상위 디렉티브에 의해 변경됩니다.
우리는 @underEachArrayItem을 「메타 디렉티브」라고 부릅니다: 다른 디렉티브의 동작을 변경하는 디렉티브입니다. 이를 통해 개발자는 GraphQL 쿼리 내에 프로그래밍 로직을 추가하는 「메타 스크립팅」 기능을 갖게 됩니다.
GraphQL 쿼리 형식화
공백 문자는 의미론적 값을 갖지 않으므로, 쿼리와 SDL을 형식화하여 중첩 구조를 더 명확하게 표현할 수 있습니다:
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}중첩 디렉티브의 파이프라인 정의하기
@underEachArrayItem은 어떻게 @strTitleCase의 동작을 변경해야 한다고 판단할까요? 이전 예시에서는 바로 앞에 배치되어 있었기 때문입니다. 하지만 그 뒤에 또 다른 디렉티브가 있다면 어떻게 될까요?
예를 들어, 다음 쿼리에서는:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem은 디렉티브 @strTranslate의 동작도 변경해야 합니다. 이 디렉티브도 String에 적용되어야 하므로, 다음과 같은 응답이 생성됩니다:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}그러나 뒤에 배치된 디렉티브가 개별 String 값이 아닌 배열에 적용되어야 하는 경우도 있습니다. 예를 들어, 아래의 디렉티브 @arrayPad는 배열에서 누락된 항목을 기본값으로 채우므로, @underEachArrayItem의 영향을 받아서는 안 됩니다:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...다음과 같은 응답이 생성됩니다:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}이 두 가지 상황을 구분하기 위해 @underEachArrayItem에 인수 affectDirectivesUnderPos를 도입합니다. 이는 영향을 받아야 하는 디렉티브의 상대적 위치를 Int 배열로 정의합니다.
아래 쿼리에서 @underEachArrayItem은 @strTitleCase와 @strTranslate에 적용해야 한다고 인식합니다. 이들이 자신으로부터 상대적 위치 1과 2에 배치되어 있기 때문입니다:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}이 다른 쿼리에서는 @underEachArrayItem이 @strTitleCase(상대적 위치 1)에만 적용되고 @arrayPad에는 적용되지 않습니다:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}affectDirectivesUnderPos의 기본값은 [1]로 설정되어 있으므로, 지정하지 않으면 디렉티브는 항상 바로 다음에 오는 디렉티브에 적용됩니다. 위의 쿼리는 다음과 동일합니다:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}메타 디렉티브의 영향을 받는 디렉티브와 받지 않는 디렉티브의 임의의 조합을 정의할 수 있습니다:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}