Skip to main content

앱 아키텍처

Shopify 앱을 구축하려면 인증 및 세션 관리부터 UI 렌더링과 이벤트 처리까지, 애플리케이션이 Shopify 플랫폼과 모든 레이어에서 어떻게 통합되는지 이해해야 합니다. 이 레슨에서는 프로덕션 수준의 Shopify 앱을 구축하기 위해 필요한 기초적인 아키텍처 결정과 패턴을 다룹니다.

앱 유형

Shopify는 세 가지 주요 앱 유형을 지원하며, 각각 다른 사용 사례에 적합합니다:

Public 앱

Public 앱은 Shopify App Store에 등록되며 모든 판매자가 설치할 수 있습니다. 앱 심사 프로세스를 거쳐야 하며 Shopify의 품질, 보안, 성능 요구 사항을 충족해야 합니다.

  • Shopify App Store를 통해 배포
  • 각 판매자와의 OAuth 기반 인증
  • 멀티테넌트 데이터 격리를 처리해야 함
  • Shopify와 수익 공유 (연간 첫 $1M에 대해 Shopify가 0%, 이후 일정 비율)

Custom 앱

Custom 앱은 단일 판매자 또는 조직을 위해 구축됩니다. App Store에 등록되지 않으며 Shopify Admin을 통해 직접 설치됩니다.

  • 특정 스토어 또는 Plus 조직을 위해 구축
  • 커스텀 앱 액세스 토큰 사용 (OAuth 흐름 불필요)
  • 단일 테넌트를 서비스하므로 더 간단한 아키텍처
  • App Store 심사 불필요

Draft 앱

Draft 앱은 아직 심사를 위해 제출되지 않은 개발 중인 앱입니다. 테스트를 위해 개발 스토어에 설치할 수 있습니다.

  • Partners Dashboard 또는 Shopify CLI를 통해 생성
  • 설치 URL을 통해 개발 스토어에 설치
  • 테스트 및 개발을 위한 전체 기능
  • 준비되면 public 또는 custom 앱으로 전환
올바른 유형 선택

특정 클라이언트를 위해 구축하는 경우 custom 앱으로 시작하십시오. 에코시스템을 위한 제품을 구축하는 경우 draft 앱으로 시작하고 준비되면 public으로 전환하십시오. 이 레슨의 아키텍처 패턴은 세 가지 유형 모두에 적용됩니다.

Remix 앱 템플릿

Shopify의 공식 앱 스캐폴드는 풀스택 React 프레임워크인 Remix를 기반으로 구축되어 있습니다. 템플릿은 즉시 필요한 모든 것을 제공합니다:

# Remix 템플릿으로 새 Shopify 앱 생성
shopify app init --template remix

# 스캐폴딩 후 프로젝트 구조
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # 메인 앱 페이지
│ │ ├── app.tsx # 네비게이션이 있는 앱 레이아웃
│ │ ├── auth.$.tsx # OAuth 콜백 핸들러
│ │ ├── auth.login/
│ │ │ └── route.tsx # 로그인 페이지
│ │ └── webhooks.tsx # Webhook 핸들러
│ ├── shopify.server.ts # Shopify API 설정
│ └── entry.server.tsx # 서버 진입점
├── extensions/ # 앱 확장
├── prisma/
│ └── schema.prisma # 데이터베이스 스키마
├── shopify.app.toml # 앱 설정
├── package.json
└── remix.config.js

주요 파일 설명

shopify.server.ts 파일이 Shopify 연동의 핵심입니다. 인증, API 접근, 세션 스토리지를 설정합니다:

// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
DeliveryMethod,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";

const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
apiVersion: ApiVersion.January25,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL!,
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session }) => {
// 성공적인 OAuth 후 웹훅 등록
shopify.registerWebhooks({ session });
},
},
});

export default shopify;
export const apiVersion = ApiVersion.January25;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;

App Bridge 4.x

App Bridge는 앱의 프론트엔드를 Shopify Admin에 연결하는 JavaScript 라이브러리입니다. 버전 4.x는 통합 모델을 단순화한 주요 개편입니다.

App Bridge의 기능

  • 네비게이션: 앱이 Shopify Admin iframe 내에 나타납니다. App Bridge가 네비게이션, 뒤로 가기 버튼, URL 동기화를 처리합니다.
  • 인증: 세션 토큰을 관리하고 API 요청이 올바르게 인증되도록 합니다.
  • UI 연동: 토스트, 모달, 컨텍스트 바와 같은 Shopify Admin UI 요소에 대한 접근을 제공합니다.
  • 리소스 피커: 판매자가 상품, 컬렉션, 기타 Shopify 리소스를 선택할 수 있는 내장 컴포넌트입니다.

Remix에서의 App Bridge

Remix 템플릿에서는 App Bridge가 자동으로 설정됩니다. app.tsx 레이아웃 컴포넌트가 앱을 감쌉니다:

// app/routes/app.tsx
import { Outlet } from "@remix-run/react";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export default function App() {
return (
<AppProvider isEmbeddedApp apiKey={window.ENV.SHOPIFY_API_KEY}>
<Outlet />
</AppProvider>
);
}
App Bridge 4.x 주요 변경 사항

App Bridge 4.x에서는 이전 버전에서 사용되던 createApp() 함수 패턴이 제거되었습니다. App Bridge 3.x에서 마이그레이션하는 경우 더 이상 명시적 앱 인스턴스를 생성하지 않습니다. 대신 앱이 Shopify Admin 내에서 로드될 때 App Bridge가 자동으로 초기화됩니다. 이전의 app.dispatch() 패턴은 직접 window.shopify 호출로 대체됩니다.

Polaris 디자인 시스템

Polaris는 Shopify의 디자인 시스템으로, Shopify Admin의 모양과 느낌에 맞는 React 컴포넌트를 제공합니다. Polaris를 사용하면 앱이 판매자에게 네이티브처럼 느껴집니다.

// app/routes/app._index.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import {
Page,
Layout,
Card,
BlockStack,
Text,
DataTable,
Badge,
Button,
InlineStack,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";

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

const response = await admin.graphql(`
query {
orders(first: 10, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
displayFulfillmentStatus
createdAt
}
}
}
}
`);

const data = await response.json();
return json({ orders: data.data.orders.edges.map((e: any) => e.node) });
};

export default function Index() {
const { orders } = useLoaderData<typeof loader>();

const rows = orders.map((order: any) => [
order.name,
`${order.totalPriceSet.shopMoney.amount} ${order.totalPriceSet.shopMoney.currencyCode}`,
order.displayFulfillmentStatus,
new Date(order.createdAt).toLocaleDateString(),
]);

return (
<Page title="대시보드" primaryAction={{ content: "데이터 동기화" }}>
<Layout>
<Layout.Section>
<Card>
<BlockStack gap="300">
<InlineStack align="space-between">
<Text as="h2" variant="headingMd">
최근 주문
</Text>
<Badge tone="success">실시간</Badge>
</InlineStack>
<DataTable
columnContentTypes={["text", "numeric", "text", "text"]}
headings={["주문", "합계", "이행 상태", "날짜"]}
rows={rows}
/>
</BlockStack>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
Polaris 버전

Remix 템플릿은 Polaris 12.x+와 함께 제공됩니다. 업그레이드 시 항상 Polaris 마이그레이션 가이드를 확인하십시오. 주요 버전 간에 컴포넌트 API가 변경됩니다. BlockStackInlineStack 컴포넌트는 Polaris 12에서 기존의 Stack 컴포넌트를 대체했습니다.

세션 관리

Shopify 앱은 인증된 판매자를 위한 세션을 관리해야 합니다. 세션에는 스토어 도메인, 액세스 토큰, 권한 스코프가 포함됩니다. Remix 템플릿은 Prisma 기반 세션 스토리지를 통해 이를 처리합니다.

세션 흐름

세션 스토리지 설정

// prisma/schema.prisma
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?

@@index([shop])
}
액세스 토큰 보안

Shopify 액세스 토큰을 절대 로그에 기록하거나, 클라이언트 측 코드에 노출하거나, 쿠키에 저장하지 마십시오. 액세스 토큰은 앱의 스코프 내에서 판매자 스토어에 대한 전체 API 접근 권한을 부여합니다. 데이터베이스에 저장할 때 암호화하고 서버 측에서만 접근하십시오.

Webhooks와 이벤트 기반 아키텍처

Webhooks는 판매자 스토어에서 이벤트가 발생할 때 Shopify가 앱으로 보내는 HTTP 콜백입니다. 반응형 앱 동작의 기반입니다.

Webhooks 등록

Webhooks는 shopify.server.ts에서 선언되며 OAuth 후 자동으로 등록됩니다:

// shopify.server.ts 웹훅 설정에서
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_DATA_REQUEST: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
SHOP_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},

Webhooks 처리

// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";

export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, session, admin, payload } =
await authenticate.webhook(request);

switch (topic) {
case "APP_UNINSTALLED":
if (session) {
await db.session.deleteMany({ where: { shop } });
}
// 이 스토어의 앱 데이터 정리
await db.appData.deleteMany({ where: { shop } });
break;

case "ORDERS_CREATE":
// 새 주문 처리
await processNewOrder(shop, payload);
break;

case "PRODUCTS_UPDATE":
// 상품 변경 동기화
await syncProductUpdate(shop, payload);
break;

case "CUSTOMERS_DATA_REQUEST":
// GDPR: 고객 데이터 반환
await handleDataRequest(shop, payload);
break;

case "CUSTOMERS_REDACT":
// GDPR: 고객 데이터 삭제
await handleCustomerRedact(shop, payload);
break;

case "SHOP_REDACT":
// GDPR: 모든 스토어 데이터 삭제
await handleShopRedact(shop, payload);
break;

default:
console.warn(`처리되지 않은 webhook 주제: ${topic}`);
}

return new Response(null, { status: 200 });
};
필수 GDPR Webhooks

모든 Shopify 앱은 세 가지 GDPR 웹훅을 반드시 처리해야 합니다: CUSTOMERS_DATA_REQUEST, CUSTOMERS_REDACT, SHOP_REDACT. 이들이 올바르게 구현되지 않으면 App Store 심사에 실패합니다. 앱이 고객 데이터를 저장하지 않더라도 이러한 웹훅에 200 응답으로 응답해야 합니다.

Webhook 모범 사례

  1. 항상 빠르게 200으로 응답 -- Webhook 페이로드를 비동기적으로 처리합니다. Shopify는 5초 이내의 응답을 기대합니다.
  2. 멱등성 처리 -- Shopify는 동일한 웹훅을 여러 번 보낼 수 있습니다. X-Shopify-Webhook-Id 헤더를 사용하여 중복을 제거합니다.
  3. HMAC 서명 검증 -- authenticate.webhook() 호출이 이를 처리하지만, 커스텀 핸들러를 구축하는 경우 항상 X-Shopify-Hmac-SHA256 헤더를 검증하십시오.
  4. 전달 실패 모니터링 -- Shopify는 실패한 웹훅을 48시간 동안 재시도합니다. 엔드포인트가 지속적으로 실패하면 Shopify가 등록을 제거합니다.

아키텍처 결정 다이어그램

다음 단계

이제 아키텍처 기초를 이해했으므로 Admin UI Extensions로 이동하여 커스텀 UI 컴포넌트로 Shopify Admin을 확장하는 방법을 알아보십시오.