์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Shopify API์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์œผ๋กœ ๋งŒ๋“œ๋Š” ๋‚˜๋งŒ์˜ ๋งž์ถคํ˜• ์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ
๐Ÿ›๏ธ ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ยท ์‡ผํ•‘๋ชฐ๊ด€๋ จ ๊ฐœ๋ฐœ

๐Ÿš€ Shopify API์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์œผ๋กœ ๋งŒ๋“œ๋Š” ๋‚˜๋งŒ์˜ ๋งž์ถคํ˜• ์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ

Liquid ํ…œํ”Œ๋ฆฟ๋ถ€ํ„ฐ REST/GraphQL API, ์•ฑ ๊ฐœ๋ฐœ๊นŒ์ง€ โ€” ์‹ค์ „ ์ค‘์‹ฌ์œผ๋กœ ํŒŒํ—ค์ณ๋ด„ โœจ

์•ผ Shopify๋กœ ์‡ผํ•‘๋ชฐ ๋งŒ๋“ค๋ฉด ๊ทธ๋ƒฅ ํ…Œ๋งˆ ๊ณ ๋ฅด๊ณ  ๋ ์•„๋‹˜?? ใ…‹ใ…‹ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ๋ญ๊ฐ€ ํ•„์š”ํ•ด
ใ„ดใ„ด ๊ทธ๊ฑด ์™„์ „ ์ดˆ๋ณด ๋งˆ์ธ๋“œ์ž„ ใ…‹ใ…‹ใ…‹ Shopify๋Š” API ํŒŒ๋ฉด ํŒ”์ˆ˜๋ก ์ง„์งœ ๋ฌด๊ถ๋ฌด์ง„ํ•จ... ๋งž์ถคํ˜• ์‡ผํ•‘๋ชฐ ์›ํ•˜๋ฉด API๋ž‘ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•„์ˆ˜์ž„ ์ง„์งœ๋กœ

Shopify๋Š” ์ „ ์„ธ๊ณ„ 175๊ฐœ๊ตญ ์ด์ƒ์—์„œ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋Š” ๊ธ€๋กœ๋ฒŒ ์ด์ปค๋จธ์Šค ํ”Œ๋žซํผ์ด์•ผ. ๋‹จ์ˆœํžˆ "ํ…Œ๋งˆ ๊ณจ๋ผ์„œ ์ƒํ’ˆ ์˜ฌ๋ฆฌ๋ฉด ๋"์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์™„์ „ ์˜ค์‚ฐ์ž„ ใ…‹ใ…‹ใ…‹

์‹ค์ œ๋กœ Shopify๊ฐ€ ๊ฐ•๋ ฅํ•œ ์ด์œ ๋Š” API ์ƒํƒœ๊ณ„์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ž์œ ๋„ ๋•Œ๋ฌธ์ด๊ฑฐ๋“ . ๊ธฐ์—…ํ˜• ์‡ผํ•‘๋ชฐ๋ถ€ํ„ฐ ๊ฐœ์ธ ๋ธŒ๋žœ๋“œ ์Šคํ† ์–ด๊นŒ์ง€, ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์„ API๋กœ ์—ฐ๊ฒฐํ•˜๊ณ  Liquid ํ…œํ”Œ๋ฆฟ์œผ๋กœ UI๋ฅผ ์กฐ๊ฐํ•˜๋ฉด ์ง„์งœ ์„ธ์ƒ์— ํ•˜๋‚˜๋ฟ์ธ ์‡ผํ•‘๋ชฐ์ด ํƒ„์ƒํ•จ.

์ด ๊ธ€์—์„œ๋Š” Shopify ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ์ธ API ๊ตฌ์กฐ, Liquid ์ปค์Šคํ„ฐ๋งˆ์ด์ง•, ์•ฑ ๊ฐœ๋ฐœ, ์›นํ›… ํ™œ์šฉ๊นŒ์ง€ ์‹ค์ „ ์œ„์ฃผ๋กœ ์‹น ๋‹ค ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ ๐Ÿ’ช

Shopify ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„ ๊ตฌ์กฐ Shopify Core Platform REST API ์ƒํ’ˆยท์ฃผ๋ฌธยท๊ณ ๊ฐ ๊ด€๋ฆฌ GraphQL API ์œ ์—ฐํ•œ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ Storefront API ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค Webhooks ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ Liquid ํ…œํ”Œ๋ฆฟ ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• Shopify App ๊ธฐ๋Šฅ ํ™•์žฅ ๊ฐœ๋ฐœ โ†‘ ๋ชจ๋“  ์š”์†Œ๊ฐ€ Shopify Core์™€ ์—ฐ๊ฒฐ๋˜์–ด ๋™์ž‘ํ•จ

๐Ÿ”Œ Shopify API, ๋ญ๊ฐ€ ์žˆ๋Š”์ง€ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ž

Shopify API๋Š” ํฌ๊ฒŒ 3๊ฐ€์ง€ ํ•ต์‹ฌ API๋กœ ๋‚˜๋‰˜์–ด. ๊ฐ๊ฐ ์“ฐ์ž„์ƒˆ๊ฐ€ ๋‹ค๋ฅด๋‹ˆ๊นŒ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๊ณจ๋ผ ์จ์•ผ ํ•จ ใ…‹ใ…‹

๐Ÿ”ง
Admin REST API
๋ฐฑ์—”๋“œ ๊ด€๋ฆฌ์šฉ
์ƒํ’ˆยท์ฃผ๋ฌธยท๊ณ ๊ฐ CRUD
โšก
Admin GraphQL API
์œ ์—ฐํ•œ ์ฟผ๋ฆฌ
๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
๐Ÿ›’
Storefront API
ํ”„๋ก ํŠธ์—”๋“œ์šฉ
ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค

โ‘  Admin REST API โ€” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ด ๋˜๋Š” ๋…€์„

Admin REST API๋Š” Shopify ์Šคํ† ์–ด์˜ ๋ฐฑ์—”๋“œ ๋ฐ์ดํ„ฐ๋ฅผ CRUDํ•  ์ˆ˜ ์žˆ๋Š” API์•ผ. ์ƒํ’ˆ ๋“ฑ๋ก, ์ฃผ๋ฌธ ์กฐํšŒ, ๊ณ ๊ฐ ์ •๋ณด ์ˆ˜์ • ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ API๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ.

์—”๋“œํฌ์ธํŠธ ํ˜•์‹์€ ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ์–ด:

https://{store-name}.myshopify.com/admin/api/{version}/{resource}.json

์˜ˆ๋ฅผ ๋“ค์–ด ์ƒํ’ˆ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ:

GET https://my-store.myshopify.com/admin/api/2024-01/products.json


์ธ์ฆ์€ Access Token ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•จ. ํ—ค๋”์— X-Shopify-Access-Token์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋จ.

API ๋ฒ„์ „ ๊ด€๋ฆฌ๋„ ์ค‘์š”ํ•œ๋ฐ, Shopify๋Š” ๋ถ„๊ธฐ๋งˆ๋‹ค ์ƒˆ ๋ฒ„์ „์„ ๋ฆด๋ฆฌ์ฆˆํ•˜๊ณ  ์ตœ์†Œ 12๊ฐœ์›”์€ ์ง€์›ํ•ด์คŒ. 2024-01, 2024-04 ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฒ„์ „์ด ๋ถ™์–ด์žˆ์–ด.

โ‘ก Admin GraphQL API โ€” ์š”์ฆ˜ ๋Œ€์„ธ๋Š” ์ด๊ฑฐ์ž„

Shopify๋Š” REST API๋ณด๋‹ค GraphQL API๋ฅผ ์ ์  ๋” ๋ฐ€๊ณ  ์žˆ์Œ ใ…‹ใ…‹ ์‹ค์ œ๋กœ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค์€ GraphQL์—์„œ๋งŒ ์ง€์›๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„.

GraphQL์˜ ์žฅ์ ์€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ •ํ™•ํ•˜๊ฒŒ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ. REST๋Š” ์—”๋“œํฌ์ธํŠธ ํ•˜๋‚˜ ์น˜๋ฉด ์“ธ๋ฐ์—†๋Š” ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์™•์ฐฝ ์˜ค๋Š”๋ฐ, GraphQL์€ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ํ•„๋“œ๋งŒ ๋”ฑ ์ง€์ •ํ•ด์„œ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ.

POST https://my-store.myshopify.com/admin/api/2024-01/graphql.json

query {
  products(first: 10) {
    edges {
      node {
        id
        title
        priceRangeV2 {
          minVariantPrice {
            amount
            currencyCode
          }
        }
      }
    }
  }
}


์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ƒํ’ˆ 10๊ฐœ์˜ ID, ์ œ๋ชฉ, ์ตœ์ €๊ฐ€๋งŒ ๋”ฑ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Œ. REST๋กœ ํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ๊ฐ€ ์ˆ˜์‹ญ ๊ฐœ ๋”ธ๋ ค์˜ค๋Š”๋ฐ ใ…‹ใ…‹

โ‘ข Storefront API โ€” ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค์˜ ํ•ต์‹ฌ

Storefront API๋Š” ๊ณ ๊ฐ facing ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ์œ„ํ•œ API์•ผ. ์ฆ‰, ์‡ผํ•‘๋ชฐ ๋ฐฉ๋ฌธ์ž๋“ค์ด ์ƒํ’ˆ ๋ณด๊ณ , ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ณ , ๊ฒฐ์ œํ•˜๋Š” ๋ชจ๋“  ๊ณผ์ •์„ ์ปค์Šคํ…€ ํ”„๋ก ํŠธ์—”๋“œ๋กœ ๊ตฌํ˜„ํ•  ๋•Œ ์”€.

ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค(Headless Commerce)๊ฐ€ ๋ญ๋ƒ๋ฉด, Shopify์˜ ๋ฐฑ์—”๋“œ(์ƒํ’ˆ๊ด€๋ฆฌ, ๊ฒฐ์ œ, ์žฌ๊ณ )๋Š” ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ ํ”„๋ก ํŠธ์—”๋“œ๋Š” React, Next.js, Vue ๊ฐ™์€ ๊ฑธ๋กœ ์™„์ „ํžˆ ์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์ด์•ผ.

์ธ์ฆ ๋ฐฉ์‹๋„ ๋‹ฌ๋ผ. Storefront API๋Š” Storefront Access Token์„ ์“ฐ๋Š”๋ฐ, ์ด๊ฑด ํผ๋ธ”๋ฆญ ํ† ํฐ์ด๋ผ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ ์จ๋„ ๋จ (Admin API ํ† ํฐ์€ ์ ˆ๋Œ€ ํ”„๋ก ํŠธ์— ๋…ธ์ถœํ•˜๋ฉด ์•ˆ ๋จ!).

const client = createStorefrontApiClient({
  storeDomain: 'my-store.myshopify.com',
  apiVersion: '2024-01',
  publicAccessToken: 'your-storefront-access-token',
});
๐Ÿ’ก API ์„ ํƒ ๊ฐ€์ด๋“œ - ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ โ†’ Admin GraphQL API (REST๋ณด๋‹ค GraphQL ์šฐ์„ )
- ์ปค์Šคํ…€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ โ†’ Storefront API
- ๋ ˆ๊ฑฐ์‹œ ํ†ตํ•ฉ์ด๋‚˜ ๋‹จ์ˆœ CRUD โ†’ Admin REST API

๐Ÿ’ง Liquid ํ…œํ”Œ๋ฆฟ ์–ธ์–ด โ€” Shopify ํ…Œ๋งˆ์˜ ์‹ฌ์žฅ

Liquid๋Š” Shopify๊ฐ€ ๋งŒ๋“  ์˜คํ”ˆ์†Œ์Šค ํ…œํ”Œ๋ฆฟ ์–ธ์–ด์•ผ. Ruby ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๊ณ , Shopify ํ…Œ๋งˆ์˜ ๋ชจ๋“  HTML ํŒŒ์ผ์—์„œ ์‚ฌ์šฉ๋จ. ์ฒ˜์Œ ๋ณด๋ฉด ์ข€ ๋‚ฏ์„ค ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ต์ˆ™ํ•ด์ง€๋ฉด ์ง„์งœ ๊ฐ•๋ ฅํ•จ ใ…‹ใ…‹

Liquid๊ฐ€ ๋ญ”์ง€ ๋ชจ๋ฅด๊ฒ ๋Š”๋ฐ... HTML์ด๋ž‘ ๋‹ค๋ฅธ ๊ฑฐ์•ผ?
HTML ์•ˆ์— Liquid ๋ฌธ๋ฒ•์„ ์„ž์–ด์„œ ์“ฐ๋Š” ๊ฑฐ์ž„! ๋™์  ๋ฐ์ดํ„ฐ๋ฅผ HTML์— ๋„ฃ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ…œํ”Œ๋ฆฟ ์—”์ง„์ด๋ผ๊ณ  ๋ณด๋ฉด ๋จ ใ…‹ใ…‹

Liquid ๊ธฐ๋ณธ ๋ฌธ๋ฒ• 3๊ฐ€์ง€

1. ์ถœ๋ ฅ ํƒœ๊ทธ (Output Tags) โ€” ์ด์ค‘ ์ค‘๊ด„ํ˜ธ ์‚ฌ์šฉ

{{ product.title }}
{{ product.price | money }}
{{ shop.name }}


2. ๋กœ์ง ํƒœ๊ทธ (Logic Tags) โ€” ์ค‘๊ด„ํ˜ธ + ํผ์„ผํŠธ ์‚ฌ์šฉ

{% if product.available %}
  <button>์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ</button>
{% else %}
  <button disabled>ํ’ˆ์ ˆ</button>
{% endif %}

{% for product in collection.products %}
  <div>{{ product.title }}</div>
{% endfor %}


3. ํ•„ํ„ฐ (Filters) โ€” ํŒŒ์ดํ”„(|)๋กœ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜

{{ product.price | money }}           โ†’ โ‚ฉ15,000
{{ product.title | upcase }}          โ†’ PRODUCT NAME
{{ 'style.css' | asset_url | stylesheet_tag }}
{{ product.description | truncate: 100 }}

Shopify ํ…Œ๋งˆ ํŒŒ์ผ ๊ตฌ์กฐ

Shopify ํ…Œ๋งˆ๋Š” ์ •ํ•ด์ง„ ํด๋” ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ์•ผ ํ•จ:

theme/
โ”œโ”€โ”€ assets/          # CSS, JS, ์ด๋ฏธ์ง€ ํŒŒ์ผ
โ”œโ”€โ”€ config/          # ํ…Œ๋งˆ ์„ค์ • (settings_schema.json)
โ”œโ”€โ”€ layout/          # ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ (theme.liquid)
โ”œโ”€โ”€ locales/         # ๋‹ค๊ตญ์–ด ๋ฒˆ์—ญ ํŒŒ์ผ
โ”œโ”€โ”€ sections/        # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์„น์…˜ ์ปดํฌ๋„ŒํŠธ
โ”œโ”€โ”€ snippets/        # ์ž‘์€ ์žฌ์‚ฌ์šฉ ์ฝ”๋“œ ์กฐ๊ฐ
โ””โ”€โ”€ templates/       # ๊ฐ ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ


ํŠนํžˆ sections/ ํด๋”๊ฐ€ ํ•ต์‹ฌ์ž„. Shopify 2.0๋ถ€ํ„ฐ๋Š” ์„น์…˜ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋กœ ๋ฐ”๋€Œ์–ด์„œ, ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋์–ด.

Online Store 2.0 โ€” ํ…Œ๋งˆ ๊ฐœ๋ฐœ์˜ ํŒจ๋Ÿฌ๋‹ค์ž„ ๋ณ€ํ™”

Shopify Online Store 2.0(2021๋…„ ์ถœ์‹œ)์€ ํ…Œ๋งˆ ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ์™„์ „ํžˆ ๋ฐ”๊ฟ”๋†จ์Œ. ํ•ต์‹ฌ ๋ณ€ํ™”๋Š”:


1
JSON ํ…œํ”Œ๋ฆฟ โ€” ๊ธฐ์กด Liquid ํ…œํ”Œ๋ฆฟ ๋Œ€์‹  JSON์œผ๋กœ ์„น์…˜ ๊ตฌ์„ฑ์„ ์ •์˜. ๋” ์œ ์—ฐํ•œ ํŽ˜์ด์ง€ ๋นŒ๋” ๊ฐ€๋Šฅ
2
์„น์…˜ ์–ด๋””์„œ๋‚˜ โ€” ์ด์ „์—” ํ™ˆํŽ˜์ด์ง€์—๋งŒ ์„น์…˜ ์ถ”๊ฐ€ ๊ฐ€๋Šฅํ–ˆ๋Š”๋ฐ, ์ด์ œ ๋ชจ๋“  ํŽ˜์ด์ง€์— ์„น์…˜ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
3
์•ฑ ๋ธ”๋ก โ€” ์•ฑ์ด ํ…Œ๋งˆ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ์„น์…˜/๋ธ”๋ก ํ˜•ํƒœ๋กœ ํ†ตํ•ฉ ๊ฐ€๋Šฅ
4
๋ฉ”ํƒ€ํ•„๋“œ ์ง€์› โ€” ์ƒํ’ˆ, ์ปฌ๋ ‰์…˜, ๊ณ ๊ฐ ๋“ฑ์— ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ ํ•„๋“œ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

์‹ค์ „ ์„น์…˜ ์ฝ”๋“œ ์˜ˆ์‹œ

์ปค์Šคํ…€ ์ƒํ’ˆ ์นด๋“œ ์„น์…˜ ๋งŒ๋“ค๊ธฐ ์˜ˆ์‹œ:

{% comment %} sections/custom-product-card.liquid {% endcomment %}

<div class="product-card">
  {% if section.settings.show_image %}
    <img src="{{ product.featured_image | img_url: '400x400' }}"
         alt="{{ product.featured_image.alt }}">
  {% endif %}

  <h2>{{ product.title }}</h2>
  <p class="price">{{ product.price | money }}</p>

  {% if product.compare_at_price > product.price %}
    <span class="sale-badge">SALE</span>
    <s>{{ product.compare_at_price | money }}</s>
  {% endif %}

  <form action="/cart/add" method="post">
    <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
    <button type="submit">์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ</button>
  </form>
</div>

{% schema %}
{
  "name": "์ปค์Šคํ…€ ์ƒํ’ˆ ์นด๋“œ",
  "settings": [
    {
      "type": "checkbox",
      "id": "show_image",
      "label": "์ด๋ฏธ์ง€ ํ‘œ์‹œ",
      "default": true
    }
  ]
}
{% endschema %}
โš ๏ธ ์ฃผ์˜! Liquid๋Š” ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์ด๋ผ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด JavaScript์™€ ํ•จ๊ป˜ ์จ์•ผ ํ•จ. ํŠนํžˆ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์—…๋ฐ์ดํŠธ, ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ  ํ™•์ธ ๊ฐ™์€ ๊ฑด Ajax + Shopify Cart API ์กฐํ•ฉ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.

Liquid ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง ํ๋ฆ„ ๋ธŒ๋ผ์šฐ์ € ํŽ˜์ด์ง€ ์š”์ฒญ ๐ŸŒ Shopify ์„œ๋ฒ„ Liquid ํŒŒ์ผ ๋กœ๋“œ + DB ๋ฐ์ดํ„ฐ ์กฐํšŒ Liquid ์—”์ง„ {{ }} ํƒœ๊ทธ ์ฒ˜๋ฆฌ {% %} ๋กœ์ง ์‹คํ–‰ HTML ์ƒ์„ฑ ์™„์„ฑ๋œ HTML ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „์†ก ํ™”๋ฉด ๐Ÿ–ฅ๏ธ ํ•ต์‹ฌ ํฌ์ธํŠธ Liquid๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋จ โ†’ ํด๋ผ์ด์–ธํŠธ์—๋Š” ์™„์„ฑ๋œ HTML๋งŒ ์ „๋‹ฌ โ†’ ๋™์  ๊ธฐ๋Šฅ์€ JS + Shopify Ajax API ํ™œ์šฉ {{ product.title }} โ†’ "๋‚˜์ดํ‚ค ์—์–ด๋งฅ์Šค" Liquid ๋ณ€์ˆ˜ โ†’ ์‹ค์ œ ๊ฐ’์œผ๋กœ ์น˜ํ™˜ {% if product.available %} โ†’ ์žฌ๊ณ  ์žˆ์œผ๋ฉด ๋ฒ„ํŠผ ํ‘œ์‹œ ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๋™์  ๋ Œ๋”๋ง {{ price | money }} โ†’ โ‚ฉ15,000 ํ•„ํ„ฐ๋กœ ๋ฐ์ดํ„ฐ ํฌ๋งท ๋ณ€ํ™˜

๐Ÿ“ฑ Shopify ์•ฑ ๊ฐœ๋ฐœ โ€” ๊ธฐ๋Šฅ ํ™•์žฅ์˜ ๋ํŒ์™•

Shopify ์•ฑ์€ ์Šคํ† ์–ด์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ํฌ๊ฒŒ ํผ๋ธ”๋ฆญ ์•ฑ๊ณผ ์ปค์Šคํ…€ ์•ฑ์œผ๋กœ ๋‚˜๋‰จ.

๐ŸŒ ํผ๋ธ”๋ฆญ ์•ฑ Shopify App Store์— ์˜ฌ๋ ค์„œ ๋ชจ๋“  ์ƒ์ธ์ด ์„ค์น˜ํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ. OAuth ์ธ์ฆ ํ•„์ˆ˜. ์ˆ˜์ต ์ฐฝ์ถœ ๊ฐ€๋Šฅ.

OAuth 2.0 App Store ์ˆ˜์ตํ™”

๐Ÿ”’ ์ปค์Šคํ…€ ์•ฑ ํŠน์ • ์Šคํ† ์–ด ํ•˜๋‚˜๋งŒ์„ ์œ„ํ•œ ์•ฑ. ๋ณต์žกํ•œ OAuth ์—†์ด Admin API ํ† ํฐ์œผ๋กœ ๋ฐ”๋กœ ์—ฐ๊ฒฐ. ๋‚ด๋ถ€ ๋„๊ตฌ ๊ฐœ๋ฐœ์— ์ตœ์ .

๋‹จ์ผ ์Šคํ† ์–ด ๋น ๋ฅธ ๊ฐœ๋ฐœ ๋‚ด๋ถ€ ๋„๊ตฌ

Shopify CLI๋กœ ์•ฑ ๊ฐœ๋ฐœ ์‹œ์ž‘ํ•˜๊ธฐ

Shopify๋Š” Shopify CLI๋ผ๋Š” ๊ณต์‹ ๊ฐœ๋ฐœ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•จ. ์ด๊ฑธ๋กœ ์•ฑ ํ”„๋กœ์ ํŠธ๋ฅผ ๋น ๋ฅด๊ฒŒ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ์–ด:

# Shopify CLI ์„ค์น˜
npm install -g @shopify/cli @shopify/theme

# ์ƒˆ ์•ฑ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
shopify app init

# ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
shopify app dev


๊ธฐ๋ณธ ์•ฑ ์Šคํƒ์€ Node.js + Express + React ์กฐํ•ฉ์ด ๋งŽ์ด ์“ฐ์ด๊ณ , ์ตœ๊ทผ์—” Remix ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ Shopify๊ฐ€ ๊ณต์‹ ์ง€์›ํ•˜๋ฉด์„œ Remix ๊ธฐ๋ฐ˜ ์•ฑ ๊ฐœ๋ฐœ์ด ํŠธ๋ Œ๋“œ์ž„ ใ…‹ใ…‹

OAuth ์ธ์ฆ ํ”Œ๋กœ์šฐ โ€” ํผ๋ธ”๋ฆญ ์•ฑ ํ•„์ˆ˜ ์ง€์‹

1
์ƒ์ธ์ด App Store์—์„œ ์•ฑ ์„ค์น˜ ํด๋ฆญ โ†’ Shopify๊ฐ€ ์•ฑ์˜ redirect_uri๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
2
์•ฑ์ด Shopify ์ธ์ฆ URL๋กœ ์ƒ์ธ์„ ๋ณด๋ƒ„ (์š”์ฒญํ•  ๊ถŒํ•œ ์Šค์ฝ”ํ”„ ํฌํ•จ)
3
์ƒ์ธ์ด ๊ถŒํ•œ ์Šน์ธ โ†’ Shopify๊ฐ€ ์ž„์‹œ code๋ฅผ ์•ฑ์— ์ „๋‹ฌ
4
์•ฑ์ด code๋ฅผ access_token์œผ๋กœ ๊ตํ™˜ (์„œ๋ฒ„์‚ฌ์ด๋“œ์—์„œ!)
5
์ดํ›„ access_token์œผ๋กœ Admin API ํ˜ธ์ถœ ๊ฐ€๋Šฅ ๐ŸŽ‰
// access_token ๊ตํ™˜ ์˜ˆ์‹œ (Node.js)
const response = await fetch(
  `https://${shop}/admin/oauth/access_token`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: process.env.SHOPIFY_API_KEY,
      client_secret: process.env.SHOPIFY_API_SECRET,
      code: req.query.code,
    }),
  }
);
const { access_token } = await response.json();

Shopify App Bridge โ€” ์•ฑ UI ํ†ตํ•ฉ

App Bridge๋Š” Shopify ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์•ˆ์— ์•ฑ UI๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ†ตํ•ฉ์‹œ์ผœ์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์ด๊ฑธ ์“ฐ๋ฉด Shopify ๊ด€๋ฆฌ์ž์˜ ๋„ค๋น„๊ฒŒ์ด์…˜, ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€, ๋ชจ๋‹ฌ ๋“ฑ์„ ์•ฑ์—์„œ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ.

Polaris๋Š” Shopify์˜ ๊ณต์‹ ๋””์ž์ธ ์‹œ์Šคํ…œ + React ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์•ฑ UI๋ฅผ Shopify ์Šคํƒ€์ผ์— ๋งž๊ฒŒ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ:

import { Page, Card, Button, TextField } from '@shopify/polaris';

function MyApp() {
  return (
    <Page title="๋‚ด ์ปค์Šคํ…€ ์•ฑ">
      <Card>
        <TextField label="์ƒํ’ˆ๋ช…" value={value} onChange={setValue} />
        <Button primary>์ €์žฅํ•˜๊ธฐ</Button>
      </Card>
    </Page>
  );
}
๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ๋„ Shopify ์•ฑ ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์–ด! ์ปค์Šคํ…€ ์•ฑ ๊ฐœ๋ฐœ์ด๋‚˜ API ์—ฐ๋™ ์ž‘์—…์„ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์˜๋ขฐํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ž„ ใ…‹ใ…‹

๐Ÿ”” Webhooks โ€” ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์˜ ํ•ต์‹ฌ

Webhook์€ Shopify์—์„œ ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋‚ด ์„œ๋ฒ„๋กœ ์ž๋™์œผ๋กœ HTTP POST ์š”์ฒญ์„ ๋ณด๋‚ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ํด๋ง(์ฃผ๊ธฐ์ ์œผ๋กœ API ํ˜ธ์ถœํ•ด์„œ ๋ณ€๊ฒฝ ํ™•์ธ) ๋Œ€์‹  ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ›จ์”ฌ ํšจ์œจ์ ์ž„.

์ฃผ์š” Webhook ์ด๋ฒคํŠธ ๋ชฉ๋ก:


orders/create orders/paid orders/fulfilled orders/cancelled products/create products/update customers/create customers/update inventory_levels/update checkouts/create refunds/create app/uninstalled

Webhook ๋“ฑ๋ก ๋ฐฉ๋ฒ•

GraphQL๋กœ Webhook ๋“ฑ๋กํ•˜๊ธฐ:

mutation {
  webhookSubscriptionCreate(
    topic: ORDERS_CREATE
    webhookSubscription: {
      format: JSON
      callbackUrl: "https://my-app.com/webhooks/orders"
    }
  ) {
    webhookSubscription {
      id
      topic
      endpoint {
        __typename
        ... on WebhookHttpEndpoint {
          callbackUrl
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

Webhook ์ˆ˜์‹  ๋ฐ ๊ฒ€์ฆ โ€” ๋ณด์•ˆ์ด ํ•ต์‹ฌ!

Webhook์„ ๋ฐ›์„ ๋•Œ ๋ฐ˜๋“œ์‹œ ์„œ๋ช… ๊ฒ€์ฆ์„ ํ•ด์•ผ ํ•จ! ์•„๋ฌด๋‚˜ ๋‚ด ์—”๋“œํฌ์ธํŠธ์— ๊ฐ€์งœ ์š”์ฒญ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฑฐ๋“  ใ…‹ใ…‹

Shopify๋Š” ๊ฐ Webhook ์š”์ฒญ์— X-Shopify-Hmac-Sha256 ํ—ค๋”๋ฅผ ํฌํ•จ์‹œ์ผœ. ์ด๊ฑธ ๋‚ด API Secret์œผ๋กœ ๊ฒ€์ฆํ•ด์•ผ ํ•จ:

const crypto = require('crypto');

function verifyWebhook(req) {
  const hmac = req.headers['x-shopify-hmac-sha256'];
  const body = req.rawBody; // raw body ํ•„์š”!

  const hash = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET)
    .update(body, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(hash),
    Buffer.from(hmac)
  );
}

// Express ๋ผ์šฐํ„ฐ์—์„œ
app.post('/webhooks/orders', (req, res) => {
  if (!verifyWebhook(req)) {
    return res.status(401).send('Unauthorized');
  }

  const order = req.body;
  console.log('์ƒˆ ์ฃผ๋ฌธ ๋ฐœ์ƒ!', order.id);

  // ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๋กœ์ง...
  res.status(200).send('OK');
});


์ค‘์š”: Webhook ์ฒ˜๋ฆฌ๋Š” 5์ดˆ ์ด๋‚ด์— 200 ์‘๋‹ต์„ ๋ณด๋‚ด์•ผ ํ•จ. ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ ํ(Queue)์— ๋„ฃ๊ณ  ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.


๐Ÿ—ƒ๏ธ ๋ฉ”ํƒ€ํ•„๋“œ โ€” ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ ์ €์žฅ์˜ ๋ชจ๋“  ๊ฒƒ

Shopify์˜ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•  ๋•Œ๊ฐ€ ์žˆ์ž–์•„? ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒํ’ˆ์— "์›์‚ฐ์ง€", "์„ฑ๋ถ„ ๋ชฉ๋ก", "์‚ฌ์šฉ ๋ฐฉ๋ฒ•" ๊ฐ™์€ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ  ์‹ถ์„ ๋•Œ ๋ฉ”ํƒ€ํ•„๋“œ(Metafields)๋ฅผ ์‚ฌ์šฉํ•จ.

๋ฉ”ํƒ€ํ•„๋“œ๊ฐ€ ๋ญ”์ง€ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ด์ค˜ ใ…‹ใ…‹
์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด Shopify ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ์— "์ปค์Šคํ…€ ์ปฌ๋Ÿผ" ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑฐ์ž„! ์ƒํ’ˆ์— ๊ธฐ๋ณธ์œผ๋กœ ์ œ๋ชฉ, ๊ฐ€๊ฒฉ, ์žฌ๊ณ  ์žˆ๋Š”๋ฐ, ๊ฑฐ๊ธฐ์— "์›์‚ฐ์ง€"๋ผ๋Š” ํ•„๋“œ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์œผ๋ฉด ๋ฉ”ํƒ€ํ•„๋“œ ์“ฐ๋ฉด ๋จ ใ…‹ใ…‹

๋ฉ”ํƒ€ํ•„๋“œ ์ง€์› ๋ฆฌ์†Œ์Šค:


products variants collections customers orders pages blogs shop

์ง€์› ๋ฐ์ดํ„ฐ ํƒ€์ž…:


single_line_text_field multi_line_text_field number_integer number_decimal date boolean json file_reference product_reference color url rating

๋ฉ”ํƒ€ํ•„๋“œ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ

GraphQL๋กœ ์ƒํ’ˆ ๋ฉ”ํƒ€ํ•„๋“œ ์ƒ์„ฑ:

mutation {
  metafieldsSet(metafields: [
    {
      ownerId: "gid://shopify/Product/1234567890"
      namespace: "custom"
      key: "origin_country"
      value: "๋Œ€ํ•œ๋ฏผ๊ตญ"
      type: "single_line_text_field"
    }
  ]) {
    metafields {
      id
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}


Liquid์—์„œ ๋ฉ”ํƒ€ํ•„๋“œ ์ถœ๋ ฅ:

{% comment %} ์ƒํ’ˆ ํŽ˜์ด์ง€์—์„œ ๋ฉ”ํƒ€ํ•„๋“œ ์ถœ๋ ฅ {% endcomment %}
{% if product.metafields.custom.origin_country %}
  <p>์›์‚ฐ์ง€: {{ product.metafields.custom.origin_country.value }}</p>
{% endif %}

๋ฉ”ํƒ€์˜ค๋ธŒ์ ํŠธ โ€” ๋” ๋ณต์žกํ•œ ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ

๋ฉ”ํƒ€์˜ค๋ธŒ์ ํŠธ(Metaobjects)๋Š” 2022๋…„์— ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์œผ๋กœ, ์™„์ „ํžˆ ์ปค์Šคํ…€ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด "ํŒ€์› ์†Œ๊ฐœ", "FAQ ํ•ญ๋ชฉ", "๋ธŒ๋žœ๋“œ ์Šคํ† ๋ฆฌ" ๊ฐ™์€ ๊ตฌ์กฐํ™”๋œ ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค ๋•Œ ์”€.

๋ฉ”ํƒ€์˜ค๋ธŒ์ ํŠธ๋Š” ํƒ€์ž… ์ •์˜ โ†’ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ โ†’ ์ฐธ์กฐ ์ˆœ์„œ๋กœ ์‚ฌ์šฉํ•ด:

# 1. ๋ฉ”ํƒ€์˜ค๋ธŒ์ ํŠธ ํƒ€์ž… ์ •์˜ (GraphQL)
mutation {
  metaobjectDefinitionCreate(definition: {
    name: "ํŒ€์›"
    type: "team_member"
    fieldDefinitions: [
      { name: "์ด๋ฆ„", key: "name", type: "single_line_text_field" }
      { name: "์ง์ฑ…", key: "role", type: "single_line_text_field" }
      { name: "์‚ฌ์ง„", key: "photo", type: "file_reference" }
    ]
  }) {
    metaobjectDefinition { id }
  }
}

๐Ÿš€ ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค โ€” Next.js + Shopify ์กฐํ•ฉ

ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค๋Š” Shopify ๋ฐฑ์—”๋“œ + ์ปค์Šคํ…€ ํ”„๋ก ํŠธ์—”๋“œ ์กฐํ•ฉ์ด์•ผ. ์„ฑ๋Šฅ, ์œ ์—ฐ์„ฑ, ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜ ๋ชจ๋‘ ์ตœ๊ณ  ์ˆ˜์ค€์œผ๋กœ ๋Œ์–ด์˜ฌ๋ฆด ์ˆ˜ ์žˆ์Œ.

Shopify๋Š” ๊ณต์‹์ ์œผ๋กœ Hydrogen์ด๋ผ๋Š” React ๊ธฐ๋ฐ˜ ํ—ค๋“œ๋ฆฌ์Šค ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ œ๊ณตํ•˜๊ณ , ๋ฐฐํฌ๋Š” Oxygen(Shopify ์ž์ฒด ํ˜ธ์ŠคํŒ…)์—์„œ ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ”ฅ Hydrogen (๊ณต์‹) Shopify ๊ณต์‹ ํ—ค๋“œ๋ฆฌ์Šค ํ”„๋ ˆ์ž„์›Œํฌ. Remix ๊ธฐ๋ฐ˜. Shopify API์™€ ์ตœ์ ํ™”๋œ ํ†ตํ•ฉ. Oxygen์— ๋ฐฐํฌ.

Remix ๊ณต์‹ ์ง€์› Oxygen ๋ฐฐํฌ

โšก Next.js + Storefront API Next.js๋กœ ํ”„๋ก ํŠธ ๊ตฌ์„ฑ + Storefront API๋กœ Shopify ์—ฐ๊ฒฐ. ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ์Šคํƒ.

Next.js Vercel ๋ฐฐํฌ ์œ ์—ฐ์„ฑ ์ตœ๊ณ 

Next.js + Storefront API ์‹ค์ „ ์˜ˆ์‹œ

// lib/shopify.js
const domain = process.env.SHOPIFY_STORE_DOMAIN;
const storefrontAccessToken = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN;

async function shopifyFetch({ query, variables }) {
  const res = await fetch(
    `https://${domain}/api/2024-01/graphql.json`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token': storefrontAccessToken,
      },
      body: JSON.stringify({ query, variables }),
    }
  );
  return res.json();
}

// ์ƒํ’ˆ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
export async function getProducts() {
  const { data } = await shopifyFetch({
    query: `
      query {
        products(first: 20) {
          edges {
            node {
              id
              title
              handle
              priceRange {
                minVariantPrice {
                  amount
                  currencyCode
                }
              }
              images(first: 1) {
                edges {
                  node {
                    url
                    altText
                  }
                }
              }
            }
          }
        }
      }
    `,
  });
  return data.products.edges.map(({ node }) => node);
}

// pages/products/index.js
export async function getStaticProps() {
  const products = await getProducts();
  return { props: { products }, revalidate: 60 };
}

export default function ProductsPage({ products }) {
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.title}</h2>
          <p>{product.priceRange.minVariantPrice.amount}์›</p>
        </div>
      ))}
    </div>
  );
}

์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ตฌํ˜„ โ€” Storefront API Cart

ํ—ค๋“œ๋ฆฌ์Šค์—์„œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋Š” Storefront API์˜ Cart API๋กœ ๊ตฌํ˜„ํ•จ:

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒ์„ฑ
mutation cartCreate($input: CartInput!) {
  cartCreate(input: $input) {
    cart {
      id
      checkoutUrl
      lines(first: 10) {
        edges {
          node {
            id
            quantity
            merchandise {
              ... on ProductVariant {
                id
                title
                priceV2 { amount currencyCode }
              }
            }
          }
        }
      }
    }
  }
}

// ์•„์ดํ…œ ์ถ”๊ฐ€
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
  cartLinesAdd(cartId: $cartId, lines: $lines) {
    cart {
      id
      totalQuantity
    }
  }
}


์žฅ๋ฐ”๊ตฌ๋‹ˆ ID๋Š” localStorage์— ์ €์žฅํ•ด์„œ ์„ธ์…˜ ๊ฐ„ ์œ ์ง€ํ•˜๋ฉด ๋จ. ๊ฒฐ์ œ๋Š” checkoutUrl๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋ฉด Shopify ๊ธฐ๋ณธ ๊ฒฐ์ œ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ด.


์ „ํ†ต ๋ฐฉ์‹ vs ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค ๋น„๊ต ๐Ÿช ์ „ํ†ต Shopify ๋ฐฉ์‹ Shopify ๊ด€๋ฆฌ์ž Liquid ํ…œํ”Œ๋ฆฟ ํ…Œ๋งˆ ์—”์ง„ HTML ๋ Œ๋”๋ง ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ธŒ๋ผ์šฐ์ € ํ‘œ์‹œ ๐ŸŒ ๐Ÿš€ ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค ๋ฐฉ์‹ Shopify ๋ฐฑ์—”๋“œ ์ƒํ’ˆยท์ฃผ๋ฌธยท๊ฒฐ์ œ ๊ด€๋ฆฌ Storefront API GraphQL ๋ฐ์ดํ„ฐ ์ œ๊ณต Next.js / Hydrogen ์ปค์Šคํ…€ ํ”„๋ก ํŠธ์—”๋“œ ์™„์ „ ์ปค์Šคํ…€ UI ๋ฌดํ•œ ์ž์œ ๋„ ๐ŸŽจ โœ… ๋น ๋ฅธ ์‹œ์ž‘ ยท ์‰ฌ์šด ๊ด€๋ฆฌ โŒ ๋””์ž์ธ ์ž์œ ๋„ ์ œํ•œ ยท ์„ฑ๋Šฅ ํ•œ๊ณ„ โœ… ๋ฌดํ•œ ์ž์œ ๋„ ยท ์ตœ๊ณ  ์„ฑ๋Šฅ ยท SEO ์ตœ์ ํ™” โŒ ๊ฐœ๋ฐœ ๋ณต์žก๋„ ๋†’์Œ ยท ์ดˆ๊ธฐ ๋น„์šฉ ํผ

๐Ÿ’ณ ๊ฒฐ์ œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• โ€” Checkout Extensions

Shopify์˜ ๊ฒฐ์ œ ํŽ˜์ด์ง€๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” ๊ฑด ์˜ˆ์ „์—” Plus ํ”Œ๋žœ์—์„œ๋งŒ ๊ฐ€๋Šฅํ–ˆ๋Š”๋ฐ, ์ด์ œ๋Š” Checkout Extensions๋ฅผ ํ†ตํ•ด ์ผ๋ฐ˜ ํ”Œ๋žœ์—์„œ๋„ ์–ด๋А ์ •๋„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ๊ฐ€๋Šฅํ•ด์กŒ์–ด.

Checkout Extensions๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค:


โœ“
๊ฒฐ์ œ ํŽ˜์ด์ง€์— ์ปค์Šคํ…€ UI ๋ธ”๋ก ์ถ”๊ฐ€ (๋ฐฐ๋„ˆ, ํผ ํ•„๋“œ, ์ฒดํฌ๋ฐ•์Šค ๋“ฑ)
โœ“
๋ฐฐ์†ก ๋ฐฉ๋ฒ• ์ปค์Šคํ„ฐ๋งˆ์ด์ง• (ํŠน์ • ์กฐ๊ฑด์—์„œ ๋ฐฐ์†ก ์˜ต์…˜ ์ˆจ๊ธฐ๊ธฐ/ํ‘œ์‹œ)
โœ“
๊ฒฐ์ œ ๋ฐฉ๋ฒ• ํ•„ํ„ฐ๋ง (ํŠน์ • ๊ตญ๊ฐ€์—์„œ ํŠน์ • ๊ฒฐ์ œ ์ˆ˜๋‹จ ์ˆจ๊ธฐ๊ธฐ)
โœ“
์ฃผ๋ฌธ ํ™•์ธ ํŽ˜์ด์ง€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

Checkout Extension ๊ธฐ๋ณธ ๊ตฌ์กฐ:

import {
  extension,
  Banner,
  BlockStack,
  Text,
} from '@shopify/ui-extensions/checkout';

export default extension(
  'purchase.checkout.block.render',
  (root, { lines, cost }) => {
    const totalItems = lines.current.reduce(
      (sum, line) => sum + line.quantity, 0
    );

    root.appendChild(
      root.createComponent(Banner, { status: 'info' },
        root.createComponent(BlockStack, {},
          root.createComponent(Text, {},
            `์ด ${totalItems}๊ฐœ ์ƒํ’ˆ ยท ${cost.totalAmount.current.amount}์›`
          )
        )
      )
    );
  }
);
โš ๏ธ Checkout.liquid๋Š” deprecated! ์˜ˆ์ „์— Plus ํ”Œ๋žœ์—์„œ ์“ฐ๋˜ checkout.liquid๋Š” 2024๋…„ 8์›” ์ดํ›„ ์ง€์› ์ข…๋ฃŒ๋จ. ์ด์ œ๋Š” Checkout Extensions๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•ด์•ผ ํ•ด.

โšก Shopify ์‡ผํ•‘๋ชฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

์•„๋ฌด๋ฆฌ ๋ฉ‹์ง„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ํ•ด๋„ ๋А๋ฆฌ๋ฉด ๋‹ค ์†Œ์šฉ์—†์Œ ใ…‹ใ…‹ Core Web Vitals ์ ์ˆ˜๊ฐ€ SEO์—๋„ ์ง๊ฒฐ๋˜๋‹ˆ๊นŒ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜์•ผ.

์ด๋ฏธ์ง€ ์ตœ์ ํ™”

Shopify๋Š” ์ž์ฒด CDN์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ œ๊ณตํ•˜๋Š”๋ฐ, Liquid์—์„œ ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•ด์„œ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ํฐ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด:

{% comment %} ๋‚˜์œ ์˜ˆ - ์›๋ณธ ์ด๋ฏธ์ง€ ๊ทธ๋Œ€๋กœ ๋กœ๋“œ {% endcomment %}
<img src="{{ product.featured_image | img_url }}">

{% comment %} ์ข‹์€ ์˜ˆ - ํฌ๊ธฐ ์ง€์ • + WebP ํฌ๋งท {% endcomment %}
<img
  src="{{ product.featured_image | image_url: width: 800 }}"
  srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
          {{ product.featured_image | image_url: width: 800 }} 800w,
          {{ product.featured_image | image_url: width: 1200 }} 1200w"
  sizes="(max-width: 768px) 100vw, 50vw"
  loading="lazy"
  alt="{{ product.featured_image.alt }}"
>


Shopify CDN์€ ์ž๋™์œผ๋กœ WebP ๋ณ€ํ™˜์„ ์ง€์›ํ•˜๊ณ , image_url ํ•„ํ„ฐ๋กœ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ๋ฆฌ์‚ฌ์ด์ง•ํ•ด์„œ ์คŒ.

JavaScript ์ตœ์ ํ™”

Shopify ํ…Œ๋งˆ์—์„œ JS ์„ฑ๋Šฅ์„ ๋†’์ด๋Š” ํ•ต์‹ฌ ์ „๋žต๋“ค:


1
defer/async ์†์„ฑ โ€” ์Šคํฌ๋ฆฝํŠธ ๋กœ๋”ฉ์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด์„œ ๋ Œ๋”๋ง ๋ธ”๋กœํ‚น ๋ฐฉ์ง€
2
Web Components โ€” Shopify Dawn ํ…Œ๋งˆ์ฒ˜๋Ÿผ Custom Elements๋กœ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ. ํ”„๋ ˆ์ž„์›Œํฌ ์—†์ด ๊ฐ€๋ณ๊ฒŒ
3
Intersection Observer โ€” ๋ทฐํฌํŠธ์— ๋“ค์–ด์˜ฌ ๋•Œ๋งŒ JS ์‹คํ–‰ (๋ ˆ์ด์ง€ ๋กœ๋”ฉ)
4
Predictive Prefetch โ€” ๋งˆ์šฐ์Šค ํ˜ธ๋ฒ„ ์‹œ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฏธ๋ฆฌ ๋กœ๋“œ

์„น์…˜ ๋ Œ๋”๋ง API โ€” ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ์˜ ํ˜๋ช…

Shopify์˜ Section Rendering API๋Š” ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜์ง€ ์•Š๊ณ  ํŠน์ • ์„น์…˜๋งŒ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ. ํ•„ํ„ฐ, ์ •๋ ฌ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์—…๋ฐ์ดํŠธ ๋“ฑ์— ํ™œ์šฉ:

// ์ปฌ๋ ‰์…˜ ํ•„ํ„ฐ ์ ์šฉ ์‹œ ์„น์…˜๋งŒ ์—…๋ฐ์ดํŠธ
async function updateProductGrid(filterParams) {
  const url = `${window.location.pathname}?${filterParams}&sections=product-grid`;

  const response = await fetch(url);
  const data = await response.json();

  // ์„น์…˜ HTML๋งŒ ๊ต์ฒด
  document.getElementById('product-grid').innerHTML =
    data['product-grid'];
}


์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ „์ฒด ํŽ˜์ด์ง€ ๋ฆฌ๋กœ๋“œ ์—†์ด ์ƒํ’ˆ ๋ชฉ๋ก๋งŒ ์—…๋ฐ์ดํŠธ๋จ. UX๊ฐ€ ํ›จ์”ฌ ๋ถ€๋“œ๋Ÿฌ์›Œ์ง€๊ณ  ์„ฑ๋Šฅ๋„ ์ข‹์•„์ง ใ…‹ใ…‹


๐ŸŽฏ ์‹ค์ „ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์‚ฌ๋ก€ ๋ชจ์Œ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ดค์œผ๋‹ˆ๊นŒ ์‹ค์ œ๋กœ ๋งŽ์ด ์“ฐ์ด๋Š” ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์‚ฌ๋ก€๋“ค์„ ์‚ดํŽด๋ณด์ž ใ…‹ใ…‹

โ‘  ์ปค์Šคํ…€ ํ•„ํ„ฐ & ์ •๋ ฌ ๊ตฌํ˜„

Shopify์˜ ๊ธฐ๋ณธ ํ•„ํ„ฐ ๊ธฐ๋Šฅ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ฑฐ๋‚˜, Storefront API๋กœ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ํ•„ํ„ฐ UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

query getFilteredProducts(
  $handle: String!
  $filters: [ProductFilter!]
  $sortKey: ProductCollectionSortKeys
  $reverse: Boolean
) {
  collection(handle: $handle) {
    products(
      first: 20
      filters: $filters
      sortKey: $sortKey
      reverse: $reverse
    ) {
      filters {
        id
        label
        type
        values {
          id
          label
          count
          input
        }
      }
      edges {
        node {
          id
          title
          priceRange {
            minVariantPrice { amount }
          }
        }
      }
    }
  }
}

โ‘ก ๋ฒˆ๋“ค ์ƒํ’ˆ & ์—…์…€ ๊ธฐ๋Šฅ

์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ๋ฌถ์–ด์„œ ํ• ์ธ ํŒ๋งคํ•˜๋Š” ๋ฒˆ๋“ค ๊ธฐ๋Šฅ์€ Shopify ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์— ์—†์–ด์„œ ์ปค์Šคํ…€์œผ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ. ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€:


๋ฐฉ๋ฒ• 1: ๋ฉ”ํƒ€ํ•„๋“œ ํ™œ์šฉ โ€” ๋ฒˆ๋“ค ์ƒํ’ˆ์— ๊ด€๋ จ ์ƒํ’ˆ ID๋ฅผ ๋ฉ”ํƒ€ํ•„๋“œ๋กœ ์ €์žฅํ•˜๊ณ , ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€

// ๋ฒˆ๋“ค ์ƒํ’ˆ๋“ค์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€
async function addBundleToCart(bundleItems) {
  const response = await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      items: bundleItems.map(item => ({
        id: item.variantId,
        quantity: item.quantity,
        properties: { '_bundle_id': bundleItems[0].variantId }
      }))
    })
  });
  return response.json();
}


๋ฐฉ๋ฒ• 2: Shopify Bundles ์•ฑ โ€” Shopify ๊ณต์‹ ๋ฒˆ๋“ค ์•ฑ ์‚ฌ์šฉ (2023๋…„ ์ถœ์‹œ, ๋ฌด๋ฃŒ)

โ‘ข ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ  ์•Œ๋ฆผ ๊ธฐ๋Šฅ

ํ’ˆ์ ˆ ์ƒํ’ˆ์— "์žฌ์ž…๊ณ  ์•Œ๋ฆผ" ๊ธฐ๋Šฅ ๊ตฌํ˜„:

// ์žฌ์ž…๊ณ  ์•Œ๋ฆผ ์‹ ์ฒญ ํผ
<form id="back-in-stock-form">
  <input type="email" name="email" placeholder="์ด๋ฉ”์ผ ์ž…๋ ฅ" required>
  <input type="hidden" name="variant_id" value="{{ product.selected_variant.id }}">
  <button type="submit">์žฌ์ž…๊ณ  ์•Œ๋ฆผ ์‹ ์ฒญ</button>
</form>

<script>
document.getElementById('back-in-stock-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const formData = new FormData(e.target);

  // ๋‚ด ์•ฑ ์„œ๋ฒ„์— ์‹ ์ฒญ ์ •๋ณด ์ €์žฅ
  await fetch('/apps/back-in-stock/subscribe', {
    method: 'POST',
    body: JSON.stringify({
      email: formData.get('email'),
      variantId: formData.get('variant_id')
    })
  });

  alert('์žฌ์ž…๊ณ  ์‹œ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋“œ๋ฆด๊ฒŒ์š”! ๐Ÿ“ง');
});
</script>


Webhook inventory_levels/update๋ฅผ ๊ตฌ๋…ํ•ด์„œ ์žฌ๊ณ ๊ฐ€ ์ƒ๊ธฐ๋ฉด ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ๋กœ ์ž๋™ ๋ฐœ์†กํ•˜๋ฉด ๋จ.

โ‘ฃ ๋‹ค๊ตญ์–ด & ๋‹คํ†ตํ™” ์ง€์›

Shopify Markets๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๊ธ€๋กœ๋ฒŒ ํŒ๋งค๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ง. Liquid์—์„œ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ:

{% comment %} locales/ko.default.json {% endcomment %}
{
  "products": {
    "add_to_cart": "์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ",
    "sold_out": "ํ’ˆ์ ˆ",
    "sale": "์„ธ์ผ"
  }
}

{% comment %} locales/en.default.json {% endcomment %}
{
  "products": {
    "add_to_cart": "Add to Cart",
    "sold_out": "Sold Out",
    "sale": "Sale"
  }
}

{% comment %} ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉ {% endcomment %}
<button>{{ 'products.add_to_cart' | t }}</button>


ํ†ตํ™” ๋ณ€ํ™˜์€ Shopify๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š”๋ฐ, {{ product.price | money_with_currency }}๋กœ ํ˜„์žฌ ํ†ตํ™”์— ๋งž๊ฒŒ ํ‘œ์‹œ๋จ.


๐Ÿ› ๏ธ Shopify ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ… & ํ•„์ˆ˜ ๋„๊ตฌ

Shopify ๊ฐœ๋ฐœ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ๋ญ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•จ?? ใ…‹ใ…‹
Shopify CLI ์„ค์น˜ํ•˜๊ณ  ํŒŒํŠธ๋„ˆ ๊ณ„์ • ๋งŒ๋“ค๋ฉด ๊ฐœ๋ฐœ ์Šคํ† ์–ด ๋ฌด๋ฃŒ๋กœ ์“ธ ์ˆ˜ ์žˆ์Œ! ๊ฑฐ๊ธฐ์„œ ๋งˆ์Œ๊ป ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋จ ใ…‹ใ…‹

ํ•„์ˆ˜ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ… ์ˆœ์„œ:


1
Shopify Partners ๊ณ„์ • ์ƒ์„ฑ โ€” partners.shopify.com์—์„œ ๋ฌด๋ฃŒ ๊ฐ€์ž…. ๊ฐœ๋ฐœ ์Šคํ† ์–ด ๋ฌด์ œํ•œ ์ƒ์„ฑ ๊ฐ€๋Šฅ
2
Node.js ์„ค์น˜ โ€” LTS ๋ฒ„์ „ ๊ถŒ์žฅ (ํ˜„์žฌ v20.x)
3
Shopify CLI ์„ค์น˜ โ€” npm install -g @shopify/cli
4
๊ฐœ๋ฐœ ์Šคํ† ์–ด ์ƒ์„ฑ โ€” Partners ๋Œ€์‹œ๋ณด๋“œ์—์„œ Development Store ์ƒ์„ฑ
5
ํ…Œ๋งˆ ๊ฐœ๋ฐœ ์‹œ์ž‘ โ€” shopify theme dev --store=my-store.myshopify.com

์ถ”์ฒœ ๊ฐœ๋ฐœ ๋„๊ตฌ & ์ต์Šคํ…์…˜

๐Ÿ”ง VS Code ์ต์Šคํ…์…˜


Shopify Liquid Liquid Languages Support Shopify Theme Check

Theme Check๋Š” Liquid ์ฝ”๋“œ์˜ ์˜ค๋ฅ˜์™€ ์„ฑ๋Šฅ ์ด์Šˆ๋ฅผ ์ž๋™์œผ๋กœ ์žก์•„์คŒ. ํ•„์ˆ˜์ž„ ใ…‹ใ…‹

๐Ÿงช ํ…Œ์ŠคํŠธ & ๋””๋ฒ„๊น…


Shopify Theme Inspector Chrome DevTools Postman / Insomnia

Theme Inspector๋Š” Liquid ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์‹œ๊ฐํ™”ํ•ด์„œ ๋ณด์—ฌ์คŒ. ๋ณ‘๋ชฉ ์ฐพ๊ธฐ ์ตœ๊ณ 

Shopify Theme Check โ€” ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ

Theme Check๋Š” Liquid ์ฝ”๋“œ์˜ ๋ฌธ์ œ๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์ฃผ๋Š” ๋ฆฐํ„ฐ์•ผ:

# Theme Check ์‹คํ–‰
shopify theme check

# ํŠน์ • ๊ทœ์น™๋งŒ ์ฒดํฌ
shopify theme check --only LiquidHTMLSyntaxError

# ์ž๋™ ์ˆ˜์ • ๊ฐ€๋Šฅํ•œ ์ด์Šˆ ์ˆ˜์ •
shopify theme check --auto-correct


์ฃผ์š” ์ฒดํฌ ํ•ญ๋ชฉ๋“ค:


Liquid ๋ฌธ๋ฒ• ์˜ค๋ฅ˜ ๋ฏธ์‚ฌ์šฉ ๋ณ€์ˆ˜ ์„ฑ๋Šฅ ์ด์Šˆ ์ ‘๊ทผ์„ฑ ๋ฌธ์ œ ๋ฒˆ์—ญ ํ‚ค ๋ˆ„๋ฝ deprecated ํ•„ํ„ฐ
๐Ÿ’ก ๊ฐœ๋ฐœ ํŒ! shopify theme dev๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋กœ์ปฌ์—์„œ ์ˆ˜์ •ํ•œ ๋‚ด์šฉ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐœ๋ฐœ ์Šคํ† ์–ด์— ๋ฐ˜์˜๋จ. Hot reload ์ง€์›์ด๋ผ ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ์—„์ฒญ ๋นจ๋ผ์ง ใ…‹ใ…‹

๐Ÿ” ๋ณด์•ˆ & ๊ฐœ๋ฐœ ๋ชจ๋ฒ” ์‚ฌ๋ก€

Shopify ๊ฐœ๋ฐœ์—์„œ ๋ณด์•ˆ์€ ์ ˆ๋Œ€ ํƒ€ํ˜‘ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ์˜์—ญ์ด์•ผ. ํŠนํžˆ API ํ‚ค ๊ด€๋ฆฌ์™€ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์€ ๊ธฐ๋ณธ ์ค‘์˜ ๊ธฐ๋ณธ์ž„.

๐Ÿšจ ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ๋“ค:


โŒ
Admin API ํ† ํฐ์„ ํ”„๋ก ํŠธ์—”๋“œ์— ๋…ธ์ถœ โ€” ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ฝ”๋“œ์— ์ ˆ๋Œ€ ๋„ฃ์œผ๋ฉด ์•ˆ ๋จ. ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„œ๋ฒ„์—์„œ๋งŒ ์‚ฌ์šฉ
โŒ
Webhook ์„œ๋ช… ๊ฒ€์ฆ ์ƒ๋žต โ€” ๊ฒ€์ฆ ์—†์ด Webhook ์ฒ˜๋ฆฌํ•˜๋ฉด ๊ฐ€์งœ ์š”์ฒญ์— ๋‹นํ•  ์ˆ˜ ์žˆ์Œ
โŒ
API ํ‚ค๋ฅผ Git์— ์ปค๋ฐ‹ โ€” .env ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ .gitignore์— ์ถ”๊ฐ€
โŒ
๊ณผ๋„ํ•œ API ๊ถŒํ•œ ์š”์ฒญ โ€” ์•ฑ์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ์Šค์ฝ”ํ”„๋งŒ ์š”์ฒญ (์ตœ์†Œ ๊ถŒํ•œ ์›์น™)

โœ… ๋ฐ˜๋“œ์‹œ ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ๋“ค:


โœ“
API Rate Limit ์ฒ˜๋ฆฌ โ€” Shopify REST API๋Š” ๋ถ„๋‹น 40 ์š”์ฒญ, GraphQL์€ ๋น„์šฉ ๊ธฐ๋ฐ˜ ์ œํ•œ. 429 ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์ˆ˜
โœ“
CSRF ๋ณดํ˜ธ โ€” ์•ฑ์—์„œ ํผ ์ œ์ถœ ์‹œ CSRF ํ† ํฐ ๊ฒ€์ฆ
โœ“
์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ & ์ด์Šค์ผ€์ดํ”„ โ€” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ Liquid์— ์ถœ๋ ฅํ•˜๋ฉด XSS ์œ„ํ—˜
โœ“
HTTPS ๊ฐ•์ œ โ€” ๋ชจ๋“  API ํ†ต์‹ ์€ HTTPS๋กœ๋งŒ

API Rate Limit ์ฒ˜๋ฆฌ ์˜ˆ์‹œ

// GraphQL API์˜ ๋น„์šฉ ๊ธฐ๋ฐ˜ Rate Limit ์ฒ˜๋ฆฌ
async function shopifyGraphQL(query, variables) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify({ query, variables }),
  });

  const data = await response.json();

  // Rate Limit ์ •๋ณด ํ™•์ธ
  const cost = data.extensions?.cost;
  if (cost) {
    const { throttleStatus } = cost;
    const { currentlyAvailable, restoreRate } = throttleStatus;

    // ๋‚จ์€ ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•˜๋ฉด ๋Œ€๊ธฐ
    if (currentlyAvailable < 100) {
      const waitTime = (100 - currentlyAvailable) / restoreRate * 1000;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }

  return data;
}

Shopify ๊ฐœ๋ฐœ์ž ์„ฑ์žฅ ๋กœ๋“œ๋งต ๐ŸŒฑ ์ž…๋ฌธ Liquid ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ํ…Œ๋งˆ ๊ตฌ์กฐ ์ดํ•ด Shopify CLI ์‚ฌ์šฉ ๐ŸŒฟ ์ดˆ๊ธ‰ REST API ํ™œ์šฉ ์„น์…˜ ๊ฐœ๋ฐœ ๋ฉ”ํƒ€ํ•„๋“œ ํ™œ์šฉ ๐ŸŒณ ์ค‘๊ธ‰ GraphQL API ์ปค์Šคํ…€ ์•ฑ ๊ฐœ๋ฐœ Webhook ์ฒ˜๋ฆฌ ๐Ÿ† ๊ณ ๊ธ‰ ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค ํผ๋ธ”๋ฆญ ์•ฑ ๊ฐœ๋ฐœ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๐Ÿ“š ํ•„์ˆ˜ ํ•™์Šต ์ž๋ฃŒ ๊ณต์‹ ๋ฌธ์„œ shopify.dev Shopify Partners ๋ธ”๋กœ๊ทธ ์ปค๋ฎค๋‹ˆํ‹ฐ Shopify Community Forum r/shopify (Reddit) ์‹ค์Šต ํ™˜๊ฒฝ Shopify Partners ๊ฐœ๋ฐœ ์Šคํ† ์–ด GitHub Shopify ์˜ˆ์ œ ์žฌ๋Šฅ ๊ฑฐ๋ž˜ ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€ ๋ฉ˜ํ† ๋ง ํ™œ์šฉ ๐ŸŽฏ ํ•ต์‹ฌ ์กฐ์–ธ Dawn ํ…Œ๋งˆ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ฒƒ์ด Shopify ๊ฐœ๋ฐœ ์‹ค๋ ฅ์„ ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์ž„ Shopify๊ฐ€ ๊ณต์‹ ์ œ๊ณตํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค ํ…Œ๋งˆ๋ผ ๋ชจ๋“  ๋ชจ๋ฒ” ์‚ฌ๋ก€๊ฐ€ ๋‹ด๊ฒจ์žˆ์Œ โœจ

๐Ÿ“‹ ํ•ต์‹ฌ ๋‚ด์šฉ ์ด์ •๋ฆฌ

์ž ์ด์ œ Shopify API์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์— ๋Œ€ํ•ด ๊ฝค ๊นŠ์ด ํŒŒ๋ดค๋Š”๋ฐ, ํ•ต์‹ฌ๋งŒ ๋‹ค์‹œ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ ใ…‹ใ…‹

๐Ÿ”Œ API ์„ ํƒ ๊ธฐ์ค€


โ€ข Admin GraphQL API โ€” ๋ฐฑ์—”๋“œ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์˜ ํ‘œ์ค€. REST๋ณด๋‹ค GraphQL ์šฐ์„  ์‚ฌ์šฉ
โ€ข Storefront API โ€” ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ ์‹œ ํ•„์ˆ˜
โ€ข Admin REST API โ€” ๋ ˆ๊ฑฐ์‹œ ํ†ตํ•ฉ์ด๋‚˜ ๋‹จ์ˆœ CRUD์—๋งŒ ์‚ฌ์šฉ

๐Ÿ’ง Liquid ๊ฐœ๋ฐœ ํ•ต์‹ฌ


โ€ข Online Store 2.0 ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ๋ฐœ (JSON ํ…œํ”Œ๋ฆฟ + ์„น์…˜ ์•„ํ‚คํ…์ฒ˜)
โ€ข Theme Check๋กœ ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ
โ€ข ๋™์  ๊ธฐ๋Šฅ์€ Section Rendering API + JavaScript ์กฐํ•ฉ์œผ๋กœ ๊ตฌํ˜„
โ€ข Dawn ํ…Œ๋งˆ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๋ ˆํผ๋Ÿฐ์Šค๋กœ ํ™œ์šฉ

๐Ÿ“ฑ ์•ฑ ๊ฐœ๋ฐœ ํ•ต์‹ฌ


โ€ข ํผ๋ธ”๋ฆญ ์•ฑ: OAuth 2.0 ์ธ์ฆ + Polaris UI + App Bridge
โ€ข ์ปค์Šคํ…€ ์•ฑ: Admin API ํ† ํฐ์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœ
โ€ข Webhook์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (์„œ๋ช… ๊ฒ€์ฆ ํ•„์ˆ˜!)
โ€ข Shopify CLI + Remix ์Šคํƒ์ด ํ˜„์žฌ ํŠธ๋ Œ๋“œ

๐Ÿš€ ํ—ค๋“œ๋ฆฌ์Šค ์ปค๋จธ์Šค ํ•ต์‹ฌ


โ€ข Shopify Hydrogen (๊ณต์‹) ๋˜๋Š” Next.js + Storefront API
โ€ข ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋Š” Cart API๋กœ ๊ตฌํ˜„, checkoutUrl๋กœ ๊ฒฐ์ œ ์—ฐ๊ฒฐ
โ€ข ์ด๋ฏธ์ง€ ์ตœ์ ํ™” + ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์œผ๋กœ ์„ฑ๋Šฅ ํ™•๋ณด
โ€ข ISR(Incremental Static Regeneration)์œผ๋กœ SEO + ์„ฑ๋Šฅ ๋™์‹œ ๋‹ฌ์„ฑ

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ํŒ! Shopify ๊ฐœ๋ฐœ์€ ํ˜ผ์ž ๋‹ค ํ•˜๋ ค๊ณ  ํ•˜๋ฉด ํž˜๋“ค ์ˆ˜ ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ Shopify ์ „๋ฌธ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ์•„ ํ˜‘์—…ํ•˜๊ฑฐ๋‚˜, ํŠน์ • ๊ธฐ๋Šฅ๋งŒ ์˜๋ขฐํ•˜๋Š” ๊ฒƒ๋„ ๋งค์šฐ ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ž„ ใ…‹ใ…‹ ํŠนํžˆ ๋ณต์žกํ•œ API ์—ฐ๋™์ด๋‚˜ ์ปค์Šคํ…€ ์•ฑ ๊ฐœ๋ฐœ์€ ๊ฒฝํ—˜ ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์™€ ํ•จ๊ป˜ํ•˜๋ฉด ์‹œ๊ฐ„์„ ์—„์ฒญ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด.
์™€ ์ง„์งœ Shopify๊ฐ€ ์ด๋ ‡๊ฒŒ ๊นŠ์€ ์ค„ ๋ชฐ๋ž์Œ ใ…‹ใ…‹ใ…‹ ํ…Œ๋งˆ ๊ณ ๋ฅด๊ณ  ๋์ธ ์ค„ ์•Œ์•˜๋Š”๋ฐ
ใ…‹ใ…‹ใ…‹ ๋งž์•„ ๊ฒ‰๋ณด๊ธฐ์—” ์‰ฌ์›Œ ๋ณด์ด๋Š”๋ฐ ํŒŒ๋ฉด ํŒ”์ˆ˜๋ก ์ง„์งœ ๋ฌด๊ถ๋ฌด์ง„ํ•จ. ๊ทธ๋ž˜์„œ Shopify ๊ฐœ๋ฐœ์ž ์ˆ˜์š”๊ฐ€ ๊ณ„์† ๋†’์€ ๊ฑฐ์ž„! ์ด์ œ API๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ ์‹œ์ž‘ํ•ด๋ด ๐Ÿ’ช
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.