스키마 튜토리얼
스키마 튜토리얼레슨 23: API 게이트웨이 만들기

레슨 23: API 게이트웨이 만들기

API 게이트웨이는 클라이언트와 여러 필요 서비스 간의 API 통신을 중앙에서 처리하는 애플리케이션 컴포넌트입니다.

API 게이트웨이는 서버에 저장되고 클라이언트가 호출하는 GraphQL 퍼시스티드 쿼리를 통해 구현할 수 있으며, 이 쿼리는 하나 이상의 백엔드 서비스와 상호 작용하여 결과를 수집한 뒤 단일 응답으로 클라이언트에 반환합니다.

GraphQL 퍼시스티드 쿼리를 사용하여 API 게이트웨이를 제공하는 이점은 다음과 같습니다.

  • 클라이언트가 백엔드 서비스에 대한 연결을 직접 처리할 필요가 없어 로직이 단순해집니다
  • 백엔드 서비스에 대한 접근이 중앙 집중화됩니다
  • 클라이언트에 인증 정보가 노출되지 않습니다
  • 서비스의 응답을 클라이언트가 기대하거나 더 잘 처리할 수 있는 형식으로 변환할 수 있습니다
  • 백엔드 서비스가 업그레이드되더라도 클라이언트에 파괴적 변경을 일으키지 않고 퍼시스티드 쿼리를 수정할 수 있습니다
  • 서버는 백엔드 서비스 접근 로그를 저장하고 분석 강화를 위한 지표를 추출할 수 있습니다

이 튜토리얼 레슨에서는 GitHub Actions API에서 최신 아티팩트를 가져오고, 클라이언트가 GitHub에 로그인하지 않아도 다운로드할 수 있는 URL을 추출하는 API 게이트웨이를 시연합니다.

GraphQL 기반 API 게이트웨이로 GitHub Actions 아티팩트에 접근하기

아래 GraphQL 쿼리는 퍼시스티드 쿼리로 저장해야 합니다(예: 슬러그 retrieve-public-urls-for-github-actions-artifacts 사용).

이 쿼리는 GitHub Actions 아티팩트의 공개적으로 접근 가능한 다운로드 URL을 가져옵니다.

  • 먼저 GitHub Actions에서 최신 X개의 아티팩트를 가져오고, 각각에 접근하기 위한 프록시 URL을 추출합니다. (인증된 사용자만 아티팩트에 접근할 수 있으므로, 이 URL들은 아직 실제 아티팩트를 가리키지 않습니다.)
  • 그런 다음 이 프록시 URL들(아티팩트가 짧은 기간 동안 공개 위치에 업로드되어 있음)에 각각 접근하여 HTTP 응답의 Location 헤더에서 실제 URL을 추출합니다
  • 마지막으로 공개적으로 접근 가능한 모든 URL을 출력하여, 비인증 사용자가 해당 시간 내에 GitHub 아티팩트를 다운로드할 수 있게 합니다

(튜토리얼 레슨은 여기서 끝나지만, 이후 단계로서 GraphQL 쿼리에서 이 URL들을 활용할 수 있습니다: 이메일로 전송하기, FTP로 파일 업로드하기, InstaWP 사이트에 설치하기 등.)

query RetrieveGitHubAccessToken {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @export(as: "githubAccessToken")
    @remove
}
 
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  githubArtifactsEndpoint: _sprintf(
    string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
    values: [$numberArtifacts]
  )
    @remove
 
  # Retrieve Artifact data from GitHub Actions API
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubArtifactsEndpoint,
      options: {
        auth: {
          password: $githubAccessToken
        },
        headers: [
          {
            name: "Accept",
            value: "application/vnd.github+json"
          }
        ]
      }
    }
  )
    @remove
  
  # Extract the URL from within each "artifacts" item
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData,
    by: {
      key: "artifacts"
    }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $artifactItem,
          by: {
            key: "archive_download_url"
          }
        },
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}
 
query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(
      passValueOnwardsAs: "url"
    )
      @applyField(
        name: "_objectAddEntry",
        arguments: {
          object: {
            options: {
              auth: {
                password: $githubAccessToken
              },
              headers: {
                name: "Accept",
                value: "application/vnd.github+json"
              },
              allowRedirects: null
            }
          },
          key: "url",
          value: $url
        },
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    @remove
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(
    inputs: $httpRequestInputs
  ) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}
 
query PrintArtifactDownloadURLsAsList
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}

응답은 다음과 같습니다.

{
  "data": {
    "gitHubProxyArtifactDownloadURLs": [
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444209/zip",
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444208/zip",
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444207/zip"
    ],
    "_sendHTTPRequests": [
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
      }
    ],
    "artifactDownloadURLs": [
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
    ]
  }
}

대안: HTTP 요청에서 GitHub 인증 정보 가져오기

사용자가 헤더를 통해 자신의 GitHub 인증 정보를 직접 제공할 수 있도록 허용할 수도 있습니다.

이 GraphQL 쿼리는 이전 쿼리를 응용한 것으로, 다음과 같은 차이점이 있습니다.

  • 오퍼레이션 RetrieveGitHubAccessToken은 현재 HTTP 요청의 X-Github-Access-Token 헤더에서 값을 읽어 내보내며, 이 헤더가 제공되지 않은 경우 이를 알립니다
  • FailIfGitHubAccessTokenIsMissing은 헤더가 없을 때 오류를 발생시킵니다
  • 다른 모든 오퍼레이션에 디렉티브 @skip(if: $isGithubAccessTokenMissing)가 추가되어, 토큰이 없을 경우 실행되지 않습니다
query RetrieveGitHubAccessToken {
  githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
    @export(as: "githubAccessToken")
    @remove
 
  isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
    @export(as: "isGithubAccessTokenMissing")
}
 
query FailIfGitHubAccessTokenIsMissing
  @depends(on: "RetrieveGitHubAccessToken")
  @include(if: $isGithubAccessTokenMissing)
{
  _fail(
    message: "Header 'X-Github-Access-Token' has not been provided"
  ) @remove
}
 
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query PrintArtifactDownloadURLsAsList
  @depends(on: [
    "RetrieveActualArtifactDownloadURLs",
    "FailIfGitHubAccessTokenIsMissing"
  ])
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}

헤더 X-Github-Access-Token이 제공된 경우, 응답은 위와 동일합니다.

제공되지 않은 경우, 응답은 다음과 같습니다.

{
  "errors": [
    {
      "message": "Header 'X-Github-Access-Token' has not been provided",
      "locations": [
        {
          "line": 18,
          "column": 3
        }
      ],
      "extensions": {
        "path": [
          "_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
          "query FailIfGitHubAccessTokenIsMissing @depends(on: \"ValidateHasGitHubAccessToken\") @skip(if: $isGithubAccessTokenMissing) { ... }"
        ],
        "type": "QueryRoot",
        "field": "_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
        "id": "root",
        "code": "PoPSchema/FailFieldAndDirective@e1"
      }
    }
  ],
  "data": {
    "isGithubAccessTokenMissing": false
  }
}

API 게이트웨이에서 사용하는 여러 서비스의 인증 정보를 헤더에서 가져오면서, 모두 제공되었는지 검증할 수 있습니다.

query RetrieveServiceTokens {
  githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
    @export(as: "githubAccessToken")
  slackAccessToken: _httpRequestHeader(name: "X-Slack-Access-Token")
    @export(as: "slackAccessToken")
 
  isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
  isSlackAccessTokenMissing: _isEmpty(value: $__slackAccessToken)    
  isAnyAccessTokenMissing: _or(values: [
    $__isGithubAccessTokenMissing,
    $__isSlackAccessTokenMissing
  ])
    @export(as: "isAnyAccessTokenMissing")
}
 
query FailIfAnyAccessTokenMissing
  @depends(on: "RetrieveServiceTokens")
  @include(if: $isAnyAccessTokenMissing)
{
  _fail(
    message: "Access tokens for GitHub and Slack must be provided"
  ) @remove
}
 
query RetrieveProxyArtifactDownloadURLs
  @depends(on: "RetrieveServiceTokens")
  @skip(if: $isAnyAccessTokenMissing)
{
  # Do something
  # ...
}
 
# Do something
# ...

단계별 설명: GraphQL 쿼리 작성하기

아래는 이 쿼리가 어떻게 동작하는지에 대한 자세한 분석입니다.

연결할 엔드포인트는 동적으로 생성할 수 있으며, 이 경우 _sprintf를 사용합니다.

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  githubArtifactsEndpoint: _sprintf(
    string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
    values: [$numberArtifacts]
  )
    @remove
 
  # ...
}

GitHub Actions API의 응답은 크기가 크고 우리에게 필요하지 않으므로, @remove로 응답에서 제거합니다. 단, 개발 중에는 반환된 JSON 객체의 형태를 시각화하고 이해하여 추출해야 할 데이터 항목을 파악하기 위해 이 디렉티브를 비활성화합니다.

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  # ...
 
  # Retrieve Artifact data from GitHub Actions API
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubArtifactsEndpoint,
      options: {
        auth: {
          password: $githubAccessToken
        },
        headers: [
          {
            name: "Accept",
            value: "application/vnd.github+json"
          }
        ]
      }
    }
  )
    # @remove   <= Disabled to visualize output
}

응답은 다음과 같습니다.

{
  "data": {
    "gitHubArtifactData": {
      "total_count": 8344,
      "artifacts": [
        {
          "id": 803739808,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDg=",
          "name": "gato-graphql-testing-schema-1.0.0-dev",
          "size_in_bytes": 62952,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:17:15Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        },
        {
          "id": 803739806,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDY=",
          "name": "gato-graphql-testing-1.0.0-dev",
          "size_in_bytes": 123914,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:17:11Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        },
        {
          "id": 803739803,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDM=",
          "name": "gato-graphql-1.0.0-dev",
          "size_in_bytes": 33394234,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:21:42Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        }
      ]
    }
  }
}

우리가 관심을 갖는 데이터 항목은 "archive_download_url" 속성입니다. JSON 객체 구조 내에서 각 데이터 항목으로 이동하여, 디렉티브 @applyField를 통해 적용된 필드 _objectProperty를 사용해 해당 값을 추출하고, 인수 setResultInResponse: true를 전달하여 반복 중인 요소를 덮어씁니다.

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  # ...
  
  # Extract the URL from within each "artifacts" item
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData,
    by: {
      key: "artifacts"
    }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $artifactItem,
          by: {
            key: "archive_download_url"
          }
        },
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}

필드 _sendHTTPRequests를 통해(여러 HTTP 요청을 비동기로 전송하면서)추출된 모든 아티팩트 URL에 동시에 연결하고, 각 응답에서 Location 헤더를 조회합니다.

필드 _sendHTTPRequests는 인수 input[HTTPRequestInput] 타입)을 받으므로, 이 입력을 다음과 같이 동적으로 생성합니다.

  • 아티팩트 URL 각각을 반복합니다(동적 변수 $gitHubProxyArtifactDownloadURLs에 저장됨)
  • 각 URL에 대해 필요한 모든 파라미터(헤더, 인증 등)를 포함하는 JSON 객체를 동적으로 구성합니다(필드 _objectAddEntry 사용)
  • 이 JSON 객체에 URL을 추가합니다(동적 변수 $url로 사용 가능)

이렇게 동적으로 생성된 JSON 객체 목록은 _sendHTTPRequests(input:)의 인수로 전달될 때 [HTTPRequestInput]으로 강제 변환됩니다. 절차가 올바르지 않아 어떤 항목이 HTTPRequestInput으로 변환되지 않는 경우(예: 필수 속성을 제공하지 않았거나 존재하지 않는 속성을 지정한 경우), GraphQL 서버는 강제 변환 오류를 발생시킵니다.

필드 httpRequestInputs에는 GitHub 토큰(password: $githubAccessToken 하위)이 포함되어 있으므로 응답에 출력되지 않도록 @remove를 사용해야 합니다. 단, 개발 중에는 이 디렉티브를 비활성화할 수 있습니다.

query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(
      passValueOnwardsAs: "url"
    )
      @applyField(
        name: "_objectAddEntry",
        arguments: {
          object: {
            options: {
              auth: {
                password: $githubAccessToken
              },
              headers: {
                name: "Accept",
                value: "application/vnd.github+json"
              },
              allowRedirects: null
            }
          },
          key: "url",
          value: $url
        },
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    # @remove   <= Disabled to visualize output
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(
    inputs: $httpRequestInputs
  ) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}

@remove가 주석 처리되었으므로, 이제 응답에서 생성된 JSON 객체 입력값(httpRequestInputs 항목 하위)과 각 HTTP 응답에서 나온 Location 헤더 결과(별칭 artifactDownloadURL 하위)를 시각화할 수 있습니다.

{
  "data": {
    "gitHubProxyArtifactDownloadURLs": [
      // ...
    ],
    "httpRequestInputs": [
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip"
      },
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip"
      },
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip"
      }
    ],
    "_sendHTTPRequests": [
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2766840Z&urlSigningMethod=HMACV2&urlSignature=Ype82npdlUlLk4gcGZcBiz80e0ZuvcvnC2rdaSDg9p8%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2961965Z&urlSigningMethod=HMACV2&urlSignature=FdWAh8JXNPJsVIPNuiYN8R7i0vRnN8eCGc57VZDNUEc%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2861087Z&urlSigningMethod=HMACV2&urlSignature=0Go8QnkZqIbn0urTQqfbMW4rQtjMfDAR9fSm6fCePjw%3D"
      }
    ]
  }
}

마지막으로 _echo를 사용하여 모든 artifactDownloadURL 항목을 목록으로 함께 출력합니다(동적 변수 $artifactDownloadURLs로 사용 가능).

query PrintArtifactDownloadURLsAsList
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}

이렇게 하면 다음이 출력됩니다.

{
  "data": {
    // ...
    "artifactDownloadURLs": [
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4998268Z&urlSigningMethod=HMACV2&urlSignature=1c1qNRfD9KFwSuzMjw9tsumq9B5I1c9H4LWgSbR0Kwg%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4878741Z&urlSigningMethod=HMACV2&urlSignature=htjc1HrmZpbecECpBQnEHhlP7lkqkdyjzATb0vFnzDE%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.5240496Z&urlSigningMethod=HMACV2&urlSignature=YDuHFqweL9m6LIycLsVy0bJJ4zePc4pWkHz8RfjfzCg%3D"
    ]
  }
}