Skip to main content

Shopify를 위한 GraphQL 핵심

GraphQL은 Shopify API와 통신하기 위한 기본 언어입니다. REST API만 사용해 본 적이 있다면 GraphQL은 다르게 느껴질 것입니다. 원하는 데이터를 정확히 설명하면 서버가 정확히 그 형태를 반환합니다. 오버페칭도, 언더페칭도, 여러 요청을 연결할 필요도 없습니다. 이 강의에서는 GraphQL을 기초부터 가르치며, 매일 사용하게 될 Shopify 전용 패턴을 포함합니다.

REST보다 GraphQL을 사용하는 이유

상품과 변형, 이미지, 메타필드를 함께 조회하는 경우를 생각해 보세요. REST에서는 다음이 필요할 수 있습니다:

GET /admin/api/2026-01/products/123.json          → 상품 데이터
GET /admin/api/2026-01/products/123/variants.json → 변형 데이터
GET /admin/api/2026-01/products/123/images.json → 이미지 데이터
GET /admin/api/2026-01/products/123/metafields.json → 메타필드 데이터

4개의 HTTP 요청이고, 각각 필요하지 않을 수도 있는 필드를 반환합니다. GraphQL에서는:

query {
product(id: "gid://shopify/Product/123") {
title
status
variants(first: 10) {
edges {
node {
title
price
inventoryQuantity
}
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
metafields(first: 10) {
edges {
node {
namespace
key
value
}
}
}
}
}

한 번의 요청으로 필요한 필드만 정확히 가져옵니다. 이것이 Shopify가 GraphQL에 올인하는 이유입니다.

쿼리: 데이터 읽기

쿼리는 Shopify에서 데이터를 읽는 방법입니다. 모든 쿼리는 루트 필드에서 시작하여 데이터 그래프로 들어갑니다.

기본 쿼리 구조

query ProductList {
products(first: 10, query: "status:active") {
edges {
node {
id
title
handle
status
totalInventory
priceRangeV2 {
minVariantPrice {
amount
currencyCode
}
maxVariantPrice {
amount
currencyCode
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

주요 개념:

  • 작업 이름 (ProductList): 선택 사항이지만 권장 -- 디버깅 및 로깅에 도움됩니다
  • 인자 (first: 10, query: "status:active"): 결과 필터링 및 페이지네이션
  • 선택 세트: 반환받고 싶은 중첩 필드
  • Edge와 Node: Shopify는 목록에 Relay 연결 패턴을 사용합니다
  • pageInfo: 커서 기반 페이징을 위한 페이지네이션 메타데이터

변수 사용

값을 쿼리 문자열에 직접 삽입하지 마세요. 대신 변수를 사용하세요:

query ProductById($id: ID!) {
product(id: $id) {
title
description
vendor
productType
tags
createdAt
}
}
{
"variables": {
"id": "gid://shopify/Product/7654321"
}
}
항상 변수를 사용하세요

변수는 GraphQL 인젝션 공격을 방지하고, 쿼리 캐싱을 가능하게 하며, 코드를 더 깔끔하게 만듭니다. Remix 템플릿의 Shopify Admin GraphQL 클라이언트는 변수를 기본적으로 지원합니다.

일반적인 쿼리 패턴

라인 아이템과 풀필먼트 상태가 포함된 주문 조회:

query RecentOrders($first: Int!) {
orders(first: $first, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
displayFinancialStatus
displayFulfillmentStatus
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
lineItems(first: 50) {
edges {
node {
title
quantity
variant {
sku
}
}
}
}
customer {
displayName
email
}
}
}
}
}

필터를 사용한 고객 검색:

query SearchCustomers($query: String!) {
customers(first: 25, query: $query) {
edges {
node {
id
displayName
email
ordersCount
totalSpent
tags
addresses {
city
province
country
}
}
}
}
}

위치별 재고 수준 확인:

query InventoryLevels($variantId: ID!) {
productVariant(id: $variantId) {
title
sku
inventoryItem {
inventoryLevels(first: 10) {
edges {
node {
location {
name
}
quantities(names: ["available", "committed", "on_hand"]) {
name
quantity
}
}
}
}
}
}
}

뮤테이션: 데이터 쓰기

뮤테이션은 데이터를 수정합니다. Shopify의 API에서 일관된 패턴을 따릅니다: input 객체를 전달하고, 뮤테이션은 수정된 리소스와 사용자 오류를 반환합니다.

상품 생성

mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
status
variants(first: 5) {
edges {
node {
id
price
}
}
}
}
userErrors {
field
message
}
}
}
{
"variables": {
"input": {
"title": "Premium Widget",
"descriptionHtml": "<p>A high-quality widget for discerning customers.</p>",
"vendor": "WidgetCo",
"productType": "Widgets",
"tags": ["premium", "new-arrival"],
"status": "DRAFT",
"variants": [
{
"price": "29.99",
"sku": "WIDGET-001",
"inventoryQuantities": [
{
"availableQuantity": 100,
"locationId": "gid://shopify/Location/1"
}
]
}
]
}
}
}
항상 userErrors를 확인하세요

Shopify 뮤테이션은 예외를 던지는 대신 userErrors 배열을 반환합니다. 뮤테이션이 HTTP 200과 성공적인 GraphQL 응답을 반환하면서도 userErrors가 있을 수 있습니다. 작업이 성공했다고 가정하기 전에 항상 이 배열을 확인하세요.

주문 태그 업데이트

mutation AddOrderTags($id: ID!, $tags: [String!]!) {
tagsAdd(id: $id, tags: $tags) {
node {
... on Order {
id
tags
}
}
userErrors {
field
message
}
}
}

메타필드 뮤테이션

mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
namespace
key
value
}
userErrors {
field
message
}
}
}
{
"variables": {
"metafields": [
{
"ownerId": "gid://shopify/Product/123",
"namespace": "custom",
"key": "warranty_years",
"value": "3",
"type": "number_integer"
}
]
}
}

구독: 실시간 업데이트

GraphQL 구독을 통해 앱이 Webhook을 통해 실시간 업데이트를 받을 수 있습니다. Shopify는 WebSocket 기반 구독을 지원하지 않지만, GraphQL을 통해 등록하는 Webhook 구독을 사용합니다:

mutation CreateWebhookSubscription {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://your-app.com/webhooks/orders-create"
format: JSON
}
) {
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}

사용 가능한 토픽에는 ORDERS_CREATE, PRODUCTS_UPDATE, CUSTOMERS_CREATE, INVENTORY_LEVELS_UPDATE 등 수십 가지가 있습니다.

GraphQL Explorer

Shopify는 쿼리 테스트를 위한 대화형 GraphQL Explorer를 제공합니다:

  1. Partner Dashboard로 이동합니다
  2. 앱을 선택한 다음 API access를 클릭합니다
  3. GraphiQL을 클릭하여 Explorer를 엽니다

또는 Shopify CLI를 사용합니다:

# 개발 스토어용 GraphiQL 열기
shopify app dev --graphiql

Explorer는 다음을 제공합니다:

  • 스키마 문서: 모든 타입, 필드, 인자 탐색
  • 자동 완성: 입력하면 제안 표시
  • 쿼리 유효성 검사: 실행 전 오류 하이라이트
  • 변수 편집기: 다른 입력으로 테스트
  • 응답 뷰어: 포맷된 JSON 출력
Claude Code를 GraphQL Explorer로 사용하세요

GraphiQL에서 수동으로 쿼리를 작성하는 대신 Claude Code에 요청하세요: "최근 5개 주문을 라인 아이템, 고객 이메일, 풀필먼트 상태와 함께 조회하는 GraphQL 쿼리를 작성해주세요." Claude Code는 Shopify의 스키마를 이해하고 정확하고 효율적인 쿼리를 생성합니다.

대량 작업

수천 또는 수백만 개의 레코드를 처리해야 할 때 개별 쿼리는 너무 느립니다. Shopify의 Bulk Operations API를 사용하면 전체 데이터셋에 대해 쿼리를 비동기적으로 실행할 수 있습니다.

mutation BulkExportProducts {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
handle
status
variants {
edges {
node {
id
sku
price
inventoryQuantity
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
url
}
userErrors {
field
message
}
}
}

대량 작업은 비동기적으로 실행됩니다. 완료 여부를 폴링합니다:

query BulkOperationStatus {
currentBulkOperation {
id
status
objectCount
url
errorCode
fileSize
}
}

statusCOMPLETED이면 url 필드에 모든 결과가 포함된 JSONL 파일에 대한 링크가 있습니다. 각 줄은 JSON 객체이며, 중첩된 객체는 __parentId 필드를 통해 부모를 참조합니다.

{"id":"gid://shopify/Product/1","title":"Widget A","handle":"widget-a","status":"ACTIVE"}
{"id":"gid://shopify/ProductVariant/10","sku":"WA-001","price":"29.99","__parentId":"gid://shopify/Product/1"}
{"id":"gid://shopify/Product/2","title":"Widget B","handle":"widget-b","status":"DRAFT"}
{"id":"gid://shopify/ProductVariant/20","sku":"WB-001","price":"19.99","__parentId":"gid://shopify/Product/2"}
대량 작업을 사용해야 할 때

수백 개 이상의 레코드를 내보내거나 처리해야 할 때 대량 작업을 사용하세요. 일반적인 사용 사례: 데이터 마이그레이션, 보고서 작성, 외부 시스템과의 동기화, 초기 데이터 로드. 대량 작업은 일반 레이트 리밋의 적용을 받지 않습니다.

오류 처리

Shopify의 GraphQL 오류는 두 가지 형태로 나타납니다:

1. GraphQL 오류 (쿼리 수준)

쿼리 자체의 문제를 나타냅니다 -- 구문 오류, 유효하지 않은 필드, 인증 실패:

{
"errors": [
{
"message": "Field 'nonExistentField' doesn't exist on type 'Product'",
"locations": [{ "line": 3, "column": 5 }]
}
]
}

2. 사용자 오류 (비즈니스 로직)

유효한 쿼리지만 비즈니스 규칙에 실패한 경우입니다:

{
"data": {
"productCreate": {
"product": null,
"userErrors": [
{
"field": ["input", "title"],
"message": "Title can't be blank"
}
]
}
}
}

두 유형 모두 항상 처리하세요:

const response = await admin.graphql(query, { variables });
const { data, errors } = await response.json();

// 쿼리 수준 오류 확인
if (errors) {
console.error("GraphQL errors:", errors);
throw new Error(errors[0].message);
}

// 비즈니스 로직 오류 확인
const result = data.productCreate;
if (result.userErrors.length > 0) {
console.error("User errors:", result.userErrors);
throw new Error(result.userErrors[0].message);
}

// 성공
return result.product;

핵심 요약

  • GraphQL은 단일 요청으로 필요한 데이터를 정확히 가져올 수 있게 해줍니다
  • 안전성과 캐싱을 위해 문자열 보간 대신 변수를 사용하세요
  • 뮤테이션 응답에서 항상 userErrors를 확인하세요
  • 대규모 데이터 세트(수백 개 이상의 레코드)에는 대량 작업을 사용하세요
  • Relay 연결 패턴(edges/nodes/pageInfo)은 모든 목록에 사용됩니다
  • Claude Code는 자연어 설명에서 Shopify GraphQL 쿼리를 생성할 수 있습니다

다음으로 Shopify의 Online Store 2.0 테마를 구동하는 템플릿 언어인 Liquid를 살펴보겠습니다.