Skip to main content

Admin UI Extensions

Admin UI extensions를 사용하면 Shopify Admin에 커스텀 인터페이스를 직접 삽입할 수 있습니다. 판매자가 앱의 전체 페이지로 이동하도록 강제하는 대신, 판매자가 필요한 곳 -- 상품 페이지, 주문 상세 보기, 고객 프로필 등 -- 에 기능을 정확히 표시할 수 있습니다. 이러한 확장은 Shopify의 UI 컴포넌트에 접근할 수 있는 샌드박스 환경을 사용하여 Shopify Admin 내에서 렌더링됩니다.

확장 유형 개요

Shopify는 여러 카테고리의 Admin UI 확장을 제공합니다:

확장 유형표시 위치사용 사례
Admin Action리소스 페이지의 액션 메뉴컨텍스트 메뉴에서 워크플로우 트리거
Admin Block리소스 상세 페이지에 임베디드Shopify 데이터와 함께 앱 데이터 표시
Product Configuration상품 생성/편집 양식상품 설정에 커스텀 필드 추가
NavigationAdmin 사이드바앱 섹션으로 딥 링크
Bulk Action리소스 목록 페이지여러 항목에 대해 한 번에 작업

Admin Action Extensions

Admin actions는 리소스 페이지의 "추가 작업" 드롭다운에 나타납니다. 확장이 렌더링되는 모달 오버레이가 열립니다.

Admin Action 생성

# admin action 확장 생성
shopify app generate extension --template admin_action --name ai-description-generator

확장 스캐폴드가 생성됩니다:

extensions/ai-description-generator/
├── src/
│ └── ActionExtension.tsx
├── locales/
│ └── en.default.json
├── shopify.extension.toml
└── package.json

확장 설정

# extensions/ai-description-generator/shopify.extension.toml
api_version = "2025-01"

[[extensions]]
type = "admin_action"
name = "Generate AI Description"
handle = "ai-description-generator"

[[extensions.targeting]]
module = "./src/ActionExtension.tsx"
target = "admin.product-details.action.render"
확장 대상

target 필드는 확장이 나타나는 위치를 결정합니다. 일반적인 대상은 다음과 같습니다:

  • admin.product-details.action.render -- 상품 상세 페이지 액션
  • admin.order-details.action.render -- 주문 상세 페이지 액션
  • admin.customer-details.action.render -- 고객 상세 페이지 액션
  • admin.product-index.action.render -- 상품 목록 페이지 액션

액션 UI 구축

Admin 확장은 Shopify의 원격 UI 컴포넌트를 사용합니다 -- 샌드박스 확장 환경을 위해 설계된 Polaris 컴포넌트의 하위 집합입니다:

// extensions/ai-description-generator/src/ActionExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
BlockStack,
Button,
InlineStack,
ProgressIndicator,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.action.render';

export default reactExtension(TARGET, () => <GenerateDescriptionAction />);

function GenerateDescriptionAction() {
const { data, close, intents } = useApi(TARGET);
const [tone, setTone] = useState('professional');
const [generatedDescription, setGeneratedDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const productId = data.selected?.[0]?.id;

async function handleGenerate() {
setLoading(true);
setError(null);

try {
const response = await fetch('/api/generate-description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
tone,
}),
});

if (!response.ok) {
throw new Error('Failed to generate description');
}

const result = await response.json();
setGeneratedDescription(result.description);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}

async function handleApply() {
try {
// 확장에서 직접 Admin API 사용
const response = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
mutation updateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
descriptionHtml
}
userErrors {
field
message
}
}
}
`,
variables: {
input: {
id: productId,
descriptionHtml: generatedDescription,
},
},
}),
});

close();
} catch (err) {
setError('Failed to update product');
}
}

return (
<AdminAction
title="Generate AI Description"
primaryAction={
<Button
onPress={handleApply}
disabled={!generatedDescription}
>
Apply Description
</Button>
}
secondaryAction={
<Button onPress={() => close()}>Cancel</Button>
}
>
<BlockStack gap="large">
<Select
label="Tone"
value={tone}
onChange={setTone}
options={[
{ label: 'Professional', value: 'professional' },
{ label: 'Casual & Friendly', value: 'casual' },
{ label: 'Luxurious', value: 'luxurious' },
{ label: 'Technical', value: 'technical' },
{ label: 'Playful', value: 'playful' },
]}
/>

<Button onPress={handleGenerate} disabled={loading}>
{loading ? 'Generating...' : 'Generate Description'}
</Button>

{loading && <ProgressIndicator size="small" />}

{error && (
<Text tone="critical">{error}</Text>
)}

{generatedDescription && (
<BlockStack gap="small">
<Text variant="headingSm">Generated Description</Text>
<TextField
label="Description"
value={generatedDescription}
onChange={setGeneratedDescription}
multiline={6}
autoComplete="off"
/>
</BlockStack>
)}
</BlockStack>
</AdminAction>
);
}
확장 샌드박싱

Admin 확장은 샌드박스 환경에서 실행됩니다. window, document에 접근하거나 임의의 npm 패키지를 사용할 수 없습니다. Shopify가 제공하는 UI 컴포넌트와 API만 사용할 수 있습니다. 백엔드를 호출해야 하는 경우 상대 URL로 fetch()를 사용하십시오 -- Shopify가 요청을 앱 서버로 프록시합니다.

Admin Block Extensions

Admin 블록은 리소스 상세 페이지에 나타나는 영구적인 UI 패널입니다. 액션(모달로 열리는)과 달리 블록은 항상 보이며 실시간으로 업데이트됩니다.

// extensions/loyalty-block/src/BlockExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
InlineStack,
Text,
Badge,
Divider,
ProgressIndicator,
Heading,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.customer-details.block.render';

export default reactExtension(TARGET, () => <LoyaltyBlock />);

function LoyaltyBlock() {
const { data } = useApi(TARGET);
const [loyaltyData, setLoyaltyData] = useState<any>(null);
const [loading, setLoading] = useState(true);

const customerId = data.selected?.[0]?.id;

useEffect(() => {
async function fetchLoyaltyData() {
try {
const response = await fetch(
`/api/loyalty/customer?id=${encodeURIComponent(customerId)}`
);
const result = await response.json();
setLoyaltyData(result);
} catch (err) {
console.error('Failed to fetch loyalty data:', err);
} finally {
setLoading(false);
}
}

if (customerId) {
fetchLoyaltyData();
}
}, [customerId]);

if (loading) {
return (
<AdminBlock title="Loyalty Program">
<ProgressIndicator size="small" />
</AdminBlock>
);
}

if (!loyaltyData) {
return (
<AdminBlock title="Loyalty Program">
<Text>Customer is not enrolled in the loyalty program.</Text>
</AdminBlock>
);
}

return (
<AdminBlock title="Loyalty Program">
<BlockStack gap="base">
<InlineStack gap="base" blockAlignment="center">
<Text variant="headingSm">Tier:</Text>
<Badge tone={loyaltyData.tier === 'Gold' ? 'warning' : 'info'}>
{loyaltyData.tier}
</Badge>
</InlineStack>

<Divider />

<InlineStack gap="large">
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">포인트 잔액</Text>
<Heading>{loyaltyData.pointsBalance.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">누적 포인트</Text>
<Heading>{loyaltyData.lifetimePoints.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">사용 횟수</Text>
<Heading>{loyaltyData.totalRedemptions}</Heading>
</BlockStack>
</InlineStack>

<Divider />

<Text variant="bodySm" tone="subdued">
가입일: {new Date(loyaltyData.enrolledAt).toLocaleDateString()}
</Text>
</BlockStack>
</AdminBlock>
);
}

Product Configuration Extension

상품 구성 확장은 상품 생성 및 편집 경험에 커스텀 필드를 추가합니다. 추가 상품 메타데이터가 필요한 앱에 적합합니다.

// extensions/product-ai-config/src/ProductConfig.tsx
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
Checkbox,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.configuration.render';

export default reactExtension(TARGET, () => <ProductAIConfig />);

function ProductAIConfig() {
const { data, applyMetafieldsChange } = useApi(TARGET);

const [autoDescription, setAutoDescription] = useState(false);
const [targetAudience, setTargetAudience] = useState('general');
const [keywords, setKeywords] = useState('');

async function handleAutoDescriptionToggle(checked: boolean) {
setAutoDescription(checked);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'auto_description',
value: String(checked),
valueType: 'boolean',
});
}

async function handleAudienceChange(value: string) {
setTargetAudience(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'target_audience',
value,
valueType: 'single_line_text_field',
});
}

async function handleKeywordsChange(value: string) {
setKeywords(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'seo_keywords',
value,
valueType: 'single_line_text_field',
});
}

return (
<AdminBlock title="AI Configuration">
<BlockStack gap="base">
<Text variant="bodySm" tone="subdued">
AI 도구가 이 상품과 상호작용하는 방식을 구성합니다.
</Text>

<Checkbox
label="저장 시 설명 자동 생성"
checked={autoDescription}
onChange={handleAutoDescriptionToggle}
/>

<Select
label="대상 고객"
value={targetAudience}
onChange={handleAudienceChange}
options={[
{ label: '일반', value: 'general' },
{ label: '젊은 성인 (18-25)', value: 'young_adults' },
{ label: '전문가', value: 'professionals' },
{ label: '부모', value: 'parents' },
{ label: '매니아', value: 'enthusiasts' },
]}
/>

<TextField
label="SEO 키워드"
value={keywords}
onChange={handleKeywordsChange}
helpText="AI 생성 콘텐츠에서 강조할 쉼표로 구분된 키워드"
autoComplete="off"
/>
</BlockStack>
</AdminBlock>
);
}

확장 배포

확장은 Shopify CLI를 사용하여 앱과 함께 배포됩니다:

# 개발 모드 -- 실시간 리로드
shopify app dev

# 프로덕션에 배포
shopify app deploy

# 확장만 배포 (앱 코드 건너뛰기)
shopify app deploy --reset

확장 버전 관리

각 배포는 확장의 새 버전을 생성합니다. Shopify가 배포를 관리합니다:

# 확장 상태 확인
shopify app versions list

# 특정 버전 릴리스
shopify app release --version 1.2.0
개발 워크플로우

개발 중에 shopify app dev는 터널을 시작하고 확장에 대한 핫 리로딩을 활성화합니다. 확장 코드 변경 사항은 재배포 없이 Shopify Admin에 즉시 반영됩니다. 테스트에는 개발 스토어(라이브 스토어가 아닌)를 사용하십시오.

배포 전 체크리스트

Admin UI 확장을 프로덕션에 배포하기 전에:

  1. 다양한 화면 크기에서 테스트 -- Admin은 데스크톱, 태블릿, 모바일 기기에서 사용됩니다
  2. 로딩 상태 처리 -- 데이터를 가져오는 동안 항상 스피너나 스켈레톤을 표시합니다
  3. 오류 상태 처리 -- 네트워크 장애가 발생합니다. 유용한 메시지와 재시도 옵션을 표시합니다
  4. 레이트 리밋 준수 -- Admin API에는 레이트 리밋이 있습니다. 가능한 경우 응답을 캐시합니다
  5. 문자열 현지화 -- 모든 사용자 대면 텍스트에 locales/ 디렉토리를 사용합니다
  6. 접근성 확인 -- 확장이 키보드로 탐색 가능하고 스크린 리더에 친화적인지 확인합니다
확장 크기 제한

Admin UI 확장의 최대 번들 크기는 512 KB(압축)입니다. 종속성을 최소화하십시오. 큰 라이브러리를 번들에 포함하지 마십시오 -- 자체 컴포넌트 라이브러리를 가져오는 대신 Shopify가 제공하는 UI 컴포넌트를 사용하십시오.

다음 단계

Checkout Extensions로 이동하여 커스텀 UI, 결제 로직, 배송 규칙으로 Shopify 결제 경험을 커스터마이즈하는 방법을 알아보십시오.