앱 배포
훌륭한 Shopify 앱을 구축하는 것은 전투의 절반에 불과합니다 -- 프로덕션에서 안정적으로 배포하고 운영하는 것이 나머지 절반입니다. 이 모듈에서는 호스팅 플랫폼 선택, 환경 변수 관리, GitHub Actions를 사용한 CI/CD 파이프라인 설정, 모니터링, 로깅, 오류 추적을 다룹니다. 마지막에는 모든 릴리스에 확신을 줄 수 있는 프로덕션 수준의 배포 파이프라인을 갖추게 됩니다.
호스팅 옵션
Shopify 앱은 HTTPS를 통해 공개적으로 접근 가능해야 하는 웹 애플리케이션입니다. 최적의 호스팅 플랫폼은 앱의 아키텍처, 트래픽 패턴, 예산에 따라 달라집니다.
플랫폼 비교
| 플랫폼 | 적합한 용도 | 시작 비용 | 콜드 스타트 | 데이터베이스 |
|---|---|---|---|---|
| Vercel | Remix/Next.js 앱, 서버리스 | 무료 티어 | 예 (서버리스) | 외부 필요 |
| Railway | 풀스택 앱, 간편 설정 | $5/월 | 아니요 | 내장 PostgreSQL |
| Render | 컨테이너, 백그라운드 워커 | 무료 티어 | 예 (무료 티어) | 내장 PostgreSQL |
| Fly.io | 저지연 글로벌 앱 | 사용량 기반 | 아니요 | 내장 PostgreSQL |
| AWS | 엔터프라이즈, 완전 제어 | 다양 | 서비스에 따라 다름 | RDS, DynamoDB |
Railway에 배포하기
Railway는 Shopify 앱에 탁월한 선택입니다. 장시간 실행 프로세스(웹훅 및 백그라운드 작업에 필요)를 지원하고 내장 PostgreSQL을 포함합니다:
# Install Railway CLI
npm install -g @railway/cli
# Login and initialize
railway login
railway init
# Add PostgreSQL
railway add --plugin postgresql
# Deploy
railway up
Railway용 Procfile을 구성하십시오:
web: npm run start
worker: npm run worker
Deploying to Fly.io
Fly.io는 저지연 글로벌 분산 배포가 필요할 때 이상적입니다:
# fly.toml
app = "your-shopify-app"
primary_region = "iad"
[build]
builder = "heroku/buildpacks:20"
[env]
NODE_ENV = "production"
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
[[services]]
protocol = "tcp"
internal_port = 8080
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[services.concurrency]
type = "connections"
hard_limit = 250
soft_limit = 200
[[services.http_checks]]
interval = 10000
grace_period = "5s"
method = "get"
path = "/health"
protocol = "http"
timeout = 2000
# Deploy to Fly.io
fly launch
fly deploy
# Scale to multiple regions
fly regions add lhr sin
fly scale count 2
Deploying to Vercel
Remix 기반 Shopify 앱의 경우 Vercel이 제로 설정 배포를 제공합니다:
// vercel.json
{
"framework": "remix",
"buildCommand": "npm run build",
"installCommand": "npm install",
"regions": ["iad1", "lhr1"],
"functions": {
"api/**/*.js": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/webhooks/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}
Vercel의 서버리스 함수에는 최대 실행 시간이 있습니다(Pro에서 30초, Enterprise에서 300초). 앱이 장시간 실행되는 웹훅이나 대량 작업을 처리하는 경우, Railway 또는 Fly.io와 같이 영구 프로세스를 지원하는 플랫폼을 사용하거나, 무거운 작업을 별도의 워커 서비스로 오프로드하십시오.
환경 변수
필수 환경 변수
모든 Shopify 앱은 프로덕션에서 이러한 환경 변수를 구성해야 합니다:
# .env.example (never commit actual .env files)
# Shopify App Credentials
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SCOPES=read_products,write_products,read_orders
# App Configuration
HOST=https://your-app.fly.dev
PORT=8080
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# Session Storage
REDIS_URL=redis://default:pass@host:6379
# Encryption (for access token storage)
ENCRYPTION_KEY=generate-a-32-byte-hex-string
# Error Tracking
SENTRY_DSN=https://key@sentry.io/project-id
# Logging
LOG_LEVEL=info
.env 파일을 절대 버전 관리에 커밋하지 마십시오. .gitignore에 .env*를 추가하십시오. 호스팅 플랫폼의 시크릿 관리(Railway 변수, Fly.io secrets, Vercel 환경 변수)를 사용하여 런타임에 주입하십시오.
안전한 암호화 키 생성
# Generate a 32-byte encryption key for AES-256
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
플랫폼별 시크릿 관리
# Fly.io
fly secrets set SHOPIFY_API_KEY=xxx SHOPIFY_API_SECRET=yyy
# Railway (via dashboard or CLI)
railway variables set SHOPIFY_API_KEY=xxx
# Vercel
vercel env add SHOPIFY_API_KEY production
CI/CD with GitHub Actions
적절한 CI/CD 파이프라인은 모든 배포가 일관되게 테스트, 빌드, 배포되도록 보장합니다.
완전한 GitHub Actions 워크플로우
# .github/workflows/deploy.yml
name: Test, Build, and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: shopify_app_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/shopify_app_test
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/shopify_app_test
SHOPIFY_API_KEY: ${{ secrets.TEST_SHOPIFY_API_KEY }}
SHOPIFY_API_SECRET: ${{ secrets.TEST_SHOPIFY_API_SECRET }}
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm audit --audit-level=high
- name: Check for secrets in code
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
deploy-staging:
needs: [lint-and-typecheck, unit-tests, security-audit]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --app your-app-staging
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Run smoke tests against staging
run: |
npm run test:smoke -- --base-url=https://your-app-staging.fly.dev
deploy-production:
needs: [deploy-staging]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://your-app.fly.dev
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --app your-app-production --strategy canary
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Verify deployment health
run: |
for i in 1 2 3 4 5; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://your-app.fly.dev/health)
if [ "$STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
sleep 10
done
echo "Health check failed"
exit 1
Shopify CLI Extension Deployment
앱에 Extension(테마 앱 Extension, Checkout UI Extension 등)이 포함되어 있는 경우 별도의 배포 단계를 추가하십시오:
deploy-extensions:
needs: [deploy-production]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Deploy Shopify extensions
run: npx shopify app deploy --force
env:
SHOPIFY_API_KEY: ${{ secrets.SHOPIFY_API_KEY }}
SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
프로덕션 배포에 필수 리뷰어가 있는 GitHub Actions 환경을 사용하십시오. 이를 통해 코드가 프로덕션에 도달하기 전에 수동 승인 게이트를 추가하여 팀이 스테이징 테스트 결과를 검토할 수 있습니다.
모니터링 및 로깅
상태 확인 엔드포인트
모든 프로덕션 앱에는 상태 확인 엔드포인트가 필요합니다:
// routes/health.js
export async function loader() {
const checks = {};
// Database connectivity
try {
await prisma.$queryRaw`SELECT 1`;
checks.database = 'healthy';
} catch (error) {
checks.database = 'unhealthy';
}
// Redis connectivity
try {
await redis.ping();
checks.redis = 'healthy';
} catch (error) {
checks.redis = 'unhealthy';
}
const allHealthy = Object.values(checks).every((v) => v === 'healthy');
return new Response(JSON.stringify({
status: allHealthy ? 'healthy' : 'degraded',
checks,
timestamp: new Date().toISOString(),
version: process.env.COMMIT_SHA || 'unknown',
}), {
status: allHealthy ? 200 : 503,
headers: { 'Content-Type': 'application/json' },
});
}
구조화된 로깅
// utils/logger.js
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
base: {
app: 'shopify-app',
env: process.env.NODE_ENV,
version: process.env.COMMIT_SHA,
},
});
// Usage
logger.info({ shop: 'example.myshopify.com', event: 'install' }, 'App installed');
logger.error({ err, shop, webhookTopic }, 'Webhook processing failed');
오류 추적
Sentry 연동
// utils/sentry.js
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.COMMIT_SHA,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
integrations: [
Sentry.prismaIntegration(),
Sentry.httpIntegration(),
],
beforeSend(event) {
// Scrub sensitive data
if (event.request?.headers) {
delete event.request.headers['x-shopify-access-token'];
delete event.request.headers['authorization'];
}
return event;
},
});
export { Sentry };
LogRocket을 활용한 세션 리플레이
앱의 Admin UI에서 판매자가 보고한 문제를 디버깅하는 경우:
// app/entry.client.jsx
import LogRocket from 'logrocket';
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
LogRocket.init('your-org/your-app');
// Identify the merchant
const shop = new URLSearchParams(window.location.search).get('shop');
if (shop) {
LogRocket.identify(shop, { shop });
}
}
LogRocket은 사용자 세션을 녹화하여 판매자가 버그를 보고했을 때 정확히 어떤 경험을 했는지 재생할 수 있게 합니다. 이는 재현하기 어려운 문제를 디버깅하는 데 매우 유용합니다. 개인정보 보호에 유의하십시오 -- LogRocket이 결제 정보와 고객 PII 같은 민감한 필드를 수정(redact)하도록 구성하십시오.
배포 체크리스트
| 항목 | 상태 | 비고 |
|---|---|---|
| HTTPS 적용 | 필수 | 위에 나열된 모든 플랫폼에서 적용 |
| 헬스 체크 엔드포인트 | 필수 | 호스팅 플랫폼 및 가동 시간 모니터링에 사용 |
| 환경 변수 보안 | 필수 | 코드에 포함하지 말고, 항상 플랫폼 시크릿 사용 |
| 데이터베이스 마이그레이션 자동화 | 필수 | 배포 전 CI에서 실행 |
| 오류 추적 구성 | 강력 권장 | Sentry, Bugsnag 또는 동등 수준 |
| 구조화된 로깅 | 강력 권장 | 검색 용이한 JSON 형식 |
| CI/CD 파이프라인 | 강력 권장 | 자동화된 테스트 및 배포 |
| 스테이징 환경 | 권장 | 프로덕션 전 테스트 |
| 카나리 배포 | 권장 | 위험 감소를 위한 점진적 롤아웃 |
| 가동 시간 모니터링 | 권장 | 외부 모니터링 (Uptime Robot, Better Stack) |
견고한 배포 파이프라인은 신뢰할 수 있는 Shopify 앱의 기반입니다. 첫날부터 제대로 설정하는 데 시간을 투자하면, 프로덕션 문제와 싸우는 시간은 훨씬 줄고 판매자가 좋아하는 기능을 구축하는 시간은 훨씬 늘어날 것입니다.