Community라이팅 & 에디팅github.com

khadinakbarlabs/shopify-app-builder

Shopify 앱 개발을 위한 오픈소스 플러그인으로, Claude Code, Codex, Cursor, OpenCode, Gemini CLI 및 AI 코딩 에이전트에서 사용 가능한 32가지 스킬을 제공합니다.

shopify-app-builder란 무엇인가요?

이 스킬은 AI 코딩 에이전트(Claude Code, Cursor, Codex, OpenCode, Gemini CLI 등)가 Shopify 앱을 구축할 때 필요한 인증 흐름을 완전히 이해하고 구현할 수 있도록 안내합니다. 2024년 이후 권장되는 토큰 교환(Token Exchange)부터 헤드리스 앱용 Managed Installation, 레거시 OAuth 2.0까지 세 가지 주요 인증 방식을 상세히 설명하며, Remix, Express.js 등 다양한 프레임워크에서의 실제 구현 코드를 제공합니다. 또한 Prisma, Redis, DynamoDB 등 여러 세션 스토리지 어댑터를 지원하고, 온라인/오프라인 토큰의 차이와 웹훅 처리 방법까지 다룹니다. 이 스킬을 통해 개발자는 Shopify 앱의 인증 시스템을 빠르고 안전하게 설정할 수 있습니다.

지원 대상✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI✓OpenCode
npx skills add khadinakbarlabs/shopify-app-builder

Installed? Explore more 라이팅 & 에디팅 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Shopify 앱 인증

Shopify는 앱 유형과 사용 사례에 따라 여러 인증 흐름을 제공합니다. 최신 표준은 토큰 교환(2024+) (서버 렌더링 앱), Managed Installation(헤드리스 앱), OAuth 2.0 인가 코드 승인(레거시/커스텀 구현)입니다. 모든 흐름은 Shopify GraphQL Admin API에 대한 액세스 토큰을 생성합니다.

인증 흐름 개요

1. 토큰 교환 (2024+ 권장)

사용 사례: 서버 렌더링 앱(Remix, SSR이 있는 Next.js), Shopify CLI 앱 흐름: 판매자가 앱 설치 → Shopify가 임시 교환 토큰 생성 → 앱이 액세스 토큰으로 교환 보안: 클라이언트 시크릿 노출 없음; 요청마다 PKCE 방식 로테이션 사용 토큰 수명: 액세스 토큰은 수명이 짧음; 리프레시 토큰은 자동으로 로테이션

토큰 교환 다이어그램:

1. 판매자가 Shopify Admin에서 "설치" 클릭
2. Shopify 리디렉션: https://your-app.com/auth/callback?code=EXCHANGE_TOKEN
3. 앱이 HMAC 검증, 코드를 액세스 토큰으로 교환 (비공개, 서버 측만)
4. Shopify Admin API가 범위 부여; 토큰을 세션/데이터베이스에 저장
5. 만료 시 다음 요청에서 토큰이 자동으로 갱신됨

Remix 구현 (토큰 교환):

// shopify.app.ts (앱 구성)
import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { restResources } from '@shopify/shopify-api/rest/admin/2026-07';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY || '',
  apiSecret: process.env.SHOPIFY_API_SECRET || '',
  scopes: process.env.SCOPES?.split(',') || [
    'write_products',
    'read_products',
    'write_orders',
    'read_orders',
    'write_customers',
    'read_customers',
    'write_discounts',
    'read_discounts',
    'write_fulfillments',
    'read_fulfillments',
    'write_inventory',
    'read_inventory',
  ],
  appUrl: process.env.SHOPIFY_APP_URL || 'http://localhost:3000',
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
  webhooks: {
    path: '/webhooks',
  },
  isEmbeddedApp: true, // Polaris 관리자 대시보드
  sessionStorage: new PrismaSessionStorage(prisma),
  restResources, // REST API 헬퍼 포함
});

export default shopify;

환경 변수 (.env):

SHOPIFY_API_KEY=partner-dashboard에서 받은 공개 API 키
SHOPIFY_API_SECRET=비공개 API 시크릿
SHOPIFY_APP_URL=https://your-domain.ngrok.io  # 또는 프로덕션 URL
SCOPES=write_products,read_products,write_orders,read_orders

인증 라우트 (routes/auth.$.tsx - 모든 경로 처리):

import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { authenticate } = await import('~/shopify.server');
  return authenticate.admin(request); // 여기서 토큰 교환 발생
};

콜백 처리 (routes/auth.callback.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  // 세션에는 다음이 포함:
  // - session.accessToken (API 호출에 유효)
  // - session.shop (판매자의 상점 도메인)
  // - session.scope (부여된 범위)
  // - session.state (선택적 커스텀 데이터)

  return redirect('/app'); // 인증 후 대시보드로 리디렉션
};

API 호출에서 토큰 사용 (routes/app.products.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { GraphQLClient } from 'graphql-request';

export const loader = async ({ request }) => {
  const { session, admin } = await authenticate.admin(request);

  // 방법 1: Shopify의 admin 헬퍼 사용 (권장)
  const response = await admin.graphql(`
    query GetProducts {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
          }
        }
      }
    }
  `);

  // 방법 2: 수동 GraphQL 호출
  const client = new GraphQLClient(
    `https://${session.shop}/admin/api/2026-07/graphql.json`,
    {
      headers: {
        'X-Shopify-Access-Token': session.accessToken,
        'Content-Type': 'application/json',
      },
    }
  );

  const data = await client.request(/* ... */);

  return json({ products: response.data?.products?.edges || [] });
};

토큰 갱신 (자동): 토큰 교환 토큰은 Shopify의 세션 미들웨어를 통해 자동 갱신됩니다. 수동 갱신 불필요:

// 각 요청에서 토큰이 투명하게 갱신됨
const response = await admin.graphql(query); // 내부적으로 갱신 처리

2. Managed Installation (헤드리스/커스텀 앱)

사용 사례: 헤드리스 스토어프론트, 모바일 앱, 타사 통합 흐름: 판매자가 앱 승인 → Shopify가 영구 액세스 토큰 생성 (시크릿 로테이션 없음) 토큰 수명: 수명이 긺; 갱신 불필요 보안: 토큰은 영구적임; 환경 변수에 안전하게 저장

Managed Installation 설정:

Shopify Partner 대시보드에서:

  1. 앱 설정 > API 자격 증명
  2. Admin API 액세스 범위에서 "Managed installation" 선택
  3. 판매자가 한 번 권한 부여
  4. 액세스 토큰을 환경에 복사
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=example-shop.myshopify.com

Managed Installation 토큰 사용:

import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient(
  `https://${process.env.SHOPIFY_SHOP_URL}/admin/api/2026-07/graphql.json`,
  {
    headers: {
      'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '',
    },
  }
);

// 토큰은 만료되지 않음; 언제든지 API 호출 가능
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);

3. OAuth 2.0 인가 코드 승인 (레거시, 여전히 지원)

사용 사례: 전통적인 OAuth 흐름을 사용하는 공개 앱 흐름: 판매자가 "설치" 클릭 → 앱이 Shopify OAuth로 리디렉션 → 판매자 승인 → 앱이 코드 수신 → 앱이 코드를 토큰으로 교환 토큰 수명: 수명이 긴 액세스 토큰 (취소되지 않는 한 만료 없음) 보안: 클라이언트 시크릿 필요; PKCE 선택 사항

OAuth 흐름 다이어그램:

1. 판매자 방문: https://your-app.com/auth
2. 앱 리디렉션: https://your-shop.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://your-app.com/auth/callback&state=RANDOM
3. 판매자가 Shopify Admin에서 앱 승인
4. Shopify 리디렉션: https://your-app.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=your-shop.myshopify.com
5. 앱이 HMAC 및 state 검증
6. 앱이 코드를 토큰으로 교환 (서버 측, 클라이언트 시크릿 사용)
7. 앱이 토큰을 데이터베이스에 저장

Express.js OAuth 예제:

import express from 'express';
import axios from 'axios';
import crypto from 'crypto';

const app = express();

const API_KEY = process.env.SHOPIFY_API_KEY || '';
const API_SECRET = process.env.SHOPIFY_API_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'https://your-app.com/auth/callback';
const SCOPES = 'write_products,read_products';

// 1단계: 판매자를 Shopify OAuth로 리디렉션
app.get('/auth', (req, res) => {
  const shop = req.query.shop as string;

  if (!shop || !shop.includes('.myshopify.com')) {
    return res.status(400).send('상점 매개변수가 없거나 유효하지 않습니다');
  }

  const state = crypto.randomBytes(16).toString('hex');
  const nonce = crypto.randomBytes(16).toString('hex');

  // 검증을 위해 state를 세션에 저장 (또는 데이터베이스)
  req.session.state = state;
  req.session.nonce = nonce;

  const authUrl = new URL(
    `/admin/oauth/authorize`,
    `https://${shop}`
  );
  authUrl.searchParams.append('client_id', API_KEY);
  authUrl.searchParams.append('scope', SCOPES);
  authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.append('state', state);

  res.redirect(authUrl.toString());
});

// 2단계: OAuth 콜백 처리
app.get('/auth/callback', async (req, res) => {
  const { code, hmac, shop, state } = req.query;

  // HMAC 검증
  const message = Object.entries(req.query)
    .filter(([key]) => key !== 'hmac')
    .map(([key, value]) => `${key}=${value}`)
    .sort()
    .join('&');

  const hash = crypto
    .createHmac('sha256', API_SECRET)
    .update(message, 'utf8')
    .digest('base64');

  if (hash !== hmac) {
    return res.status(401).send('승인되지 않은 요청이 감지되었습니다');
  }

  // state 검증
  if (state !== req.session.state) {
    return res.status(401).send('State 불일치');
  }

  try {
    // 코드를 액세스 토큰으로 교환
    const response = await axios.post(
      `https://${shop}/admin/oauth/access_token`,
      {
        client_id: API_KEY,
        client_secret: API_SECRET,
        code,
      }
    );

    const { access_token, scope } = response.data;

    // 액세스 토큰 저장 (지속성을 위해 세션이 아닌 데이터베이스에)
    await storeAccessToken(shop as string, access_token, scope);

    // 앱 대시보드로 리디렉션
    res.redirect(`/app?shop=${shop}`);
  } catch (error) {
    console.error('토큰 교환 오류:', error);
    res.status(500).send('인증 실패');
  }
});

// 토큰 저장 헬퍼 함수
async function storeAccessToken(shop: string, token: string, scope: string) {
  // 데이터베이스에 저장 (예시: mock 저장소 사용)
  const db = {
    shops: {} as Record<string, { token: string; scope: string }>,
  };
  db.shops[shop] = { token, scope };
}

app.listen(3000);

세션 스토리지 어댑터

액세스 토큰은 영구적으로 저장되어야 합니다. Shopify는 일반적인 스토리지 백엔드용 어댑터를 제공합니다:

Prisma (Remix에 권장)

스키마 (prisma/schema.prisma):

model Session {
  id        String    @id
  shop      String
  state     String
  isOnline  Boolean   @default(false)
  accessToken String
  refreshToken String?
  scope     String
  expiresAt DateTime?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@unique([shop, state])
  @@index([shop])
}

설정 (shopify.app.ts):

import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  // ...
  sessionStorage: new PrismaSessionStorage(prisma),
});

Redis

import { RedisSessionStorage } from '@shopify/shopify-app-session-storage-redis';
import redis from 'redis';

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
});

const sessionStorage = new RedisSessionStorage({
  client: redisClient,
  prefix: 'shopify_session:',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

인메모리 (개발 전용)

import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';

const sessionStorage = new MemorySessionStorage();

const shopify = shopifyApp({
  // ...
  sessionStorage, // 주의: 앱 재시작 시 세션 손실; 개발 전용
});

DynamoDB

import { DynamoDBSessionStorage } from '@shopify/shopify-app-session-storage-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const dynamoDBClient = new DynamoDBClient({ region: 'us-east-1' });

const sessionStorage = new DynamoDBSessionStorage({
  client: dynamoDBClient,
  tableName: 'shopify-sessions',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

온라인 토큰 vs 오프라인 토큰

온라인 토큰:

  • 범위: 현재 사용자의 권한 (일반적으로 관리자 사용자)
  • 만료: 24시간
  • 사용 사례: 브라우저 기반 작업 (Polaris 관리자 대시보드)
  • 제한 사항: 백그라운드 작업 실행 불가; 사용자 로그아웃 시 제한됨

오프라인 토큰:

  • 범위: 앱에 부여된 범위
  • 만료: 없음; 취소될 때까지 영구적
  • 사용 사례: 백그라운드 작업, 웹훅, 예약된 작업
  • 제한 사항: 없음; 앱 작업에 기본적으로 사용

토큰 교환에서 오프라인 토큰 요청:

// shopify.app.ts
const shopify = shopifyApp({
  // ...
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
});

// Remix는 기본적으로 오프라인 토큰을 자동으로 요청
// 별도 조치 불필요; API 호출에 session.accessToken 사용

웹훅에서 오프라인 토큰 사용:

// webhooks/products-update.ts
export const webhooks = {
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  PRODUCTS_UPDATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-update',
  },
};

export default defineWebhooksConfig(
  async (request, { admin, session }) => {
    const { body, query } = await graphql.query(request, {
      query: GET_PRODUCT,
      variables: { id: 'gid://shopify/Product/123' },
    });

    // session.accessToken은 오프라인 토큰; 여기서 유효함
    console.log(`상점 ${session.shop}의 토큰으로 웹훅 처리됨`);
  },
  webhooksConfig
);

앱 프록시 인증

사용 사례: 스토어프론트(공개)에서 앱 백엔드로의 요청 인증: HMAC 서명 검증 (웹훅과 유사) 흐름: 스토어프론트 → Liquid 프록시 요청 → 앱 백엔드(HMAC 검증) → 응답

앱 프록시 설정 (shopify.app.toml):

[[extensions]]
type = "app_proxy"
name = "Storefront API"
url = "/api/proxy"
subpath = "loyalty"  # /apps/loyalty/* 요청이 여기로 라우팅됨

앱 프록시 핸들러 (Remix routes/api/proxy.ts):

import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';

export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const hmac = url.searchParams.get('hmac') || '';
  const timestamp = url.searchParams.get('_t') || '';
  const shop = url.searchParams.get('shop') || '';

  // HMAC 검증
  const sortedParams = new URLSearchParams(url.searchParams);
  sortedParams.delete('hmac');
  sortedParams.sort();
  const message = sortedParams.toString();
  const hash = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
    .update(message, 'utf8')
    .digest('hex');

  if (hash !== hmac) {
    return json({ error: 'Invalid HMAC' }, { status: 401 });
  }

  // Storefront GraphQL API 호출 예시
  const response = await fetch(
    `https://${shop}/api/2026-07/graphql`,
    {
      method: 'POST',
      headers: {
        'X-Shopify-Storefront-Access-Token': process.env.STOREFRONT_API_TOKEN || '',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query: `{ products(first: 5) { edges { node { title } } } }` }),
    }
  );

  const data = await response.json();
  return json(data);
};

환경 변수 요약

변수설명필수
SHOPIFY_API_KEYShopify Partner 대시보드의 공개 API 키예
SHOPIFY_API_SECRETShopify Partner 대시보드의 비공개 API 시크릿예
SHOPIFY_APP_URL앱의 공개 URL (개발 시 ngrok URL)예
SCOPES쉼표로 구분된 권한 범위 목록예
SHOPIFY_ADMIN_ACCESS_TOKENManaged Installation용 영구 액세스 토큰조건부
STOREFRONT_API_TOKENStorefront API용 공개 토큰 (앱 프록시용)조건부

인증 방식 선택 가이드

앱 유형권장 인증 방식
Remix/Next.js SSR 앱토큰 교환 (기본값)
단일 테넌트 헤드리스 앱Managed Installation
다중 테넌트 공개 앱OAuth 2.0 (레거시) 또는 토큰 교환 (2024+)
CLI/봇/통합Managed Installation
모바일 앱Managed Installation (토큰은 앱 내에 저장)

문제 해결

일반적인 문제

  1. HMAC 검증 실패

    • 모든 쿼리 매개변수가 올바르게 정렬되었는지 확인
    • Shopify API 시크릿이 올바른지 확인
    • 타임스탬프가 만료되지 않았는지 확인 (일반적으로 5분)
  2. 토큰 교환 실패

    • SHOPIFY_API_KEY 및 SHOPIFY_API_SECRET이 올바르게 설정되었는지 확인
    • 리디렉션 URI가 Partner 대시보드의 허용 목록과 일치하는지 확인
    • 세션 스토리지가 올바르게 구성되었는지 확인
  3. Managed Installation 토큰이 작동하지 않음

    • 토큰이 올바른 상점에 대한 것인지 확인
    • 상점 URL이 유효한 myshopify.com 도메인인지 확인
    • Admin API 범위가 올바르게 설정되었는지 확인
  4. API 속도 제한

    • Shopify Admin API는 앱당 초당 40개 요청으로 제한
    • Webhook과 백그라운드 작업은 더 높은 한도가 있음
    • 토큰 교환은 내장된 속도 제한 처리가 있음

관련 스킬