Shopify Functions
Shopify Functions는 Shopify 플랫폼에서 가장 강력한 확장 포인트입니다. Shopify 인프라 내부에서 커스텀 로직을 실행하여 핵심 커머스 운영을 커스터마이즈할 수 있습니다: 할인, 배송비, 결제 수단, 장바구니 변환, 주문 라우팅 등. Functions는 WebAssembly 모듈로 5밀리초 이내에 실행되어 성능에 영향을 주지 않으면서 모든 결제에서 실행할 수 있을 만큼 빠릅니다. 이 레슨에서는 아키텍처, 확장 대상, 개발 워크플로우, 그리고 더 이상 사용되지 않는 Shopify Scripts에서의 중요한 마이그레이션을 다룹니다.
Shopify Functions란?
Functions는 Shopify의 커머스 로직을 가로채서 수정하는 서버리스 마이크로 프로그램이라고 생각하면 됩니다. 자체 서버에서 실행되고 API를 통해 통신하는 앱과 달리, Functions는 Shopify 서버에서 샌드박스된 WebAssembly(Wasm) 모듈로 실행됩니다.
Functions가 중요한 이유
- 네트워크 지연 없음: 코드가 데이터 바로 옆의 Shopify 서버에서 실행됩니다
- 성능 보장: 엄격한 실행 제한(5ms, 11MB 메모리)으로 결제 속도를 보장합니다
- 안정성: 외부 서버 장애나 webhook 전달 실패가 없습니다
- 확장성: 트래픽 양에 관계없이 모든 결제에서 실행됩니다
- 보안: 샌드박스된 Wasm은 네트워크, 파일 시스템, 다른 스토어에 접근할 수 없습니다
Shopify Scripts(Ruby 기반 결제 커스터마이제이션, 이전에는 Plus 전용)는 더 이상 사용되지 않으며 2026년 6월에 제거됩니다. Scripts를 사용하는 모든 판매자는 Functions로 마이그레이션해야 합니다. 클라이언트를 위해 Scripts를 유지 관리하고 있다면 마이그레이션 계획을 지금 시작해야 합니다.
확장 대상
각 Function은 커머스 흐름의 특정 부분을 대상으로 합니다. 사용 가능한 대상은 다음과 같습니다:
| 확장 대상 | 커스터마이즈 대상 | 사용 사례 예시 |
|---|---|---|
purchase.product-discount.run | 상품 수준 할인 | "2개 구매 시 10% 할인" |
purchase.order-discount.run | 주문 수준 할인 | "$100 이상 주문 시 20% 할인" |
purchase.shipping-discount.run | 배송 할인 | "$50 이상 주문 시 무료 배송" |
purchase.payment-customization.run | 결제 수단 표시 여부 | 해외 주문에 착불 결제 숨기기 |
purchase.delivery-customization.run | 배송 옵션 커스터마이제이션 | 배송비 이름 변경, 재정렬 또는 숨기기 |
cart-transform.run | 장바구니 수정 | 상품 자동 번들링, 라인 아이템 병합 |
purchase.validation.run | 장바구니/결제 유효성 검사 | 수량 제한, 주소 규칙 적용 |
fulfillment-constraints.run | 주문 처리 라우팅 | 특정 위치로 주문 라우팅 |
order-routing.run | 주문 라우팅 로직 | 가장 가까운 창고로 주문 전송 |
개발 워크플로우
Function 생성
Shopify CLI를 사용하여 새 Function을 스캐폴딩합니다:
# 앱 디렉토리로 이동
cd ~/shopify-projects/masterclass-app
# Function 확장 생성
shopify app generate extension --template product_discounts --name "volume-discount"
extensions/ 폴더에 새 디렉토리가 생성됩니다:
extensions/volume-discount/
├── src/
│ └── run.js # Function 로직 (Rust의 경우 run.rs)
├── input.graphql # 입력 쿼리 정의
├── shopify.extension.toml # 확장 설정
└── package.json # 종속성 (JS만 해당)
입력 쿼리
모든 Function은 Function이 받을 데이터를 정의하는 GraphQL 입력 쿼리로 시작됩니다. 이 쿼리는 Function이 실행되기 전에 실행되며 사용 가능한 정보를 결정합니다.
# extensions/volume-discount/input.graphql
query Input {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
id
title
hasAnyTag(tags: ["volume-eligible"])
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "volume-discount", key: "config") {
value
}
}
}
입력 쿼리는 모든 결제에서 실행됩니다. 최소한으로 유지하십시오 -- Function이 실제로 필요한 데이터만 요청합니다. 불필요한 필드는 지연 시간과 메모리 사용량을 증가시킵니다.
JavaScript로 Functions 작성
JavaScript Functions는 @shopify/shopify_function 패키지를 사용합니다:
// extensions/volume-discount/src/run.js
import { DiscountApplicationStrategy } from "../generated/api";
/**
* 대량 구매 할인: 적격 상품 3개 이상 구매 시 할인율 적용.
*/
export function run(input) {
// 메타필드에서 설정 파싱
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? '{"quantity": 3, "percentage": 10}'
);
const targets = [];
for (const line of input.cart.lines) {
const variant = line.merchandise;
// 상품이 아닌 라인 건너뛰기 (예: 기프트 카드)
if (variant.__typename !== "ProductVariant") continue;
// 대량 구매 할인 적격 여부 확인
if (!variant.product.hasAnyTag) continue;
// 수량이 임계값을 충족하는지 확인
if (line.quantity >= config.quantity) {
targets.push({
productVariant: {
id: variant.id,
},
});
}
}
// 적격 대상이 없으면 빈 할인 반환
if (targets.length === 0) {
return { discounts: [], discountApplicationStrategy: DiscountApplicationStrategy.First };
}
return {
discounts: [
{
value: {
percentage: {
value: String(config.percentage),
},
},
targets,
message: `${config.percentage}% off when you buy ${config.quantity}+`,
},
],
discountApplicationStrategy: DiscountApplicationStrategy.First,
};
}
Rust로 Functions 작성
Rust Functions는 더 작고 빠른 Wasm 모듈로 컴파일됩니다. 최대 성능이 필요하거나 Rust에 익숙할 때 적합합니다:
// extensions/volume-discount/src/run.rs
use shopify_function::prelude::*;
use shopify_function::Result;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Config {
quantity: i64,
percentage: f64,
}
impl Default for Config {
fn default() -> Self {
Config {
quantity: 3,
percentage: 10.0,
}
}
}
generate_types!(
query_path = "./input.graphql",
schema_path = "./schema.graphql"
);
#[shopify_function_target(query_path = "./input.graphql", schema_path = "./schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let config: Config = input
.discount_node
.metafield
.as_ref()
.and_then(|m| serde_json::from_str(&m.value).ok())
.unwrap_or_default();
let mut targets = vec![];
for line in &input.cart.lines {
if let input::InputCartLinesMerchandise::ProductVariant(variant) = &line.merchandise {
if variant.product.has_any_tag && line.quantity >= config.quantity {
targets.push(output::Target {
product_variant: Some(output::ProductVariantTarget {
id: variant.id.clone(),
}),
});
}
}
}
if targets.is_empty() {
return Ok(output::FunctionRunResult {
discounts: vec![],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
});
}
Ok(output::FunctionRunResult {
discounts: vec![output::Discount {
value: output::Value {
percentage: Some(output::Percentage {
value: config.percentage.to_string(),
}),
fixed_amount: None,
},
targets,
message: Some(format!(
"{}% off when you buy {}+",
config.percentage, config.quantity
)),
conditions: None,
}],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
})
}
JavaScript를 선택하는 경우: 팀이 JS를 알고, 로직이 간단하며, 빠른 반복이 필요할 때. JavaScript Functions는 작성하고 디버그하기 더 쉽습니다.
Rust를 선택하는 경우: 가장 작은 Wasm 바이너리, 최대 실행 속도가 필요하거나 Rust에 익숙할 때. Rust Functions는 일반적으로 50-100KB로 컴파일되는 반면 JavaScript는 200-500KB입니다.
둘 다 WebAssembly로 컴파일되어 동일한 샌드박스에서 실행됩니다. 일반적인 할인/배송 로직의 경우 성능 차이는 보통 무시할 수 있습니다.
확장 설정
shopify.extension.toml 파일이 Function을 설정합니다:
name = "volume-discount"
type = "product_discounts"
api_version = "2026-04"
[build]
command = "npm exec -- shopify app function build"
path = "dist/function.wasm"
watch = ["src/**/*.js"]
[ui]
enable_create = true
[ui.paths]
create = "/app/volume-discount/:functionId/create"
details = "/app/volume-discount/:functionId/:id"
[ui] 섹션은 중요합니다 -- 판매자가 앱의 UI에서 할인을 구성하는 위치를 Shopify에 알려줍니다. 판매자가 Function을 사용하여 새 할인을 만들면 Shopify가 임베디드 앱 내에서 ui.paths.create에 지정된 URL을 로드합니다.
Functions 테스트
로컬 테스트
배포 전에 Functions를 로컬에서 테스트합니다:
# 샘플 입력으로 Function 실행
shopify app function run --path extensions/volume-discount
# 특정 입력 파일로 테스트
echo '{"cart":{"lines":[{"quantity":5,"merchandise":{"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/1","product":{"id":"gid://shopify/Product/1","title":"Widget","hasAnyTag":true}},"cost":{"amountPerQuantity":{"amount":"10.00","currencyCode":"USD"}}}]},"discountNode":{"metafield":{"value":"{\"quantity\":3,\"percentage\":15}"}}}' | shopify app function run --path extensions/volume-discount
단위 테스트 (JavaScript)
// extensions/volume-discount/src/run.test.js
import { describe, it, expect } from "vitest";
import { run } from "./run";
describe("volume-discount function", () => {
it("수량이 임계값을 충족할 때 할인을 적용합니다", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(1);
expect(result.discounts[0].value.percentage.value).toBe("10");
expect(result.discounts[0].targets).toHaveLength(1);
});
it("수량이 임계값 미만이면 할인을 반환하지 않습니다", () => {
const input = {
cart: {
lines: [
{
quantity: 1,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
it("적격하지 않은 상품을 건너뜁니다", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: false },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: { metafield: null },
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
});
Scripts에서 Functions로의 마이그레이션
Shopify Scripts(Ruby 기반, Plus 전용)는 2026년 6월 제거 마감으로 더 이상 사용되지 않습니다. 본인이나 클라이언트가 Scripts를 사용하고 있다면 마이그레이션은 필수입니다.
마이그레이션 타임라인
| 날짜 | 마일스톤 |
|---|---|
| 2024 | Functions GA, Scripts 지원 중단 발표 |
| 2025 | 새 Scripts 생성 불가 |
| 2026년 1월 | Admin에서 마이그레이션 도구 제공 |
| 2026년 6월 | Scripts 영구 제거 |
변경 사항
| Shopify Scripts | Shopify Functions |
|---|---|
| Ruby 언어 | JavaScript 또는 Rust (Wasm으로 컴파일) |
| Plus 전용 | 모든 플랜에서 사용 가능 |
| Script Editor 앱 | 앱에 내장 또는 독립 실행형 |
| 라인 아이템, 배송, 결제로 제한 | 더 많은 확장 대상 |
| 입력 쿼리 없음 | 명시적 GraphQL 입력 쿼리 |
| 변경 가능한 장바구니 객체 | 불변 입력, 선언적 출력 |
마이그레이션 전략
- 기존 Scripts 감사: 사용 중인 모든 Scripts, 유형(라인 아이템, 배송 또는 결제), 로직을 나열합니다.
- Function 대상에 매핑: 라인 아이템 스크립트는
product-discount또는order-discount에 매핑됩니다. 배송 스크립트는delivery-customization에 매핑됩니다. 결제 스크립트는payment-customization에 매핑됩니다. - 로직 재작성: Ruby 로직을 JavaScript 또는 Rust로 변환합니다. 패러다임이 다릅니다 -- Functions는 장바구니 객체를 변경하는 대신 불변 입력과 선언적 출력을 사용합니다.
- 설정 UI 구축: Scripts는 Script Editor를 사용했습니다. Functions는 앱에서 판매자용 UI가 필요합니다.
- 철저한 테스트: Function 테스트 도구를 사용하고 Scripts가 처리했던 동일한 시나리오에 대해 검증합니다.
- 배포 및 활성화: 스토어에 Function을 설치하고 Script를 비활성화합니다.
마이그레이션에는 시간이 걸립니다. 특히 조건부 규칙이 많은 복잡한 Scripts의 경우 더욱 그렇습니다. 지금 계획을 시작하십시오. 개발 스토어에서 Functions를 테스트하고 2026년 초에 프로덕션 스토어에 배포를 시작하십시오. 마지막 달까지 기다리면 판매자의 결제에 장애가 발생할 위험이 있습니다.
마이그레이션 예제: 라인 아이템 Script에서 상품 할인 Function으로
기존 Ruby Script (Script Editor):
Input.cart.line_items.each do |line_item|
if line_item.quantity >= 3
line_item.change_line_price(
line_item.line_price * 0.9,
message: "10% volume discount"
)
end
end
Output.cart = Input.cart
마이그레이션된 JavaScript Function:
export function run(input) {
const discounts = [];
for (const line of input.cart.lines) {
if (line.quantity >= 3) {
discounts.push({
value: { percentage: { value: "10" } },
targets: [{ productVariant: { id: line.merchandise.id } }],
message: "10% volume discount",
});
}
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}
Function은 장바구니를 직접 변경하는 대신 불변 입력/출력 패턴으로 동일한 결과를 달성합니다.
실행 제한
Functions는 결제 성능을 보호하기 위해 제한된 환경에서 실행됩니다:
| 제한 | 값 |
|---|---|
| 실행 시간 | 5ms |
| 메모리 | 11 MB |
| Wasm 바이너리 크기 | 256 KB |
| 입력 크기 | 64 KB |
| 출력 크기 | 64 KB |
| 네트워크 접근 | 없음 |
| 파일 시스템 접근 | 없음 |
Function 로직을 간단하고 집중적으로 유지하십시오. 깊은 중첩, 큰 데이터 구조, 복잡한 문자열 연산을 피하십시오. 5ms 제한에 도달하면 코드를 프로파일링하고 불필요한 반복을 찾으십시오. Rust Functions는 일반적으로 JavaScript보다 빠르고 메모리를 적게 사용합니다.
핵심 요약
- Shopify Functions는 WebAssembly 모듈로 Shopify 인프라 내에서 커스텀 로직을 실행합니다
- 빠르고(5ms 미만), 안정적이며(외부 서버 없음), 확장 가능합니다(모든 결제에서 실행)
- 확장 대상은 할인, 배송, 결제, 장바구니 변환, 유효성 검사, 주문 처리를 포함합니다
- JavaScript(더 쉬움) 또는 Rust(더 작고 빠름)로 Functions를 작성합니다
- 입력 쿼리가 Function이 받는 데이터를 정의합니다. 출력은 선언적입니다
- Scripts는 더 이상 사용되지 않습니다 -- 2026년 6월 전에 Functions로 마이그레이션하십시오
shopify app function run및 단위 테스트로 로컬에서 테스트합니다
이것으로 Shopify 기초 모듈을 마칩니다. 이제 플랫폼 아키텍처, API, 템플릿, 헤드리스 옵션, 확장 런타임에 대한 깊은 이해를 갖추게 되었습니다. 다음 모듈에서는 Claude Code가 Shopify 개발 워크플로우를 어떻게 변환하는지 살펴봅니다.