Skip to main content

성능 최적화

Shopify 에코시스템에서 성능은 선택 사항이 아닙니다. Shopify는 앱 심사 중 엄격한 성능 요구 사항을 적용하며, 판매자는 스토어프론트를 느리게 만드는 앱을 제거합니다. 이 모듈에서는 설계부터 빠른 앱을 구축하는 방법, 실제 성능 메트릭을 모니터링하는 방법, 애플리케이션 스택의 모든 레이어를 최적화하는 방법을 배웁니다.

Web Vitals 모니터링

Google의 Core Web Vitals는 사용자 체감 성능을 측정하기 위한 업계 표준입니다. Shopify는 스토어프론트에 대해 이러한 메트릭을 추적하고 성능을 저하시키는 앱에 페널티를 부여합니다.

세 가지 핵심 Web Vitals

지표측정 대상양호 기준값
LCP (Largest Contentful Paint)로딩 성능< 2.5초
INP (Interaction to Next Paint)상호작용 응답성< 200ms
CLS (Cumulative Layout Shift)시각적 안정성< 0.1
지역별 성능 데이터 (Winter '26)

Winter '26 릴리스부터 Shopify는 Partner Dashboard에서 지역별 Web Vitals 데이터를 제공합니다. 이제 앱이 다양한 지리적 지역에서 어떤 성능을 보이는지 확인할 수 있어, CDN 커버리지 격차나 특정 데이터 센터의 느린 API 엔드포인트와 같은 지역별 병목 현상을 식별하고 수정할 수 있습니다.

Web Vitals 추적 구현

앱의 스토어프론트 컴포넌트에 실제 사용자 모니터링(RUM)을 추가합니다:

// web-vitals-monitor.js
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
shopDomain: window.Shopify?.shop,
});

// Use sendBeacon for reliability during page unload
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/vitals', body);
} else {
fetch('/api/analytics/vitals', {
body,
method: 'POST',
keepalive: true,
});
}
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
warning

판매자의 스토어프론트에 동기 JavaScript를 절대 주입하지 마세요. 모든 스토어프론트 스크립트는 defer 또는 async 속성을 사용하여 비동기적으로 로드하거나, Shopify의 Script Tag API를 통해 display_scope를 적절히 설정하여 로드해야 합니다.

성능 예산

앱의 스토어프론트 영향에 대한 성능 예산을 수립합니다:

// performance-budget.config.js
export default {
budgets: [
{
path: '/apps/your-app/*',
resourceSizes: [
{ resourceType: 'script', budget: 50 }, // 50 KB max JS
{ resourceType: 'stylesheet', budget: 20 }, // 20 KB max CSS
{ resourceType: 'image', budget: 100 }, // 100 KB max images
{ resourceType: 'total', budget: 200 }, // 200 KB total
],
resourceCounts: [
{ resourceType: 'third-party', budget: 3 }, // Max 3 third-party requests
],
},
],
};

지연 로딩과 코드 분할

Shopify 앱은 복잡한 관리 인터페이스를 포함하는 경우가 많습니다. 모든 것을 미리 로드하면 대역폭을 낭비하고 초기 렌더링이 느려집니다.

React를 사용한 라우트 기반 코드 분할

// app/routes.jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { SkeletonPage } from '@shopify/polaris';

// Lazy load heavy route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const ProductSync = lazy(() => import('./pages/ProductSync'));
const Analytics = lazy(() => import('./pages/Analytics'));
const BulkEditor = lazy(() => import('./pages/BulkEditor'));

function AppRoutes() {
return (
<Suspense fallback={<SkeletonPage primaryAction />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/sync" element={<ProductSync />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/bulk-edit" element={<BulkEditor />} />
</Routes>
</Suspense>
);
}

컴포넌트 레벨 지연 로딩

페이지 내의 무거운 컴포넌트에는 Intersection Observer 패턴을 사용합니다:

// components/LazyChart.jsx
import { useEffect, useRef, useState } from 'react';

export function LazyChart({ data }) {
const containerRef = useRef(null);
const [ChartComponent, setChartComponent] = useState(null);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
import('./HeavyChartLibrary').then((mod) => {
setChartComponent(() => mod.Chart);
});
observer.disconnect();
}
},
{ rootMargin: '200px' } // Start loading 200px before visible
);

if (containerRef.current) {
observer.observe(containerRef.current);
}

return () => observer.disconnect();
}, []);

return (
<div ref={containerRef} style={{ minHeight: 300 }}>
{ChartComponent ? <ChartComponent data={data} /> : <SkeletonChart />}
</div>
);
}

스토어프론트 컴포넌트를 위한 이미지 최적화

앱이 스토어프론트에서 이미지를 렌더링할 때는 항상 Shopify의 이미지 CDN 변환을 사용하세요:

{% comment %} Use Shopify's image_url filter for automatic CDN optimization {% endcomment %}
{% assign optimized_url = image | image_url: width: 400, height: 400, crop: 'center' %}
<img
src="{{ optimized_url }}"
loading="lazy"
decoding="async"
width="400"
height="400"
alt="{{ image.alt | escape }}"
/>

CDN 및 캐싱 전략

HTTP 캐싱 헤더

앱 서버가 적절한 캐시 헤더를 반환하도록 구성합니다:

// middleware/caching.js
export function cachingMiddleware(req, res, next) {
const path = req.path;

if (path.startsWith('/assets/')) {
// Static assets: cache for 1 year with immutable
res.set('Cache-Control', 'public, max-age=31536000, immutable');
} else if (path.startsWith('/api/storefront/')) {
// Storefront API responses: short cache with stale-while-revalidate
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
} else if (path.startsWith('/api/admin/')) {
// Admin API responses: no cache (contains merchant-specific data)
res.set('Cache-Control', 'private, no-cache, no-store');
}

next();
}

GraphQL 응답을 위한 Redis 캐싱

자주 접근하는 Shopify API 데이터를 캐시하여 지연 시간과 API 속도 제한 소비를 줄입니다:

// services/cached-shopify-client.js
import { createClient } from 'redis';
import crypto from 'crypto';

const redis = createClient({ url: process.env.REDIS_URL });

export async function cachedGraphQLQuery(session, query, variables, ttl = 300) {
const cacheKey = `shopify:${session.shop}:${crypto
.createHash('md5')
.update(JSON.stringify({ query, variables }))
.digest('hex')}`;

// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// Fetch from Shopify
const client = new shopify.clients.Graphql({ session });
const response = await client.query({ data: { query, variables } });

// Cache the response
await redis.setEx(cacheKey, ttl, JSON.stringify(response.body));

return response.body;
}
tip

데이터 변동성에 따라 다른 TTL 값을 사용하세요. 상품 데이터는 5분 동안 캐시할 수 있지만, 재고 수준의 TTL은 30초 이하여야 합니다. Webhook 기반 캐시 무효화가 골드 스탠다드입니다 -- products/update와 같은 관련 Webhook을 수신하면 캐시된 데이터를 무효화하세요.

데이터베이스 쿼리 최적화

인덱싱 전략

앱의 데이터베이스는 주요 병목 지점이 되는 경우가 많습니다. WHERE 절과 JOIN 조건에서 사용되는 컬럼에는 항상 인덱스를 생성하세요:

-- Essential indexes for a typical Shopify app
CREATE INDEX idx_shops_domain ON shops(shopify_domain);
CREATE INDEX idx_sessions_shop_id ON sessions(shop_id);
CREATE INDEX idx_sync_jobs_shop_status ON sync_jobs(shop_id, status);
CREATE INDEX idx_products_shop_shopify_id ON products(shop_id, shopify_product_id);

-- Composite index for common query patterns
CREATE INDEX idx_orders_shop_date ON orders(shop_id, created_at DESC);

Prisma를 사용한 N+1 쿼리 방지

// BAD: N+1 queries
const shops = await prisma.shop.findMany();
for (const shop of shops) {
// This runs a separate query for EACH shop
const products = await prisma.product.findMany({
where: { shopId: shop.id },
});
}

// GOOD: Eager loading with include
const shops = await prisma.shop.findMany({
include: {
products: {
where: { syncStatus: 'active' },
take: 100,
},
},
});

연결 풀링

호스팅 환경에 따라 데이터베이스 연결 풀을 구성합니다:

// prisma/schema.prisma connection string with pooling
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// For serverless environments (Vercel, AWS Lambda)
// Use PgBouncer or Prisma Accelerate for connection pooling
}
danger

서버리스 환경은 콜드 스타트 시마다 새로운 데이터베이스 연결을 생성합니다. 연결 풀링이 없으면 트래픽 급증 시 데이터베이스의 연결 제한을 소진할 수 있습니다. 서버리스 플랫폼에 배포할 때는 항상 PgBouncer, Supabase의 내장 풀러, 또는 Prisma Accelerate와 같은 외부 연결 풀러를 사용하세요.

Shopify의 앱 심사 성능 요구 사항

Shopify의 앱 심사팀은 성능을 합격/불합격 기준으로 평가합니다. 이러한 요구 사항을 충족하지 못하면 앱이 거절됩니다.

주요 요구 사항

  1. 동기 스크립트 주입 금지 -- 모든 JavaScript는 비동기적으로 로드해야 합니다
  2. 최소한의 스토어프론트 영향 -- 앱은 페이지 로드 시간에 100ms 미만만 추가해야 합니다
  3. 효율적인 API 사용 -- 속도 제한 내에 여유를 두고 유지; 대규모 데이터셋에는 벌크 작업 사용
  4. 레이아웃 시프트 없음 -- 동적으로 로드되는 콘텐츠를 위한 공간 예약
  5. 빠른 관리자 로딩 -- Shopify 관리자 내의 앱 페이지는 3초 이내에 로드

제출 전 성능 체크리스트

# Run Lighthouse audit against your app's storefront components
npx lighthouse https://test-store.myshopify.com \
--only-categories=performance \
--output=json \
--output-path=./lighthouse-report.json

# Check bundle sizes
npx webpack-bundle-analyzer stats.json

# Profile API calls
shopify app dev --verbose 2>&1 | grep "GraphQL"
info

Shopify는 Partner Dashboard에서 스토어프론트 성능 대시보드를 제공하여, 앱이 설치된 스토어의 Web Vitals에 어떤 영향을 미치는지 정확히 보여줍니다. 앱 심사 전뿐만 아니라 출시 후에도 이 대시보드를 지속적으로 모니터링하세요.

최적화 체크리스트 요약

영역작업우선순위
JavaScript비동기 로딩, 코드 분할, 트리 셰이킹최우선
이미지Shopify CDN 변환, 지연 로딩, WebP최우선
API 호출캐싱, 배치 처리, 벌크 작업높음
데이터베이스인덱싱, 연결 풀링, 쿼리 최적화높음
CSS크리티컬 CSS 인라인화, 비동기 스타일시트 로딩중간
폰트font-display: swap, 크리티컬 폰트 프리로드중간
모니터링RUM, 오류 추적, 알림지속적

성능 최적화는 일회성 작업이 아니라 지속적인 원칙입니다. 첫날부터 앱에 모니터링을 구축하고, 성능 예산을 설정하며, 성능 회귀를 릴리스를 차단하는 버그로 취급하세요. 판매자는 수익을 위해 빠른 스토어프론트에 의존하며, 앱은 이 제약을 존중해야 합니다.