Shopify 테마 기초 실습 · 힌트와 정답은 클릭해서 펼쳐보세요.

Shopify 테마 기초 실습 가이드

상품 상세(Product) 화면을 기준으로 테마를 수정하고 확장하는 실습 가이드다. 상품 정보에서 시작해 안내 배너·FAQ·배송 안내를 같은 상품 화면에 추가한다. P5에서는 상품 구매 흐름을 이해하기 위해 장바구니 요청을, P6에서는 공용 코드의 영향을 이해하기 위해 컬렉션·검색의 상품 카드까지 살펴본다. HTML·CSS·JavaScript의 기본 문법을 아는 사람을 대상으로 한다. Liquid, 테마 파일 구조, 편집기 설정, 옵션 변경에 따른 화면 갱신을 작은 실습으로 연결한다.

권장 실습 환경: 테스트 데이터를 포함한 개발 스토어와 함께 제공되는 Test Data 테마. 각자 테스트 데이터를 포함한 개발 스토어를 만들고, 제공된 Test Data 테마로 실습한다. 작성자가 사용한 테마의 코드상 이름과 버전은 Generated Data Theme 1.0.0이다. 다른 테마나 버전에서는 파일·함수 구조가 달라질 수 있다.

실습 방법: 각자 사용 권한이 있는 개발 스토어와 개발 테마에서 순서대로 실습한다. 이 저장소는 완성된 참고 코드이므로 먼저 직접 작성하고, 막히면 단계별 가이드와 접힌 힌트·정답을 확인한다. 각 단계의 확인 기준을 충족한 뒤 다음 단계로 진행한다.

학습 순서와 사용 방법

화면 흐름은 상품 정보 수정 → 안내 영역 설정화 → 상품 페이지에 배너 추가 → 상품별 데이터 연결 → 구매 흐름과 공용 카드 확인 → 품질 검사 → 배송 안내 직접 제작이다. 각 실습의 결과를 다음 단계에서 이어서 사용한다.

목표: 기존 테마의 수정 요청을 이해하고, 수정 위치와 영향 범위를 판단하여 구현·검증할 수 있게 된다. 앱 개발, 독립 테마 제작, Theme Store 출시, 실제 주문과 운영 배포는 이 가이드의 범위에 포함하지 않는다.

처음 시작한다면 0~15 → 16-1~16-2 → P1~P8 순서로 진행한다. 앞 단계에서 만든 코드를 다음 단계에서 확장하므로 중간 단계부터 시작할 때는 필요한 코드가 있는지 먼저 확인한다.

구간 실습 내용
0~4 개발 미리보기, 파일 구조, 블록 순서, Liquid와 상품 가격
5~8 옵션별 화면 갱신, snippet 분리, 편집기 설정과 CSS 변수
9~13 새 섹션, 블록, presets, 반응형 배치와 열 수 설정
14~15 문제 원인 확인과 블록별 링크
16-1~16-2 배너 이미지와 상품 메타필드
P1~P3 동적 소스 연결, 상품 템플릿 분리, FAQ 제작
P4~P6 선택 옵션 상태 갱신, 장바구니 요청 추적, 공용 카드 수정
P7~P8 번역·이미지·품질 검사, 배송 안내 종합 과제
  1. 각 단계의 파일·위치와 문제를 읽고 먼저 직접 작성한다.
  2. 막히면 접힌 힌트를 펼치고, 시도한 뒤 정답·해설과 비교한다.
  3. 코드가 작동하는 이유를 설명하고 확인 기준을 개발용 미리보기에서 점검한다. 이해되지 않거나 동작이 다르면 해당 단계에서 해결한 뒤 다음 단계로 진행한다.
  4. 예시에 나온 상품이 없으면 자신의 테스트 상품으로 대체한다. 서로 다른 가격의 옵션, 구매 가능·불가 옵션, 메타필드 값이 있는 상품과 없는 상품을 준비하면 비교하기 쉽다.

이 저장소는 완성된 참고 구현이다. 각자의 테마에서 기존 코드와 설정을 확인하고 중복 추가하지 않는다. 연습용 가격 목록 등은 데이터 흐름을 배우기 위한 예시이므로 운영 화면에 그대로 적용할 필요는 없다.

0. 개발용 미리보기 준비

왜 배우나요? 코드가 반영되는 개발 환경을 먼저 확보해야 이후 실습의 결과를 확인할 수 있다.

학습 목표: 자신의 개발 테마에서 상품 상세 미리보기와 편집기를 열고 로컬·원격 변경을 구분한다.

테마 준비: 테스트 데이터를 포함한 개발 스토어를 만들고, 함께 제공된 Test Data 테마로 실습한다.

  1. 개발 스토어 생성 시 Shopify가 제공하는 테스트 데이터를 포함하도록 선택한다.
  2. 생성된 개발 스토어의 테마 목록에서 Test Data 테마를 확인한다.
  3. 해당 테마의 코드를 로컬에 준비하고 아래 이름·버전을 확인한 뒤 개발용 미리보기를 실행한다.

Theme Store에서 별도 테마를 구매하는 과정은 필요하지 않다. 일반 스토어를 만들 때 항상 제공되는 기본 테마와는 구분한다. 테스트 데이터와 제공 테마 안내

작성자가 사용한 테마의 코드 정보는 다음과 같다. config/settings_schema.json의 theme_info 항목에서 확인할 수 있다.

항목 실습 기준
theme_name Generated Data Theme
theme_version 1.0.0
theme_author Shopify

테마 목록의 표시 이름과 코드에 기록된 이름은 다를 수 있으므로 위 정보를 함께 확인한다. 새로 제공되는 테마가 항상 같은 버전이라고 가정하지 않는다. 해당 테마가 없거나 구조가 다르면 5단계의 assets/global.js 안에 renderProductInfo()가 있는지 먼저 확인한다. 찾을 수 없다면 그 단계의 코드를 그대로 붙이지 말고 자신의 테마에서 옵션 변경을 처리하는 파일을 확인한 뒤 진행한다.

위치: 자신의 개발 테마 코드를 내려받은 로컬 폴더의 루트.

목표: 로컬 수정이 개발용 미리보기에 반영되는 경로를 확인한다.

시작 전에 개발 스토어 접근 권한, Shopify CLI, 코드 편집기, 로컬 테마 소스가 필요하다. 로컬 폴더에 layout, sections, templates, assets 등이 있는지 확인한다. 준비되지 않았다면 Shopify CLI 테마 시작 안내를 먼저 확인한다. 준비가 끝나면 테마 루트에서 실행한다. YOUR-STORE.myshopify.com은 본인의 개발 스토어 도메인으로 바꾼다.

shopify theme dev --store YOUR-STORE.myshopify.com

터미널이 안내하는 인증을 완료하고, 출력된 미리보기 URL과 테마 편집기 URL을 사용한다. 기본 로컬 미리보기는 http://127.0.0.1:9292이며 실제 출력 주소를 우선한다. 실행 중인 터미널을 유지하고, 끝낼 때는 Ctrl+C로 중지한다.

이 명령은 로컬 파일을 Shopify의 개발용 테마로 올린다. 이미 같은 환경의 개발용 테마가 있으면 로컬 상태로 갱신될 수 있다. 연습 중에는 운영 테마 ID나 --allow-live를 지정하지 않는다.

문제

  1. 편집기에서 섹션을 추가하고 저장하면, 로컬 파일도 반드시 바뀌는가?
  2. 홈 페이지에 추가한 섹션의 구성은 어디에 저장되는가?
힌트 열기 닫기

힌트 1 — 편집기 저장과 로컬 반영

저장 대상이 원격 개발 테마인지 로컬 폴더인지 구분해 보세요. 업로드와 편집기 변경 동기화는 방향이 다릅니다.

힌트 2 — 페이지 구성의 저장 위치

홈 페이지를 담당하는 JSON 템플릿을 찾아보세요. 섹션의 배치와 설정값이 어디에 들어 있는지 확인합니다.

정답·해설 열기 닫기

정답 1 — 편집기 저장과 로컬 파일 반영

편집기의 저장은 먼저 Shopify 쪽 테마에 반영된다. 로컬 자동 동기화를 보장하는 것은 아니다. 편집기 변경도 로컬에 반영하며 실습하려면 실행 중인 개발 명령을 중지한 뒤 다음 옵션을 포함해 다시 실행할 수 있다.

shopify theme dev --store YOUR-STORE.myshopify.com --theme-editor-sync

로컬과 원격에 서로 다른 편집이 있다면 충돌 내용을 확인하고 보존할 쪽을 선택한다. 무조건 한쪽으로 덮어쓰는 전략을 기본값으로 사용하지 않는다.

정답 2 — 홈 페이지 구성의 저장 위치

홈 페이지의 섹션 구성·순서는 templates/index.json에 저장된다. 기본 상품 템플릿이면 templates/product.json, 다른 상품 템플릿이면 해당 JSON이다. 섹션 내부 블록의 구성도 그 섹션 인스턴스 데이터에 포함된다.

확인 기준: 개발용 미리보기와 편집기를 열 수 있다. 원격 저장과 로컬 동기화를 구분해 설명한다. 로컬 동기화는 실제 파일 변경을 확인했을 때만 완료로 기록한다.

1. 화면에서 수정할 파일 찾기

왜 배우나요? 미리보기가 열리면 화면의 각 부분을 어떤 파일이 만드는지 알아야 수정 위치를 고를 수 있다.

학습 목표: 상품 데이터·템플릿·섹션·snippet·CSS의 역할과 변경 영향 범위를 구분한다.

파일: layout/theme.liquid, templates/product.json, sections/main-product.liquid.

문제

  1. 상품명 자체를 바꾸는 요청과 상품명의 글자 크기를 바꾸는 요청은 각각 어디에서 처리하는가? 크기 설정이 편집기에 없는 경우도 설명한다.
  2. 서로 다른 상품 템플릿이 같은 main-product 섹션을 사용한다. 그 섹션의 제목 출력을 조건 없이 바꾸면 어느 상품들에 영향이 있는가?
  3. 상품 페이지에 별도 배너 섹션을 추가하면 main-product.liquid 안에 배너 코드가 자동 삽입되는가?
힌트 열기 닫기

힌트 1 — 데이터와 디자인 구분

상품명 자체는 상품 데이터이고 글자 크기는 표현입니다. 관리자 입력값과 테마 설정·CSS 중 어느 쪽을 바꿀지 나눠 보세요.

힌트 2 — 공용 섹션의 사용처

두 템플릿의 type이 같은 섹션을 가리키는지 확인하세요. 템플릿 파일이 다르다는 이유만으로 섹션 코드까지 분리되지는 않습니다.

힌트 3 — 배너를 추가한 위치

템플릿의 섹션 목록과 main-product 내부 마크업을 구분하세요. 새 섹션을 배치하는 일과 기존 섹션의 코드를 바꾸는 일은 다릅니다.

정답·해설 열기 닫기

정답 1 — 상품명과 글자 크기의 수정 위치

상품명 데이터는 Admin의 상품 편집에서 바꾼다. 글자 크기는 제공되는 테마 설정으로 조절하거나, 설정이 없으면 CSS를 수정한다. 운영자가 반복해서 조절해야 한다면 설정과 CSS를 연결한다.

정답 2 — 공용 섹션 변경의 영향 범위

수정한 섹션을 사용하는 템플릿이 적용된 상품들이 영향을 받는다. 특정 상품에만 적용하는 조건문이 있다면 그 조건으로 범위가 좁아질 수 있다.

정답 3 — 배너 추가와 상품 섹션의 관계

새 배너는 별도 섹션으로 템플릿에 배치된다. main-product.liquid는 상품 페이지 전체가 아니라 상품 정보 섹션의 구현이다.

아래는 상품 템플릿에 배너를 추가한 경우의 구조 예시다. 현재 로컬 product.json에 이미 이 구성이 저장되어 있다는 뜻은 아니다.

layout/theme.liquid
└─ templates/product.json
   ├─ sections/main-product.liquid
   │  ├─ snippets/price.liquid
   │  └─ snippets/buy-buttons.liquid
   └─ sections/learning-banner.liquid

snippet은 필요한 위치에서 재사용하는 Liquid 조각이며 독립적인 섹션 추가 항목은 아니다.

확인 기준: 실제 코드에서 상품명·가격·구매 버튼의 출력 경로를 찾고, 수정 영향 범위를 설명한다.

2. 가격 블록 순서 바꾸기

왜 배우나요? 파일 구조를 익힌 뒤에는 가장 작은 구성 변경으로 템플릿의 역할을 확인한다.

학습 목표: 구매 기능을 유지하면서 상품 상세의 가격 블록 위치만 바꾼다.

파일: templates/product.json.

수정 위치: 상품 정보 섹션의 block_order.

문제: 가격을 옵션 선택 영역 바로 위로 옮긴다. 다른 블록끼리의 상대적인 순서는 유지한다. HTML이나 구매 로직은 바꾸지 않는다.

힌트 열기 닫기

힌트 1 — 순서를 저장하는 속성

blocks와 block_order 중 어느 값이 화면 순서를 결정하는지 확인하세요.

힌트 2 — 이동할 ID

price와 variant_picker의 위치를 찾고 가격 ID 하나만 옮겨 보세요. 나머지 ID는 삭제하지 않습니다.

정답·해설 열기 닫기

price를 variant_picker 바로 앞으로 옮긴다. 이 저장소의 해당 배열은 다음과 같다.

[
  "title",
  "vendor",
  "price",
  "variant_picker",
  "quantity_selector",
  "buy_buttons",
  "description",
  "share"
]

배열만 해당 block_order의 값으로 사용한다. 템플릿 파일 전체를 이 배열로 교체하지 않는다. 비활성화된 블록은 순서에 있어도 표시되지 않을 수 있다.

확인 기준: 옵션이 있는 상품에서 가격의 위치가 바뀐다. 옵션 선택 표시와 기존 가격 갱신이 계속 동작한다.

3. 판매 가능 여부에 따라 문구 출력하기

왜 배우나요? 배치만 바꾸는 단계에서 나아가 상품 상태에 따라 다른 내용을 출력한다.

학습 목표: Liquid 조건문으로 상품 전체의 구매 가능 여부를 표시한다.

파일: sections/main-product.liquid.

학습 삽입 위치: 제목 블록의 <h1> 아래. 완료 코드에서는 이 부분이 learning-product-info snippet으로 분리되어 있다.

문제

  1. 같은 상품의 Ice variant는 구매 불가, Dawn variant는 구매 가능하다. Ice를 선택했을 때 product.available은 무엇인가? 이유도 설명한다.
  2. 구매 가능한 상품에는 “구매 가능한 옵션이 있습니다.”, 아니면 “현재 구매 가능한 옵션이 없습니다.”를 <p>로 출력한다.
힌트 열기 닫기

힌트 1 — 상품 전체와 선택 옵션

product.available이 상품 전체를 판단하는지 현재 선택 옵션만 판단하는지 생각해 보세요. 다른 구매 가능 옵션이 남아 있다는 조건이 중요합니다.

힌트 2 — 조건과 출력 문법

조건에는 {% %}, 값 출력에는 {{ }}를 사용합니다. 일반 HTML을 출력 괄호로 감싸지 말고, 비교 연산자와 조건문 종료 태그를 확인하세요.

정답·해설 열기 닫기

정답 1 — Ice를 선택했을 때의 product.available

Dawn을 구매할 수 있으므로 true다. 구매 가능한 variant가 하나라도 있으면 참이고, 현재 선택한 variant 하나의 상태와는 다르다. 판매 가능 여부를 단순히 재고 수량이 0보다 큰지와 동일하게 취급하지 않는다. 재고 추적·품절 후 판매 설정 등도 관련된다.

정답 2 — 구매 가능 여부에 따른 문구 출력

{% if product.available %}
  <p>구매 가능한 옵션이 있습니다.</p>
{% else %}
  <p>현재 구매 가능한 옵션이 없습니다.</p>
{% endif %}

이는 문법 학습용 고정 문구다. 이후 설정으로 대체하며, 실제 다국어 배포에서는 고정 UI 문구의 locale 처리도 별도로 설계한다.

확인 기준: 구매 가능한 상품과 모든 variant가 구매 불가능한 상품에서 각각 올바른 문구 하나만 표시된다. 확인용으로 운영 상품의 재고를 변경하지 않는다.

4. 가격 데이터, 필터, 반복문 구분하기

왜 배우나요? 상태를 읽었다면 가격과 옵션 목록도 읽어 상품 데이터 구조를 익힌다.

학습 목표: 상품 최저가와 옵션별 가격을 구분하고 필터·반복문으로 출력한다.

파일: sections/main-product.liquid.

삽입 위치: 3단계에서 추가한 구매 가능 안내 바로 아래. 6단계에서 이 코드를 snippets/learning-product-info.liquid로 옮긴다.

문제

  1. product.price를 money 필터로 <p>에 출력한다. 선택한 옵션 가격과 항상 같은가?
  2. product.variants를 반복해 각 조합의 이름과 가격을 한 줄씩 출력한다.
  3. money를 붙이면 관리자에 저장된 금액도 바뀌는가?
힌트 열기 닫기

힌트 1 — 상품 가격과 금액 형식

product.price에 money 필터를 연결하세요. 상품에 가격이 다른 옵션이 있을 때 어느 값을 대표하는지도 확인합니다.

힌트 2 — 옵션 반복

product.variants를 반복하고 각 variant의 title과 price를 읽으세요. 반복문을 닫는 태그도 필요합니다.

힌트 3 — 데이터와 표시 형식

필터가 저장된 데이터를 수정하는지, 출력 형태만 바꾸는지 구분해 보세요.

정답·해설 열기 닫기

정답 1 — 상품 가격 출력과 선택 옵션 가격의 차이

<p>{{ product.price | money }}</p>

product.price는 상품의 최저 옵션 가격이므로 선택 옵션의 가격과 항상 같지는 않다.

정답 2 — 옵션 이름과 가격 목록 출력

{% for variant in product.variants %}
  <p>{{ variant.title | escape }} - {{ variant.price | money }}</p>
{% endfor %}

정답 3 — money 필터와 저장 가격

money는 표시 형식을 바꾸며 관리자에 저장된 금액은 바꾸지 않는다.

표현 비교

표현 의미
product.price 상품 variant 중 최저가
variant.price 현재 다루는 variant의 가격
product.selected_or_first_available_variant.price 렌더링 시 선택된 variant, 없으면 기본으로 선택할 variant의 가격
money 스토어의 금액 표시 형식 적용

money는 저장 가격이나 실제 청구 금액을 변경하지 않는다. 선택된 variant가 없을 때의 대체 선택 때문에 “항상 지금 클릭한 옵션의 값”이라고만 설명하지 않는다.

variant 이름은 일반 텍스트이므로 위 예시처럼 escape를 적용해 출력한다.

확인 기준: 가격이 다른 두 variant를 선택하며 목록과 기존 가격을 비교한다. 할인·구독 판매 플랜의 최종 금액은 이 단순 비교 범위에 포함하지 않는다. 판매 플랜이 있는 연습 상품은 같은 일회성 구매 조건에서 비교한다.

5. 옵션 변경과 가격 갱신 추적하기

왜 배우나요? 앞에서 추가한 Liquid 가격은 옵션을 클릭했다고 저절로 갱신되지 않는다.

학습 목표: 옵션 변경 요청과 응답을 추적해 직접 만든 가격 영역도 갱신한다.

파일: assets/global.js의 VariantSelects, VariantRadios, renderProductInfo()와 상품 정보 섹션.

가이드: change 이벤트 → 선택 옵션 읽기 → variant 찾기 → renderProductInfo() 호출 순서로 읽는다. 버튼형 선택기는 VariantRadios가 옵션 값을 읽는 구현을 사용한다. 전역 파일 전체를 읽거나 새로 작성할 필요는 없다.

문제 A — 읽기와 관찰

  1. 옵션이 바뀌면 Liquid가 브라우저에서 자동 재실행되는가?
  2. destination과 source는 각각 어디에서 요소를 찾는가?
  3. Network의 Fetch/XHR에서 variant와 section_id를 포함하는 요청을 찾는다. Response의 형식과 가격을 확인한다.
  4. 실제 destination이 찾는 ID와 같은 ID를 만드는 Liquid 여는 태그를 찾는다.
문제 A 힌트 열기 닫기

힌트 A-1 — 실행 위치

옵션 변경 때 Network에 새 요청이 생기는지 확인하세요. Liquid를 실행하는 곳과 HTML을 바꾸는 곳을 구분합니다.

힌트 A-2 — 요소를 찾는 문서

getElementById 앞의 객체를 보세요. originalSection이라는 이름만으로 현재 문서인지 응답 문서인지 판단하지 않습니다.

힌트 A-3 — 응답 형식과 값

Fetch/XHR 요청의 Response를 열어 HTML인지 JSON인지 확인하고 선택 옵션의 가격을 찾아보세요. DOMParser에 들어가는 값도 단서입니다.

힌트 A-4 — ID를 만드는 태그

JavaScript의 ID 접두사와 같은 문자열을 Liquid에서 검색하세요. ID 전체가 일치하는 요소를 찾아야 합니다.

문제 A 정답·해설 열기 닫기

정답 A-1 — Liquid의 실행 위치

현재 구현은 선택 variant와 섹션 ID를 서버에 보내고, 서버가 렌더링한 HTML을 받는다. JavaScript가 그 HTML을 파싱해 필요한 영역을 현재 화면에 반영한다.

정답 A-2 — destination과 source의 문서

const destination = document.getElementById(`price-${this.dataset.section}`);
const source = html.getElementById(
  `price-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}`
);
if (source && destination) destination.innerHTML = source.innerHTML;

document는 현재 화면 문서, html은 응답을 파싱한 문서다. 위 코드는 가격 영역 내부만 교체한다. 그 사실은 변수 이름이 아니라 ID 선택자와 해당 HTML 구조를 함께 읽어 판단한다.

정답 A-3 — Network 응답 확인

응답은 선택한 variant에 맞춰 서버가 렌더링한 HTML이다. Response에서 가격 영역을 찾아 해당 옵션의 가격인지 확인한다. 요청 URL의 variant 값과 현재 선택 옵션도 비교한다.

정답 A-4 — ID에 대응하는 Liquid 요소

대응하는 요소는 main-product.liquid의 id="price-{{ section.id }}"가 붙은 <div>다. MainProduct-…는 바깥 상품 요소이며 별개다. 같은 섹션 ID를 포함해도 전체 HTML ID가 다르면 다른 요소다.

서버 응답을 받기만 하고 DOM을 갱신하지 않으면 화면 가격은 바뀌지 않는다. 해당 가격 영역 밖에 추가한 요소도 자동으로 교체되지 않는다.

문제 B — 직접 만든 가격도 갱신하기

상품명 아래 학습 가격에 learning-price-와 섹션 ID로 고유 ID를 붙인다. 최저가 대신 선택된 variant의 가격을 출력한다. 기존 응답 처리 안에서 이 요소도 찾아 교체한다.

문제 B 힌트 열기 닫기

힌트 B-1 — 고유한 가격 영역

기존 가격 ID를 복제하지 말고 학습 가격용 접두사와 섹션 ID를 조합하세요.

힌트 B-2 — 두 문서에서 요소 찾기

현재 화면과 서버 응답에서 같은 ID 규칙으로 요소를 찾으세요.

힌트 B-3 — 교체할 변수

새 변수로 존재 여부를 확인했다면 대입문도 그 새 변수들을 사용해야 합니다.

문제 B 정답·해설 열기 닫기

정답 B — 직접 만든 가격 영역 갱신

section 안에 직접 작성할 때의 가격 요소다. snippet으로 이동한 이후에는 section.id 대신 전달받은 section_id를 사용한다.

<p id="learning-price-{{ section.id }}">
  {{ product.selected_or_first_available_variant.price | money }}
</p>

다음 코드는 기존 renderProductInfo()의 응답 처리 콜백 안, html을 만든 뒤에 추가한다. 현재 선택과 요청 당시 variant가 다른지 검사하는 기존 보호 코드, 기존 가격·재고·SKU·버튼 갱신은 유지한다. 함수 전체를 이 예제로 덮어쓰지 않는다.

const learningDestination = document.getElementById(`learning-price-${this.dataset.section}`);
const learningSource = html.getElementById(
  `learning-price-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}`
);
if (learningDestination && learningSource) {
  learningDestination.innerHTML = learningSource.innerHTML;
}

확인 기준: 서로 가격이 다른 두 variant에서 기존 가격과 추가 가격이 함께 갱신된다. Network 응답의 해당 가격과 현재 화면 값이 일치한다. 불가능한 옵션 조합, 품절·할인·판매 플랜 전체를 지원하는 완성된 구매 UI로 간주하지 않는다.

6. 연습 코드를 snippet으로 분리하기

왜 배우나요? 안내와 가격 코드가 늘어났으므로 역할별로 분리해 이후 설정을 연결하기 쉽게 만든다.

학습 목표: 학습 영역을 snippet으로 옮기고 명시적으로 값을 전달하면서 동작을 유지한다.

파일: sections/main-product.liquid, snippets/learning-product-info.liquid.

문제: 구매 가능 안내·학습 가격·variant 목록을 snippet으로 옮긴다. 원래 위치에서는 상품과 섹션 ID를 명시적으로 전달해 호출한다. 기존 화면과 가격 갱신은 유지한다.

힌트 열기 닫기

힌트 1 — 옮길 범위

구매 가능 안내·가격·옵션 목록을 새 snippet으로 옮기고 원래 위치에는 호출만 남기세요.

힌트 2 — 값 전달

render 이름에는 .liquid를 붙이지 않습니다. 인자는 매개변수명: 전달할 값 형식입니다.

힌트 3 — 섹션 ID 연결

snippet 안에서는 전달받은 매개변수 이름을 사용하세요. 호출부의 지역 변수가 자동 공유된다고 가정하지 않습니다.

정답·해설 열기 닫기
{% render 'learning-product-info',
  product: product,
  section_id: section.id
%}

snippet의 가격 ID는 다음처럼 변경한다.

<p id="learning-price-{{ section_id }}">
  {{ product.selected_or_first_available_variant.price | money }}
</p>

snippet은 전용 HTML 래퍼 없이 필요한 HTML·Liquid 조각을 담을 수 있다. render도 자동으로 <div>를 추가하지 않는다. 원래 위치의 코드는 제거하고 호출로 교체하여 ID 중복을 피한다.

render는 지역 변수 범위가 분리된다. 다만 전역·문맥 객체까지 무조건 접근할 수 없는 것은 아니다. product를 명시적으로 전달하는 이유는 snippet의 의존성을 분명하게 하기 위해서다. 여기서 section_id는 직접 정한 매개변수 이름이며, 전달한 section.id 값이 담긴다.

이후 단계까지 완료한 파일에는 추가 설정 매개변수가 더 있다. 이미 완료 코드를 사용한다면 위 기초 예제로 전체 호출을 되돌리지 않는다.

확인 기준: 안내·가격·목록이 중복 없이 한 번씩 나오고 옵션 가격 갱신이 유지된다. 호출부의 값과 snippet의 매개변수 이름을 구분해 설명한다.

7. 편집 가능한 문구와 표시 체크박스

왜 배우나요? 분리한 안내 문구를 운영자가 코드 수정 없이 바꿀 수 있도록 한다.

학습 목표: 섹션 설정을 snippet에 전달하고 문구·표시 여부·빈 값 처리를 연결한다.

파일: sections/main-product.liquid의 최상위 schema settings, snippet 호출부, snippets/learning-product-info.liquid.

문제 A — 설정 정의와 전달

다음 두 입력 설정을 추가한다. 기존 enable_sticky_info와 같은 섹션 설정 배열에 넣고, 내부 블록 설정에 넣지 않는다.

id type label default
available_message text 구매 가능 안내 문구 구매 가능한 옵션이 있습니다.
show_available_message checkbox 구매 가능 안내 표시 불리언 true

두 값을 같은 이름의 snippet 매개변수로 전달한다. 기존 product, section_id는 유지한다.

문제 A 힌트 열기 닫기

힌트 A-1 — 설정 정의

JSON 속성 이름을 큰따옴표로 감싸고 마지막 속성 뒤의 쉼표를 제거하세요. checkbox 기본값은 문자열이 아닌 불리언입니다.

힌트 A-2 — 설정 전달

section.settings.<id>로 값을 읽어 기존 render 호출에 같은 이름의 인자로 추가하세요.

문제 A 정답·해설 열기 닫기

아래는 두 설정 객체를 담은 예시 배열이다. 실제 파일에서는 기존 settings 배열 안에 객체들만 추가한다.

[
  {
    "type": "text",
    "id": "available_message",
    "label": "구매 가능 안내 문구",
    "default": "구매 가능한 옵션이 있습니다."
  },
  {
    "type": "checkbox",
    "id": "show_available_message",
    "label": "구매 가능 안내 표시",
    "default": true
  }
]

호출부에는 available_message: section.settings.available_message와 show_available_message: section.settings.show_available_message를 추가한다. 기존 인자 사이의 쉼표도 유지한다.

문제 B — 빈 값과 표시 조건

상품을 구매할 수 있을 때, 체크박스가 켜져 있고 문구가 비어 있지 않은 경우에만 안내 <p>를 출력한다. 구매 불가능 안내와 나머지 가격·목록은 그대로 유지한다.

문제 B 힌트 열기 닫기

힌트 B-1 — 표시 조건

구매 가능 안내에만 체크박스를 적용하세요. 상품 구매 가능 조건과 안내 표시 조건의 범위를 구분합니다.

힌트 B-2 — 빈 값과 안전한 출력

blank와 and로 표시 조건을 결합하고, 일반 문구를 출력할 때 escape를 적용하세요.

문제 B 정답·해설 열기 닫기
{% if product.available %}
  {% if show_available_message and available_message != blank %}
    <p>{{ available_message | escape }}</p>
  {% endif %}
{% else %}
  <p>현재 구매 가능한 옵션이 없습니다.</p>
{% endif %}

빈 문구일 때도 <p>를 항상 출력하면 여백이 남을 수 있다. 문구만 비우는 것이 아니라 요소의 생성 자체를 조건으로 제어한다.

확인 기준: 편집기에서 문구 변경·저장 후 새로고침 유지, 표시 켬/문구 있음, 표시 끔/문구 있음, 표시 켬/문구 없음의 세 경우를 확인한다. Elements에서 빈 안내 <p>가 없는지는 별도 확인 항목이다.

8. 글자 크기 설정과 CSS 변수

왜 배우나요? 내용뿐 아니라 표현도 편집기에서 조절할 수 있어야 한다.

학습 목표: 숫자 설정값을 CSS 변수로 연결해 안내 문구의 글자 크기만 바꾼다.

파일: sections/main-product.liquid, snippets/learning-product-info.liquid, assets/section-main-product.css.

문제 A — 연결하기

  1. 섹션 설정에 available_message_size를 추가한다. 타입 range, 범위 12~24, 간격 1, 기본값 16, 단위 px다.
  2. snippet에 설정값을 같은 이름의 매개변수로 전달한다.
  3. 안내 <p>에 고정 클래스 learning-available-message를 붙인다. 인라인 CSS 변수 --available-message-size에 크기와 단위를 전달한다.
  4. CSS 파일에서 그 변수를 font-size로 사용한다. 7단계 표시 조건은 유지한다.
문제 A 힌트 열기 닫기

힌트 A-1 — range 설정

범위·간격·기본값을 먼저 정하세요. schema의 unit은 편집기 표시 정보이며 CSS 단위를 붙여주는 기능은 아닙니다.

힌트 A-2 — snippet 인자

기존 render 호출의 인자에 크기 설정값을 추가하세요. 이미 전달하는 값은 유지합니다.

힌트 A-3 — 요소에 변수 선언

고정 클래스는 문자열로 쓰고, style에는 속성: 값; 형식을 사용하세요. 크기 값에 px 단위도 필요합니다.

힌트 A-4 — 변수 참조

CSS의 font-size에서 선언한 변수명을 정확히 참조하세요. 기존 표시 조건은 바꾸지 않습니다.

문제 A 정답·해설 열기 닫기

정답 A-1 — 크기 설정 정의

{
  "type": "range",
  "id": "available_message_size",
  "label": "구매 가능 안내 글자 크기",
  "min": 12,
  "max": 24,
  "step": 1,
  "unit": "px",
  "default": 16
}

정답 A-2 — snippet에 설정값 전달

기존 render 호출부에 available_message_size: section.settings.available_message_size를 추가한다. 다른 매개변수와 구분하는 쉼표도 유지한다.

정답 A-3 — 안내 요소의 클래스와 CSS 변수

7단계의 조건문 안에서 안내 <p>를 다음 형태로 쓴다.

<p class="learning-available-message"
   style="--available-message-size: {{ available_message_size }}px;">
  {{ available_message | escape }}
</p>

정답 A-4 — CSS에서 변수 사용

.learning-available-message {
  font-size: var(--available-message-size);
}

변수 이름의 하이픈과 밑줄은 다르다. 선언과 참조가 정확히 일치해야 한다.

문제 B — 왜 이렇게 쓰는가

  1. var(--available-message-size)px로 단위를 CSS 쪽에 붙일 수 있는가?
  2. <p>와 나란히 있는 가격 <div>에 같은 클래스를 붙이면 <p>의 변수값도 전달되는가?
  3. 편집기에서 16을 24로 바꿀 때마다 개발자가 코드를 직접 수정해야 하는가?
  4. 선언이 길어지면 반드시 하나의 {% style %} 블록으로 옮겨야 하는가?
문제 B 힌트 열기 닫기

힌트 B-1 — 단위 결합

CSS의 var()가 문자열을 이어 붙이는 기능인지 생각해 보세요.

힌트 B-2 — 변수 상속

변수가 선언된 요소와 형제·자식의 관계를 그려보세요. 클래스 공유와 값 상속은 다릅니다.

힌트 B-3 — 코드와 렌더링 결과

개발자가 작성한 Liquid 원본과 설정 변경 후 브라우저가 받은 결과를 구분하세요.

힌트 B-4 — 선언 위치의 선택

요소 하나에만 쓰는 값인지, 섹션 전체나 여러 파일에서 재사용하는 값인지 먼저 판단하세요.

문제 B 정답·해설 열기 닫기

정답 B-1 — 단위를 var() 뒤에 붙일 수 있는가

var() 뒤에 단위 토큰을 붙여도 20px처럼 합쳐지지 않는다. 값에 단위를 포함하거나 숫자 변수에 calc(var(--값) * 1px) 같은 계산을 사용한다. 정답 B-2 — 형제 요소가 변수값을 받는가

일반 CSS 사용자 정의 속성은 해당 요소와 자식에게 상속된다. 형제는 <p>에 선언된 값을 받지 않는다. 같은 클래스는 CSS 규칙의 선택 조건일 뿐 값을 복사하지 않는다. 정답 B-3 — 설정 변경마다 코드 수정이 필요한가

설정 연결 코드를 한 번 작성하면 편집기 변경만으로 반영된다. 편집기가 바뀐 설정으로 섹션을 다시 렌더링한다. Liquid가 브라우저에서 계속 값을 감시하는 것은 아니다. 정답 B-4 — 선언을 반드시 한 곳에 모아야 하는가

반드시 그럴 필요는 없다. 아래 세 방식 모두 사용 가능하며 기존 테마 관례, 적용 범위, 재사용성을 보고 선택한다.

구성 특징
요소의 인라인 CSS 변수 요소별 값을 가까이서 관리
섹션 ID로 범위를 제한한 {% style %} 같은 섹션의 여러 선언을 모아 관리
인라인에서 선언 전용 snippet 호출 반복되는 변수 생성 로직을 분리·재사용

Shopify 공식 Horizon도 인라인 CSS 변수와 선언용 snippet을 사용하고, Dawn은 섹션별 {% style %} 규칙도 사용한다. 어느 하나가 개발자 전체의 보편적인 선호라고 단정하지 않는다. 근거는 아래 공식 자료에서 확인할 수 있다.

직접 font-size에 Liquid 설정값을 넣는 방식도 편집기 변경을 반영한다. 단, 숫자 20px만 직접 작성한 하드코딩과는 다르다. {% style %} 안에서는 Liquid를 처리하지만 {% stylesheet %} 안에서는 Liquid를 처리하지 않는다.

확인 기준: 12px와 24px에서 안내 문구만 크기가 달라진다. 다른 상품 제목·가격에는 영향을 주지 않는다. 코드 원본과 렌더링 결과를 구분해 설명한다.

9. 추가 가능한 안내 배너 섹션 만들기

왜 배우나요? 기존 상품 정보 섹션 수정에서 독립적으로 추가 가능한 영역 제작으로 확장한다.

학습 목표: 상품 템플릿에 제목·문구를 편집할 수 있는 새 배너 섹션을 추가한다.

새 파일: sections/learning-banner.liquid. 기존 완료 파일에서는 뒤 단계의 코드도 들어 있으므로 전체를 기초 예제로 덮어쓰지 않는다.

문제: 배너 제목과 문구를 편집할 수 있는 섹션을 만든다. 제목은 <h2>, 문구는 <p>, 바깥 클래스는 learning-banner다. 편집기의 섹션 추가 목록에 “학습용 안내 배너”로 나타나게 한다.

힌트 열기 닫기

힌트 1 — 입력란 정의

제목과 문구를 schema settings에 정의하세요. 배열 안에는 설정 객체가 들어갑니다.

힌트 2 — 화면 출력

섹션에서는 section.settings로 값을 읽어 출력하세요.

힌트 3 — 추가 목록 등록

편집기의 섹션 추가 목록에 나타나게 하는 presets가 필요한지 확인하세요.

정답·해설 열기 닫기

이 단계만 수행한 새 파일의 전체 예시다.

<section class="learning-banner">
  <h2>{{ section.settings.heading | escape }}</h2>
  <p>{{ section.settings.message | escape }}</p>
</section>

{% schema %}
{
  "name": "학습용 안내 배너",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "제목",
      "default": "스토어 안내"
    },
    {
      "type": "text",
      "id": "message",
      "label": "안내 문구",
      "default": "스토어 소식을 확인하세요."
    }
  ],
  "presets": [{ "name": "학습용 안내 배너" }]
}
{% endschema %}

schema 태그는 한 파일에서 기존 블록에 이어서 관리한다. 기능을 추가할 때마다 새 schema 블록을 중복 생성하지 않는다. 파일 저장 후 테마 편집기에서 상품 템플릿을 선택하고 상품 본문 영역에 이 섹션을 추가한다. 이후 배너·FAQ·배송 안내도 이 상품 템플릿에서 실습한다.

확인 기준: 추가 목록에 나타나고, 새 배너를 추가한 뒤 제목·문구 변경이 화면에 반영된다.

10. 반복 가능한 안내 블록 만들기

왜 배우나요? 배너에 배송·교환 등 안내 항목이 늘어나도 코드를 복사하지 않도록 한다.

학습 목표: 블록을 정의하고 반복 출력하여 항목별 편집·정렬·삭제를 지원한다.

파일: sections/learning-banner.liquid.

출력 위치: 기존 배너 문구 아래, 닫는 </section> 위.

문제: “안내 항목” 블록을 여러 개 추가하고 순서를 바꿀 수 있게 한다. 블록 타입은 notice, 설정은 heading과 message인 text 입력 두 개다. 항목은 <h3>와 <p>로 출력한다.

힌트 열기 닫기

힌트 1 — 설정의 범위

섹션 전체 설정과 개별 블록 설정을 구분하세요. 반복 항목은 block.settings를 사용합니다.

힌트 2 — 반복 출력

section.blocks를 반복하고 각 블록 안에서 제목과 문구를 출력하세요.

힌트 3 — 편집기 연결

블록 래퍼의 속성 위치에 block.shopify_attributes를 출력하세요.

정답·해설 열기 닫기
{% for block in section.blocks %}
  <div {{ block.shopify_attributes }}>
    <h3>{{ block.settings.heading | escape }}</h3>
    <p>{{ block.settings.message | escape }}</p>
  </div>
{% endfor %}

schema 최상위에 blocks 배열을 추가한다.

{
  "blocks": [
    {
      "type": "notice",
      "name": "안내 항목",
      "settings": [
        {
          "type": "text",
          "id": "heading",
          "label": "항목 제목",
          "default": "배송 안내"
        },
        {
          "type": "text",
          "id": "message",
          "label": "항목 내용",
          "default": "배송 정보를 입력하세요."
        }
      ]
    }
  ]
}

위 JSON의 blocks 속성을 기존 schema의 settings, presets와 같은 수준에 병합한다. schema 전체를 이 조각으로 교체하지 않는다. notice는 직접 정한 블록 타입이고, text는 Shopify가 제공하는 입력 설정 타입이다.

block.shopify_attributes는 편집기가 렌더링된 요소를 해당 블록과 연결하는 데 사용한다. 별도의 고정 문자열이나 따옴표 안에 넣지 않고 속성 위치에서 출력한다.

확인 기준: 블록 두 개의 문구가 독립적으로 수정되고, 순서 변경과 삭제가 반영된다. 이 설정은 현재 섹션 내부의 블록이며, 별도 blocks/ 파일로 재사용하는 theme block 학습과는 구분한다.

11. 새 섹션의 기본 블록 구성하기

왜 배우나요? 블록 구조가 갖춰졌으므로 새 배너의 시작 구성을 운영자에게 제공한다.

학습 목표: presets로 새 섹션에만 기본 안내 블록을 생성한다.

파일: sections/learning-banner.liquid의 schema presets.

문제: 새 배너를 추가할 때 배송 안내와 교환 안내 블록 두 개를 자동 생성한다. 각각 제목과 문구의 초기값을 지정한다.

힌트 열기 닫기

힌트 1 — 종류와 초기 구성

최상위 blocks는 허용할 종류이고 preset 안의 blocks는 처음 만들 인스턴스 목록입니다.

힌트 2 — 초기값 객체

preset의 settings는 설정 정의 배열이 아니라 값 객체입니다. settings.heading을 키 이름 하나로 쓰지 마세요.

힌트 3 — 확인 대상

기존 배너가 아니라 새 배너를 추가해서 초기 구성을 확인하세요.

정답·해설 열기 닫기
{
  "presets": [
    {
      "name": "학습용 안내 배너",
      "blocks": [
        {
          "type": "notice",
          "settings": {
            "heading": "배송 안내",
            "message": "배송 정보를 확인하세요."
          }
        },
        {
          "type": "notice",
          "settings": {
            "heading": "교환 안내",
            "message": "교환 방법을 확인하세요."
          }
        }
      ]
    }
  ]
}

기존 schema의 presets 값만 교체한다. 마지막 객체 뒤의 쉼표는 허용되지 않는다. 이미 배치한 섹션의 구성을 덮어쓰는 명령이 아니므로 새 배너를 추가해서 검사한다.

확인 기준: 새 배너에 두 블록과 지정 문구가 기본으로 들어간다. 기존 배너에는 자동으로 추가되지 않는 이유를 설명한다.

12. 모바일 1열, 데스크톱 2열 만들기

왜 배우나요? 여러 안내 항목이 좁은 화면에서도 읽기 좋게 배치되어야 한다.

학습 목표: 배너 CSS를 연결하고 화면 너비에 따라 1열·2열을 적용한다.

파일: sections/learning-banner.liquid, 새 assets/section-learning-banner.css.

문제: 반복문 전체를 learning-banner__items 클래스로 한 번 감싼다. 개별 블록에는 learning-banner__item 클래스를 붙인다. 750px 미만은 1열, 이상은 2열이며 간격은 16px이다. 새 CSS 파일을 섹션에서 불러온다.

힌트 열기 닫기

힌트 1 — 목록과 항목 구분

반복문 전체의 래퍼와 개별 항목에 서로 다른 고정 클래스를 붙이세요.

힌트 2 — CSS 연결

새 CSS 파일을 섹션에서 불러오는 줄이 있는지 먼저 확인하세요.

힌트 3 — 반응형 규칙

기본 1열을 먼저 작성하고 미디어 쿼리에서 2열로 바꾸세요. CSS 선언은 세미콜론으로 구분합니다.

정답·해설 열기 닫기

섹션 파일 맨 위에서 CSS를 불러온다.

{{ 'section-learning-banner.css' | asset_url | stylesheet_tag }}

기존 반복문을 다음 구조로 바꾼다.

<div class="learning-banner__items">
  {% for block in section.blocks %}
    <div class="learning-banner__item" {{ block.shopify_attributes }}>
      <h3>{{ block.settings.heading | escape }}</h3>
      <p>{{ block.settings.message | escape }}</p>
    </div>
  {% endfor %}
</div>

CSS 파일 내용은 다음과 같다. 13단계까지 완료했다면 열 수는 이미 변수로 바뀌어 있으므로 이 중간 예제를 다시 덮어쓰지 않는다.

.learning-banner__items {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media screen and (min-width: 750px) {
  .learning-banner__items {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

minmax(0, 1fr)은 열의 최소 크기를 0으로 두어 콘텐츠의 최소 너비 때문에 열이 밀려나는 문제를 줄인다. 긴 문자열의 줄바꿈·이미지 크기까지 모두 해결하는 규칙은 아니다.

확인 기준: 안내 블록을 세 개 이상 두고 모바일 1열, 데스크톱 2열과 다음 줄 배치를 확인한다. 미디어 쿼리는 창의 겉모양보다 미리보기 문서의 viewport 너비를 기준으로 한다.

13. 데스크톱 열 수를 설정으로 받기

왜 배우나요? 고정된 반응형 배치를 편집기에서 조절할 수 있도록 확장한다.

학습 목표: 데스크톱 열 수를 설정으로 받고 배너 인스턴스별로 독립 적용한다.

파일: sections/learning-banner.liquid, assets/section-learning-banner.css.

문제: 최상위 섹션 설정에 desktop_columns를 추가한다. 2~4열, 간격 1, 기본값 2의 range 설정이다. 단위는 없다. 열 수 변수를 섹션별로 선언하고 데스크톱 미디어 쿼리에서 사용한다. 모바일 1열은 유지한다.

힌트 열기 닫기

힌트 1 — 설정값 준비

최상위 settings에 열 수 range를 추가하세요.

힌트 2 — 섹션별 범위

바깥 요소의 ID와 style 선택자 양쪽에 section.id를 사용하세요. 설정값은 출력 괄호로 감쌉니다.

힌트 3 — 기존 CSS 수정

데스크톱 repeat()의 첫 인자만 변수로 바꾸고 모바일 1열 규칙은 유지하세요.

정답·해설 열기 닫기

기존 최상위 settings 배열에 다음 객체를 추가한다.

{
  "type": "range",
  "id": "desktop_columns",
  "label": "데스크톱 열 수",
  "min": 2,
  "max": 4,
  "step": 1,
  "default": 2
}

CSS를 불러오는 줄 아래에 변수 선언을 넣는다.

{% style %}
  #LearningBanner-{{ section.id }} {
    --learning-banner-columns: {{ section.settings.desktop_columns }};
  }
{% endstyle %}

기존 바깥 <section>의 여는 태그에 id="LearningBanner-{{ section.id }}"를 추가한다. 같은 이름의 ID를 내부 다른 요소에 중복 부여하지 않는다.

CSS 파일의 데스크톱 규칙만 변경한다.

@media screen and (min-width: 750px) {
  .learning-banner__items {
    grid-template-columns: repeat(var(--learning-banner-columns), minmax(0, 1fr));
  }
}

섹션 바깥 요소의 변수를 내부 목록이 상속받는다. 각 배너는 다른 섹션 ID를 가지므로 서로 다른 설정값을 가질 수 있다. 4열을 선택했다면 750px 이상에서는 4열이고, 미만에서는 곧바로 1열이다. 자동으로 4→3→2→1열로 줄어드는 규칙은 작성하지 않았다.

확인 기준: 블록 네 개 이상으로 2·3·4열 설정과 750px 미만의 1열을 확인한다. 배너 두 개를 배치해 각각 2열·4열을 주고 설정이 섞이지 않는지도 확인한다.

14. 문제가 생겼을 때 확인할 순서

왜 배우나요? 설정·Liquid·CSS가 연결된 상태에서는 화면이 안 바뀌는 원인을 단계별로 좁혀야 한다.

학습 목표: 업로드·응답·실행 경로·DOM·CSS를 근거로 문제 원인을 찾는다.

문제: “저장은 했는데 화면이 그대로다”라는 보고를 받았다. 무엇을 먼저 확인할지 설명한 뒤 해당 증상 하나를 실제 개발 도구로 조사한다. 재현을 위해 운영 데이터를 바꾸지 않는다.

힌트 열기 닫기

힌트 1 — 반영 경로

로컬 파일 → 업로드 → 브라우저 응답 순서로 어느 단계까지 변경됐는지 확인하세요.

힌트 2 — 실행과 표시

응답이 바뀌었다면 실행 경로·DOM·계산된 CSS를 확인하세요.

힌트 3 — 원인 판단

화면이 그대로라는 사실만으로 캐시나 문법을 원인으로 단정하지 마세요.

정답·해설 열기 닫기
증상 확인할 근거
새 섹션이 안 보임 파일이 sections/에 저장됐는지, schema 오류·업로드 로그, presets, 올바른 개발 테마/템플릿, 실제 섹션 추가 여부
추가 가격만 갱신 안 됨 배포된 JS 내용, 콘솔 오류, learningSource·learningDestination 존재, 응답과 현재 DOM의 ID
이전 JS가 실행되는 듯함 Network에서 받은 파일 내용 확인, 개발자 도구의 캐시 비활성화 후 새로고침 비교
배너가 계속 1열 미리보기 viewport가 750px 이상인지, CSS 로딩, 변수 선언·상속, 바깥 ID 일치, Computed의 Grid 값
편집기 설정이 안 생김 섹션 설정과 블록 설정의 위치, JSON 유효성, CLI 업로드 오류
기본 블록이 안 생김 presets 변경 후 새 섹션을 추가했는지
문구가 보이지 않음 구매 가능 여부, 체크박스, blank 조건, 선택한 섹션 인스턴스
태그에 클래스가 안 붙음 고정 문자열을 Liquid 변수처럼 {{ }}로 감쌌는지

실제 학습에서는 " default"처럼 속성 이름 안에 공백이 들어간 오타를 고친 뒤 열 설정이 정상 동작했다. JSON 자체는 파싱 가능해도 Shopify schema의 올바른 속성과는 다를 수 있다. 이때 업로드 실패 로그를 확보하지 않았으므로 모든 1열 문제의 원인을 같은 오타로 일반화하지 않는다.

추가 가격 갱신 문제는 캐시 비활성화와 새로고침 후 해결됐다. 이전 JS가 남았을 가능성은 있지만 당시 캐시 상태를 별도 측정한 것은 아니다.

확인 기준: 관찰, 가설, 수정, 재검증을 구분해 기록한다. “정상” 대신 어떤 입력에서 어떤 화면이 바뀌었는지 설명한다.

15. 블록별 링크 설정

왜 배우나요? 이제 배너 안내를 읽는 데서 실제 배송·교환 페이지로 이동할 수 있게 한다.

학습 목표: 블록별 링크를 조건부로 출력하고 키보드 접근성을 유지한다.

파일: sections/learning-banner.liquid, assets/section-learning-banner.css.

문제

  1. notice 블록 설정에 URL 입력란 link를 추가한다.
  2. 각 항목 제목을 조건부 링크로 만든다. 바깥 블록 래퍼와 block.shopify_attributes는 유지한다.
  3. 링크가 있는 경우에만 클릭 가능한 표현을 주고 키보드 포커스를 확인한다.
힌트 열기 닫기

힌트 1 — 링크 설정 위치

링크는 배너 전체가 아니라 블록마다 다릅니다. 어느 settings 배열에 넣을지 판단하세요.

힌트 2 — 조건부 링크

URL이 비었을 때는 빈 href를 출력하지 않고 일반 제목을 유지하세요.

힌트 3 — 시각 표현과 키보드

링크에만 클릭 가능한 표현을 적용하고 키보드 포커스가 보이는지 확인하세요.

정답·해설 열기 닫기

정답 1 — 블록별 URL 설정

notice 블록의 settings 배열에 다음 객체를 추가한다. 새로운 최상위 섹션 설정이 아니다.

{
  "type": "url",
  "id": "link",
  "label": "안내 링크"
}

정답 2 — 링크가 있을 때만 제목을 링크로 출력

기존 반복문 안의 <h3>만 교체한다. 안내 문구와 블록 래퍼는 유지한다.

<h3>
  {% if block.settings.link != blank %}
    <a class="learning-banner__link" href="{{ block.settings.link | escape }}">
      {{ block.settings.heading | escape }}
    </a>
  {% else %}
    {{ block.settings.heading | escape }}
  {% endif %}
</h3>

정답 3 — 링크 표현과 키보드 포커스

기본 링크의 인지성과 포커스 표시를 보존한다. 예를 들어 다음처럼 링크에만 밑줄과 포커스를 지정할 수 있다.

.learning-banner__link {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.learning-banner__link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

이는 학습용 최소 예시다. 기존 테마의 링크·포커스 스타일과 충돌하는지 실제 화면에서 확인한 뒤 적용한다. 상품 가격 갱신 코드는 변경하지 않는다.

확인 기준

확인할 점: URL 입력 형식은 type: "url"로 지정하지만 값을 읽을 때는 ID인 block.settings.link를 사용한다. 빈 링크는 blank로 검사한다. 설정은 presets가 아니라 notice 블록의 settings에 정의한다. 입력란이 보이지 않을 때는 개별 블록 선택 여부와 개발 서버 실행 상태, 편집 중인 테마를 확인한다.

16. 이미지와 상품 메타필드

16-1에서 배너 이미지를 추가하고, 16-2에서 상품마다 다른 안내 문구를 출력한다. 두 실습을 마친 뒤 P1로 진행한다.

16-1. 배너 이미지와 최대 너비

왜 배우나요? 링크까지 만든 배너에 시각 정보를 추가하되 화면 크기에 맞게 제한한다.

학습 목표: 이미지 선택·빈 값 처리·최대 너비 설정을 구현한다.

파일: sections/learning-banner.liquid, assets/section-learning-banner.css.

문제: 최상위 settings에 image_picker 타입의 image를 추가하고, 선택한 이미지만 출력한다. image_max_width range 설정은 200~1200px, 간격 50px, 기본값 1200px로 만든다. 설정값과 부모 너비 중 작은 값을 최대 표시 너비로 적용한다.

힌트 열기 닫기

힌트 1 — 이미지 설정 범위

배너 전체의 이미지이므로 최상위 섹션 설정에 추가하세요.

힌트 2 — 이미지 출력

image_url로 URL을 만들고 image_tag로 태그를 출력하세요. 이미지가 없는 경우도 처리합니다.

힌트 3 — 크기 제한

섹션 ID로 범위를 제한한 CSS 변수를 만들고 이미지의 최대 너비에 연결하세요.

정답·해설 열기 닫기

먼저 sections/learning-banner.liquid의 기존 schema settings 배열에 다음 두 객체를 추가한다. 기존 객체와의 구분 쉼표를 유지한다.

[
  {
    "type": "image_picker",
    "id": "image",
    "label": "배너 이미지"
  },
  {
    "type": "range",
    "id": "image_max_width",
    "label": "이미지 최대 너비",
    "min": 200,
    "max": 1200,
    "step": 50,
    "unit": "px",
    "default": 1200
  }
]

위 배열 전체로 기존 settings를 덮어쓰지 않고 안의 객체만 추가한다.

제목 위에 다음 코드를 추가한다.

{% if section.settings.image != blank %}
  {{ section.settings.image | image_url: width: 1200 | image_tag: class: 'learning-banner__image' }}
{% endif %}

기존 {% style %}의 #LearningBanner-{{ section.id }} 규칙에 변수를 추가한다.

--learning-banner-image-max-width: {{ section.settings.image_max_width }}px;

이미지 CSS는 다음과 같다.

.learning-banner__image {
  display: block;
  max-width: min(100%, var(--learning-banner-image-max-width));
  height: auto;
}

크기 확인: 생성된 <img>의 width·height 속성과 계산된 CSS를 함께 확인한다. 예를 들어 HTML의 width가 1200이고 다른 제한이 없다면 최대 너비 설정 1200px·부모 900px에서 900px로 표시될 수 있다. display: block 자체가 이미지를 늘리는 것은 아니며, max-width는 상한선이고 height: auto는 비율을 유지한다.

개발자 도구에서 이미지의 대체 텍스트인 alt도 확인한다.

확인 기준: 이미지 선택·제거, 최대 너비 설정 변경, 모바일에서 비율 유지와 가로 넘침 여부를 확인한다. 배너를 두 개 추가해 서로 다른 최대 너비 설정이 각각 적용되는지도 확인한다.

16-2. 상품별 안내 문구 메타필드

왜 배우나요? 지금까지의 섹션 설정은 공통 안내에 적합하지만 상품마다 다른 내용은 상품 데이터로 관리해야 한다.

학습 목표: 상품 메타필드를 정의하고 상품별 문구를 조건부로 출력한다.

파일: snippets/learning-product-info.liquid.

관리자 작업: 제품 메타필드 정의와 상품별 값 저장.

문제: 같은 템플릿의 서로 다른 상품에 각기 다른 안내 문구를 출력한다. custom.learning_notice를 단일 값의 한 줄 텍스트로 정의한다. Selling Plans Ski Wax에는 “사용 전 표면을 닦아주세요.”, The Complete Snowboard에는 “사용 후 물기를 제거해주세요.”를 저장한다. 값이 없으면 안내용 <p>도 만들지 않는다.

힌트 열기 닫기

힌트 1 — 데이터 준비

메타필드 정의를 저장하는 일과 각 상품에 값을 입력하는 일은 별개입니다.

힌트 2 — 값 읽기

product.metafields.네임스페이스.키.value로 실제 값을 읽으세요.

힌트 3 — 출력과 비교

빈 값 조건과 escape를 적용하세요. 같은 상품의 옵션이 아닌 서로 다른 상품을 비교합니다.

정답·해설 열기 닫기

기존 옵션 목록 반복문의 {% endfor %} 다음에 추가한다.

{% if product.metafields.custom.learning_notice.value != blank %}
  <p>{{ product.metafields.custom.learning_notice.value | escape }}</p>
{% endif %}

같은 템플릿을 사용해도 현재 product가 다르므로 상품별 값이 출력된다. 상품 메타필드는 같은 상품의 옵션을 바꾼다고 별도의 옵션별 값으로 바뀌지 않는다.

메타필드 정의 화면:

단일 값의 한 줄 텍스트로 만든 상품별 안내 문구 정의

상품에 입력한 값:

상품별 안내 문구에 입력한 사용 전 표면을 닦아주세요 문구

확인 기준: 서로 다른 두 상품에서 각자의 문구가 표시되어야 한다. 값이 없는 상품에는 안내용 빈 <p>도 생성되지 않아야 한다.

P. 기능 확장과 종합 실습

기초 실습에서 만든 배너와 상품 정보 snippet을 이어서 사용한다. 새 기능의 요구사항을 읽고 직접 구현한 뒤 접힌 힌트·정답과 비교한다.

P1. 동적 소스 연결 실습

왜 배우나요? 상품 메타필드를 코드로 읽는 방법에 이어 편집기에서 데이터 소스를 연결하는 방법을 익힌다.

학습 목표: 기존 배너 설정에 동적 소스를 연결하고 상품별 값과 빈 값 표시를 확인한다.

위치: 개발 테마 편집기 → 상품 템플릿 → 학습용 안내 배너 → 섹션의 안내 문구. 개별 안내 항목 블록의 문구가 아니다.

관련 파일: sections/learning-banner.liquid의 section.settings.message 출력과 최상위 message 설정. 첫 연결 단계에서는 코드를 수정하지 않는다. 기존 snippets/learning-product-info.liquid의 직접 출력은 비교를 위해 유지한다.

문제: custom.learning_notice를 배너의 안내 문구에 동적 소스로 연결한다. 같은 템플릿의 Selling Plans Ski Wax와 The Complete Snowboard에서 각각 다른 문구가 표시되는지 확인한다. 메타필드 값이 없는 상품의 표시도 관찰한다. 이 설정과 직접 Liquid 출력은 어떤 차이가 있는지 설명한다.

힌트 열기 닫기

힌트 1 — 연결 대상

상품 템플릿을 미리보는 상태에서 배너 섹션의 안내 문구 설정을 선택하세요.

힌트 2 — 동적 소스

입력란 옆의 연결 아이콘에서 호환되는 상품 메타필드를 찾으세요.

힌트 3 — 보이지 않을 때

현재 상품 컨텍스트·설정 타입·메타필드 타입·편집 중인 테마를 확인하세요.

정답·해설 열기 닫기

섹션의 안내 문구 설정에서 동적 소스 연결을 열고 상품별 안내 문구를 선택하여 저장한다. 배너 코드는 계속 section.settings.message를 출력하지만, 연결된 설정값은 현재 상품의 메타필드에서 가져온다. 직접 출력 방식은 코드가 product.metafields.custom.learning_notice.value를 명시한다. 연결 방식을 쓰면 지원되는 설정에서 운영자가 데이터 소스를 선택할 수 있다. 연결 단위는 이 배너 섹션 인스턴스의 message 설정이다. 새 배너 인스턴스에는 별도 연결이 필요하다. 템플릿은 이 구성과 설정을 저장하는 곳이며, 같은 템플릿의 모든 배너에 연결이 자동 전파되는 것은 아니다.

같은 상품 템플릿의 두 상품을 번갈아 미리보면 배너에도 각각 저장한 문구가 표시되어야 한다. 기존 snippet 출력과 값이 같은지 비교한다. 메타필드가 비어 있을 때 기본 문구로 자동 대체된다고 가정하지 않는다. 배너의 안내 태그는 snippet 조건문과 별개다. 빈 태그를 만들지 않도록 다음 조건을 적용한다.

{% if section.settings.message != blank %}
  <p>{{ section.settings.message | escape }}</p>
{% endif %}

확인 기준: 두 상품에서 각각의 문구가 표시되고 값이 없는 상품에서는 빈 안내 태그가 생성되지 않아야 한다. 설정에 직접 저장한 문자열·동적 소스로 연결한 값·Liquid에서 직접 읽은 값의 차이를 설명한다.

P2. 상품군별 템플릿과 적용 범위

왜 배우나요? 상품별 문구뿐 아니라 화면 구성 자체를 다르게 할 때는 템플릿의 적용 범위를 알아야 한다.

학습 목표: 상품 템플릿을 분리하고 설정 변경과 공용 코드 변경의 영향을 구분한다.

작업 위치: 자신의 개발 테마 편집기 → 상품 템플릿 선택 메뉴.

관련 파일: 기본 상품 템플릿 templates/product.json, 새 템플릿 templates/product.learning-special.json, 공용 섹션 sections/learning-banner.liquid. 편집기에서 만든 파일이 로컬 작업 폴더에도 동기화됐다고 가정하지 않는다.

문제: 기본 상품 템플릿을 바탕으로 learning-special 템플릿을 만들고, 새 템플릿에만 학습용 안내 배너를 하나 추가한다. The Minimal Snowboard 또는 자신의 테스트 상품으로 미리본 뒤 기본 상품 템플릿과 구성이 다른지 확인한다. 이미 같은 이름의 템플릿이 있다면 새로 만들지 않고 기존 구성을 확인한다.

Step 1. 별도 템플릿 만들기

테마 편집기의 상품 템플릿 선택 메뉴에서 Create template을 선택한다. 이름은 learning-special, 기반은 기본 상품 템플릿으로 정한다.

힌트 — 템플릿과 섹션 파일의 관계 열기 닫기

힌트 1 — 템플릿의 역할

JSON 템플릿은 섹션의 종류·배치·설정값을 저장합니다.

힌트 2 — 섹션과의 관계

배너 추가가 main-product 내부 코드 삽입을 의미하는지 구분하세요. 같은 종류의 섹션은 Liquid 파일을 공유합니다.

Step 2. 새 템플릿의 구성 변경하기

learning-special을 편집하고 있는지 확인한 뒤 학습용 안내 배너를 추가하고 저장한다. 기본 상품 템플릿으로 돌아가 같은 배너가 자동으로 추가되지는 않았는지 비교한다.

힌트 — 설정을 바꾼 위치 확인하기 열기 닫기

힌트 1 — 현재 편집 대상

테마 이름과 선택한 템플릿이 learning-special인지 확인하세요.

힌트 2 — 변경 범위

상품 본문에 배너를 추가하세요. 헤더 같은 공유 영역의 변경과 구분합니다.

Step 3. 상품을 바꿔 미리보기

새 템플릿을 선택한 상태에서 미리보기 상품을 테스트 상품으로 변경한다. 배너 구성이 유지되는지 확인한다. 미리보기 상품 선택은 관리자에서 해당 상품의 템플릿을 배정하는 작업과 구분한다.

힌트 — 미리보기와 실제 상품 배정 열기 닫기

힌트 1 — 미리보기 상품

편집기의 미리보기 상품 선택 영역에서 테스트 상품을 변경하세요.

힌트 2 — 실제 배정과 구분

관리자의 Theme template 설정은 별도입니다. 새 템플릿이 배정 목록에 없다면 개발 테마를 게시하지 말고 미리보기 범위만 확인하세요.

Step 4. 변경의 영향 범위 설명하기

다음 두 경우를 비교한다. 아직 코드를 수정할 필요는 없다.

  1. learning-special의 배너 제목 설정값만 바꾸면 기본 상품 템플릿의 배너 제목도 바뀌는가?
  2. 두 템플릿이 모두 사용하는 sections/learning-banner.liquid의 제목 마크업을 조건 없이 바꾸면 어느 화면에 적용되는가?
정답·해설 열기 닫기

정답 1 — 별도 배너의 제목 설정 변경

해당 템플릿에 저장된 배너 인스턴스의 설정만 바뀌므로 다른 템플릿의 별도 배너 설정은 바뀌지 않는다.

정답 2 — 공용 배너 코드 변경

공용 파일 변경은 공용 섹션 코드를 바꾸므로 그 섹션을 사용하는 두 템플릿의 화면에 모두 적용된다. 해당 섹션을 사용하지 않는 화면에는 적용되지 않는다.

상품 미리보기 변경은 현재 템플릿에 넣어 볼 상품 데이터를 바꾸는 일이다. 상품의 실제 템플릿 배정까지 바뀌었다는 의미는 아니다. 배정 가능한 환경에서는 테스트 상품에 새 템플릿을 배정하고, 기본 템플릿을 사용하는 다른 상품과 비교해 확인한다.

확인 기준: 새 템플릿에만 배너가 추가된 구성과 테스트 상품 미리보기를 확인하고, 설정 변경·공용 코드 변경·실제 상품 배정의 차이를 설명한다. 실제 배정은 가능한 환경에서 별도 확인하며 미실행을 완료로 기록하지 않는다.

P3. FAQ 섹션 제작

왜 배우나요? 별도 상품 구성을 만들었다면 같은 화면에 운영자가 관리하는 FAQ도 추가해 본다.

학습 목표: 질문·richtext 답변 블록과 기본 HTML 펼침·접힘으로 FAQ를 제작한다.

새 파일: sections/learning-faq.liquid. HTML 출력은 파일 위쪽, schema는 아래쪽에 작성한다.

문제: P2에서 만든 상품 템플릿에 추가할 수 있는 FAQ 섹션을 만든다. 섹션 제목은 text, 반복 블록의 질문은 text, 답변은 richtext로 받는다. 질문과 답변이 모두 있을 때만 항목을 출력한다. 별도 JavaScript 없이 펼침·접힘을 제공하고, 블록 순서 변경·모바일 표시·키보드 조작을 확인한다. 설정 id는 섹션 제목 heading, 블록 타입 question, 블록 안 질문 question과 답변 answer로 한다. presets는 섹션 추가용 이름만 등록하고 초기 블록은 생략한다.

힌트 열기 닫기

힌트 1 — schema 구조

섹션·블록·preset에는 name, 개별 입력 설정에는 label을 사용하세요. 전체 schema를 JSON 객체로 감쌉니다.

힌트 2 — 펼침과 접힘

details 안에 summary를 배치하면 기본 HTML 동작을 이용할 수 있습니다.

힌트 3 — 값과 조건

질문은 escape하고 richtext 답변은 서식 HTML을 유지하세요. 질문과 답변이 모두 있는지도 검사합니다.

정답·해설 열기 닫기
<section class="learning-faq">
  <h2>{{ section.settings.heading | escape }}</h2>
  {% for block in section.blocks %}
    {% if block.settings.question != blank and block.settings.answer != blank %}
      <details {{ block.shopify_attributes }}>
        <summary>{{ block.settings.question | escape }}</summary>
        <div class="learning-faq__answer">{{ block.settings.answer }}</div>
      </details>
    {% endif %}
  {% endfor %}
</section>

{% schema %}
{
  "name": "학습용 FAQ",
  "settings": [
    {
      "id": "heading",
      "type": "text",
      "label": "제목",
      "default": "자주 묻는 질문"
    }
  ],
  "blocks": [
    {
      "type": "question",
      "name": "질문과 답변",
      "settings": [
        {
          "id": "question",
          "type": "text",
          "label": "질문"
        },
        {
          "id": "answer",
          "type": "richtext",
          "label": "답변"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "학습용 FAQ"
    }
  ]
}
{% endschema %}

block.shopify_attributes는 편집기에서 블록을 식별하는 데 사용하며, 펼침·접힘 자체는 HTML의 기본 동작이다. open 속성이 없으므로 처음에는 닫힌다. 조건문은 <details> 전체를 감싸 빈 항목의 제목·컨테이너까지 숨긴다. 숨겨진 항목도 편집기 블록 목록에서 선택하여 값을 입력할 수 있다.

확인 기준: 섹션과 블록을 추가하고, 마우스와 키보드로 펼침·접힘을 확인한다. 질문이나 답변이 비어 있는 항목은 숨겨져야 한다. Tab으로 답변 안의 링크까지 이동하고 Enter로 열 수 있어야 한다. 블록 순서 변경과 모바일의 긴 문구 줄바꿈도 확인한다.

P4. 선택 옵션 상태 갱신

왜 배우나요? FAQ 다음에는 상품의 선택 상태에 따라 달라지는 안내로 돌아가 가격 갱신 흐름을 확장한다.

학습 목표: 선택 옵션의 구매 가능 안내와 기존 구매 버튼 상태가 일치하도록 갱신한다.

파일: snippets/learning-product-info.liquid의 마지막에 상태 영역을 추가한다. assets/global.js에서는 5단계에서 추가한 학습 가격 교체 코드 바로 아래에 상태 교체 코드를 추가한다.

문제: 선택 옵션의 구매 가능 안내를 표시하고 옵션 변경 응답으로 갱신한다. 현재 화면과 응답 DOM을 구분하고, 요청 중 버튼 비활성화 및 응답 후 복구 흐름을 설명한다.

힌트 열기 닫기

힌트 1 — 선택 옵션의 상태

상품 전체가 아니라 selected_or_first_available_variant의 available을 읽으세요.

힌트 2 — 갱신 대상 ID

학습 가격과 겹치지 않는 상태 영역 ID를 만드세요.

힌트 3 — 응답 반영

기존 학습 가격 교체 아래에서 두 문서의 상태 요소를 찾으세요. 오래된 응답을 거르는 조건 뒤에 있어야 합니다.

정답·해설 열기 닫기
<div id="learning-availability-{{ section_id }}" role="status">
  {% if product.selected_or_first_available_variant.available == true %}
    <p>선택한 옵션은 구매 가능합니다.</p>
  {% else %}
    <p>선택한 옵션은 구매할 수 없습니다.</p>
  {% endif %}
</div>
const availabilityDestination = document.getElementById(`learning-availability-${this.dataset.section}`);
const availabilitySource = html.getElementById(`learning-availability-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}`);
if (availabilityDestination && availabilitySource) availabilityDestination.innerHTML = availabilitySource.innerHTML;

document는 현재 화면이고 html은 응답을 파싱한 별도 문서다. ID가 같아도 같은 요소가 아니다. 이 테마의 Quick add는 ID 중복 방지를 위해 quickadd-를 붙이고 원본 ID를 originalSection에 보관한다. 일반 상품 페이지에서는 원본 별도 지정 없이 section을 사용한다.

옵션 변경 직후 toggleAddButton(true, '', false)가 현재 버튼을 비활성화한다. 응답 속 버튼의 disabled 유무로 상태를 갱신하며 응답 버튼이 없으면 비활성화한다. 현재 버튼 상태와 응답 버튼 상태를 코드 실행 전후로 구분해 확인한다.

확인 기준: 구매 가능 → 불가 → 가능 옵션으로 전환할 때 안내와 구매 버튼이 함께 갱신·복구되어야 한다. 빠르게 옵션을 바꿔 보고, 이전 요청의 응답이 늦게 도착했을 때 기존 코드가 이를 어떻게 거르는지 설명한다. 테스트 상품에 구매 불가 옵션이 없다면 이 조건을 검증한 것으로 처리하지 않는다.

P5. 장바구니 요청 추적

왜 배우나요? 화면의 선택 상태가 맞아도 실제 전송값이 다르면 다른 옵션이 장바구니에 담길 수 있다.

학습 목표: 상품 상세의 선택 옵션·수량이 장바구니 요청과 항목에 올바르게 전달되는지 추적한다.

파일: snippets/buy-buttons.liquid, assets/product-form.js, assets/global.js, assets/cart.js.

문제: 선택 옵션과 수량이 어디에 저장되어 어떤 요청으로 전송되는지 추적한다. 상품 추가와 장바구니 항목 수량 변경의 차이를 설명한다. 추가 실패 시 오류 표시·성공 처리 중단·로딩 정리 코드를 구분한다.

힌트 열기 닫기

힌트 1 — 전송값의 출처

input[name="id"] → updateVariantInput() → FormData 순서로 따라가세요.

힌트 2 — 추가와 변경 요청

Network에서 추가 요청의 id·quantity와 변경 요청의 line·quantity를 비교하세요.

힌트 3 — 오류 처리

then에 전달된 response가 JSON 데이터인지 확인하세요. 오류 상태 기록·콜백 종료·finally 정리를 구분합니다.

정답·해설 열기 닫기
  • input[name="id"]는 선택 옵션의 ID를 담는다. updateVariantInput()이 현재 선택한 옵션 ID로 갱신하고 FormData가 전송값을 구성한다. 화면 가격만 바꾸고 입력값을 갱신하지 않으면 이전 옵션 ID가 전송될 수 있다.
  • 추가 요청의 quantity: 2는 2개 추가를 의미한다. 변경 요청의 line: 1, quantity: 1은 장바구니 첫 항목의 수량을 1로 맞춘다. line은 1부터 시작하는 항목 순서이며 상품 ID가 아니다. 수량 0으로 변경하면 해당 항목을 삭제한다.
  • JSON 오류 응답의 status에는 422 같은 값이 들어와 if (response.status)가 참으로 평가된다. 이 값은 boolean true 자체가 아니며, 파싱 전 HTTP Response의 status와 구분한다.
  • publish(PUB_SUB_EVENTS.cartError, ...)는 구독 코드에 오류를 알리고 this.handleErrorMessage(response.description)이 오류 표시를 담당한다.
  • this.error = true는 오류 상태 기록, return은 현재 콜백 실행 종료다. 해당 return 이후 같은 콜백의 성공 처리는 실행되지 않지만 Promise 체인의 finally는 실행된다.
  • error가 true인 경로에서 finally는 로딩 클래스·스피너를 정리하며 if (!this.error)를 통과하지 않아 aria-disabled를 유지한다. 모든 오류 경로가 반드시 error를 true로 설정한다고 일반화하지 않는다.
  • window.location = window.routes.cart_url은 상품 URL이 아니라 장바구니 페이지로 이동한다. 추가 성공 후 갱신할 장바구니 UI가 없을 때의 처리다.

확인 기준: 선택 옵션의 ID와 추가 요청의 id가 같고 지정한 수량이 전송되어야 한다. 장바구니에서 수량 변경과 삭제 후 실제 항목 상태도 확인한다. 오류 처리 코드를 찾아 메시지 표시·성공 처리 중단·로딩 정리를 구분한다. 실패 응답을 직접 재현하지 않았다면 코드 해석과 실제 오류 화면 확인을 구분한다.

P6. 공용 상품 카드의 표시 범위 제어

왜 배우나요? 상품 상세에서 사용한 안내를 목록 카드에도 표시하면서 공용 snippet의 적용 범위를 익힌다.

학습 목표: 컬렉션에서만 안내를 표시하고 검색 결과에서는 호출 매개변수로 숨긴다.

파일: snippets/card-product.liquid, assets/component-card.css, sections/main-collection-product-grid.liquid, sections/main-search.liquid.

문제: 컬렉션·검색에서 사용하는 상품 카드의 가격 아래에 상품별 안내 문구를 추가하고 모바일 표시를 확인한다. 이어서 컬렉션에서만 문구를 표시하고 검색 결과에서는 숨긴다. 검색어 유무로 화면을 추측하는 대신 호출부에서 boolean 매개변수를 전달한다.

힌트 열기 닫기

힌트 1 — 카드의 상품 객체

카드는 card_product로 상품을 받습니다. 컬렉션과 검색의 render 호출을 찾아보세요.

힌트 2 — 표시 여부 인자

표시 여부는 상품 속성이 아닌 별도의 render 매개변수로 전달하세요.

힌트 3 — 검색 확인

상품명으로 검색해 전체 결과 페이지에서 확인하세요. 문구를 출력했다고 메타필드 검색 기능이 추가되는 것은 아닙니다.

정답·해설 열기 닫기

컬렉션의 render 'card-product'에는 show_learning_notice: true,, 검색 결과의 상품 카드 호출에는 show_learning_notice: false,를 추가한다. 상품을 전달하는 card_product 매개변수와 같은 단계에 둔다.

카드의 {% render 'price', product: card_product, price_class: '' %} 다음에 추가한다.

{% if card_product.metafields.custom.learning_notice.value != blank and show_learning_notice == true %}
  <p class="card-product__notice">
    {{ card_product.metafields.custom.learning_notice.value | escape }}
  </p>
{% endif %}

card_product.show_learning_notice로 접근하지 않는다. 두 매개변수는 별개다. 이 방식에서는 표시 매개변수를 전달하지 않는 다른 호출부에서도 안내 문구가 숨겨진다. 추천 상품 등에도 이 공용 snippet을 쓰므로 이 기본 동작을 알고 변경해야 한다.

assets/component-card.css의 끝에 작성한다.

.card-product__notice {
  margin: 8px 0 0;
  font-size: 14px;
  line-height: 1.5;
}

확인 기준: 컬렉션에는 문구가 표시되고 검색 결과에서는 숨겨져야 한다. 모바일 배치, 정렬·필터·페이지 이동 후 표시, 상품 카드의 링크 이동을 확인한다. 공용 카드의 다른 호출부에서도 의도한 표시 여부가 유지되는지 확인한다.

P7. 번역·이미지·품질 검사

왜 배우나요? 기능을 구현한 뒤에는 언어·이미지·키보드·모바일 환경에서도 의도대로 표시되는지 확인해야 한다.

학습 목표: 선택 옵션 안내를 번역하고 아래쪽 배너 이미지의 로딩을 설정한 뒤 정적 검사와 화면 검증을 수행한다.

파일: snippets/learning-product-info.liquid, locales/ko.json, locales/en.default.json, sections/learning-banner.liquid. 다른 언어 파일도 번역 키 반영 대상이다.

문제: 선택 옵션의 구매 가능 안내를 언어별로 표시하고, 첫 화면 아래의 배너 이미지에 지연 로딩을 적용한다. 코드 검사와 실제 화면 검사를 각각 수행한다.

Step 1. 안내 문구를 번역 키로 바꾸기

한국어·영어 locale 파일의 최상위에 learning.availability.available과 learning.availability.unavailable 키를 정의한다. snippet의 두 고정 문구를 해당 키를 사용하는 출력으로 바꾸고, 옵션과 언어를 바꿔 확인한다.

힌트 — 번역 파일과 키 경로 열기 닫기

힌트 1 — 번역 파일 선택

스토어 문구는 .json, 편집기 입력란 이름은 .schema.json에 둡니다.

힌트 2 — 키 경로와 출력

최상위 learning부터 키 경로를 맞추고 t 필터에 문자열로 전달하세요.

힌트 3 — JSON 병합

기존 객체에 속성을 추가할 때 구분 쉼표와 중괄호를 확인하세요.

정답·해설 열기 닫기

locales/ko.json의 최상위 객체에 다음 속성을 추가한다. 다른 속성이 뒤따르면 구분 쉼표를 붙인다.

"learning": {
  "availability": {
    "available": "선택한 옵션은 구매 가능합니다.",
    "unavailable": "선택한 옵션은 구매할 수 없습니다."
  }
}

locales/en.default.json에는 같은 키에 각각 The selected option is available.과 The selected option is unavailable.을 넣는다.

기존 learning-availability 영역의 조건문과 ID는 유지하고 출력만 바꾼다.

{% if product.selected_or_first_available_variant.available == true %}
  <p>{{ 'learning.availability.available' | t }}</p>
{% else %}
  <p>{{ 'learning.availability.unavailable' | t }}</p>
{% endif %}

서버가 번역한 HTML을 반환하고, 기존 JavaScript가 응답에서 해당 영역을 찾아 현재 화면에 복사한다. ID가 직접 번역을 수행하는 것은 아니다.

Step 2. 아래쪽 배너 이미지의 로딩 설정하기

첫 화면 아래에 배치한 배너의 image_tag에 지연 로딩 속성을 추가한다. 개발자 도구에서 생성된 이미지 태그와 화면 표시를 확인한다.

Step 2 힌트 열기 닫기

힌트 1 — 속성 전달 위치

이미지 URL을 만드는 필터와 이미지 태그를 만드는 필터 중 어디에 loading 속성을 전달해야 할지 생각해 보세요.

힌트 2 — 적용 대상

첫 화면 아래에 있는 배너인지 확인하세요. 로딩 순서를 강제로 지정하는 기능은 아닙니다.

Step 2 정답·해설 열기 닫기

기존 이미지 조건문 안의 출력에 loading: 'lazy'를 추가한다.

{{ section.settings.image | image_url: width: 1200 | image_tag: class: 'learning-banner__image', loading: 'lazy' }}

생성된 <img>에 loading="lazy"가 있는지 확인한다. lazy는 이미지들의 로딩 완료 순서를 지정하지 않는다. 배너를 첫 화면의 주요 이미지로 옮기면 로딩 방식을 다시 검토한다.

Step 3. 검사 결과와 화면 동작 확인하기

테마 루트에서 shopify theme check를 실행한다. 가능하면 수정 전에도 실행해 기존 지적과 새 지적을 구분한다. 자동 수정 옵션 없이 검사 결과부터 읽는다.

힌트 — 검사 통과와 화면 검증은 별개 열기 닫기

힌트 1 — 검사의 역할

Theme Check로 찾을 수 있는 코드 문제와 직접 조작해야 확인되는 동작을 나누세요.

힌트 2 — 화면 확인

키보드 이동·언어별 옵션 안내·모바일 이미지 표시를 직접 확인하세요.

힌트 3 — 기존 문제 구분

수정 전부터 있던 경고와 이번 변경으로 생긴 지적을 구분하세요.

확인 기준: 언어와 옵션에 맞는 안내가 표시되고 이미지 속성이 반영되어야 한다. 키보드 이동과 모바일 배치를 확인하고, Theme Check의 기존 지적과 신규 지적을 구분할 수 있어야 한다.

실무 적용 시 번역 관리: 번역 키를 추가·변경할 때 프로젝트에 존재하는 모든 언어 파일도 함께 업데이트한다. 이 저장소의 참고 구현은 한국어·영어 외 언어에 영어 문구를 넣었지만, 실제 서비스에서는 각 지원 언어에 맞는 번역을 제공한다. Theme Check로 키 누락 여부까지 확인한다.

P8. 상품별 배송 안내 섹션 종합 과제

왜 배우나요? 앞에서 익힌 설정·상품 데이터·조건문·CSS 범위를 하나의 새로운 요구사항에 적용한다.

학습 목표: 상품별 문구 우선순위, 기본 문구 대체, 숨김 조건과 배경색을 갖춘 배송 안내 섹션을 제작한다.

파일: sections/learning-shipping-notice.liquid.

관리자 데이터: 제품 메타필드 custom.shipping_notice, 한 줄 텍스트·단일 값.

문제 — 배송 안내 제작 요청: 상품마다 다른 배송 안내를 표시한다. 상품별 안내가 있으면 우선 사용하고, 없으면 편집기에서 지정한 기본 안내를 사용한다. 표시 여부를 끄거나 두 문구가 모두 비어 있으면 제목·문구·배송 안내 컨테이너를 출력하지 않는다. 제목만 비어 있으면 문구는 표시한다. 배경색을 편집할 수 있고 두 섹션의 설정은 서로 영향을 주지 않아야 한다. 모바일에서 긴 문구가 넘치지 않아야 한다.

설정 계약: show_notice(checkbox, 기본 true), heading(text, 기본 배송 안내), fallback_message(text, 기본값 생략), background_color(color, 기본 #ffffff). presets 이름은 배송 안내이며 반복 blocks는 필요 없다. 메타필드 값은 테마 설정과 별도로 상품에 저장한다.

P8 단계별 구현 가이드

요구사항을 구현 순서에 따라 나눈 단계다. 모든 코드는 sections/learning-shipping-notice.liquid에 작성한다. 먼저 각 단계의 과제를 시도하고, 막힐 때 힌트를 펼친다. 작성한 코드가 해당 조건을 만족하는지 확인하고 동작 원리를 설명할 수 있으면 다음 단계로 간다. 전체 코드는 아래의 접힌 기준 답안에서 확인할 수 있다.

Step 1. 편집기에서 조절할 설정 정의하기

작성 위치: 파일 하단 {% schema %} 안의 settings 배열.

과제: 표시 여부·제목·기본 안내 문구·배경색 네 가지 설정을 만든다. 위 설정 계약의 id를 그대로 사용한다. 아직 화면 출력은 작성하지 않는다.

힌트 — boolean과 checkbox, 배경색 타입 열기 닫기

힌트 1 — 입력 타입

표시 여부는 checkbox, 배경색은 color 입력을 사용하세요.

힌트 2 — 기본값

기본 안내 문구는 default를 생략합니다.

힌트 3 — JSON 구조

객체의 마지막 속성 뒤에는 쉼표를 붙이지 마세요. 네 설정이 하나의 settings 배열 안에 있어야 합니다.

Step 2. 상품별 문구를 변수에 담기

작성 위치: 파일 최상단, HTML과 {% schema %}보다 앞.

과제: 상품의 custom.shipping_notice 실제 값을 notice_message 변수에 저장한다. 이 단계에서는 아직 <p>를 출력하지 않는다.

힌트 — 값을 저장하는 문법과 출력하는 문법 열기 닫기

힌트 1 — 저장 문법

값을 저장하는 assign과 화면에 출력하는 중괄호를 구분하세요. assign 안에 출력 괄호를 중첩하지 않습니다.

힌트 2 — 실제 값

메타필드의 .value를 읽으세요. escape는 이후 HTML 출력 단계에서 적용합니다.

Step 3. 상품별 문구가 없으면 기본 문구로 대체하기

작성 위치: Step 2의 assign 바로 아래, HTML보다 앞.

과제: notice_message가 비었을 때만 section.settings.fallback_message를 같은 변수에 다시 저장한다.

힌트 — 조건문 안에서 assign하기 열기 닫기

힌트 1 — 대체 조건

blank로 빈 값을 검사하세요.

힌트 2 — 대체할 변수

조건문 안에서 즉시 출력하지 말고 notice_message에 기본 문구를 다시 저장하세요.

힌트 3 — 우선순위

상품별 문구가 이미 있으면 덮어쓰지 않아야 합니다. 기본 문구도 비었다면 빈 값으로 둡니다.

Step 4. 제목과 최종 안내 문구 출력하기

작성 위치: 문구를 결정하는 Liquid 코드 아래, {% schema %} 위.

과제: <section> 안에 제목과 최종 문구를 출력한다. 제목이 비어 있으면 제목 태그만 생략한다. 같은 제목을 두 번 출력하지 않는다.

힌트 — 제목과 안내 문구는 서로 다른 값 열기 닫기

힌트 1 — 출력할 값

제목은 section.settings.heading, 문구는 notice_message입니다.

힌트 2 — 조건의 범위

제목 조건문 안에 문구까지 넣지 마세요. 제목만 비었을 때 문구는 남아야 합니다.

힌트 3 — 텍스트 처리

제목과 문구 모두 일반 텍스트이므로 출력 시 escape를 적용하세요.

Step 5. 안내 영역 전체의 출력 조건 정하기

작성 위치: Step 4의 <section> 바깥. 여는 태그부터 닫는 태그까지 조건문으로 감싼다.

과제: 표시 여부가 켜져 있고 최종 안내 문구가 있을 때만 영역을 출력한다.

힌트 — 표시 여부만 검사하면 부족한 이유 열기 닫기

힌트 1 — 두 조건

표시 여부가 켜져 있는지와 최종 문구가 있는지를 함께 검사하세요.

힌트 2 — 검사 시점

상품 메타필드 하나가 아닌 기본 문구 대체까지 끝난 notice_message를 검사하세요.

힌트 3 — 감쌀 범위

제목·문구·배경 영역을 모두 숨기려면 section 전체를 조건문으로 감싸야 합니다.

Step 6. 설정한 배경색을 해당 섹션에만 적용하기

작성 위치: HTML 위의 {% style %} 영역과 HTML의 <section> 여는 태그.

과제: ShippingNotice- 뒤에 section.id를 붙인 id를 만들고, 같은 id를 CSS 선택자에서 사용해 배경색을 적용한다.

힌트 — background_color와 background-color의 차이 열기 닫기

힌트 1 — 속성 이름

설정 ID background_color와 CSS 속성 background-color를 구분하세요.

힌트 2 — 값 출력

설정값을 출력 괄호로 넣으세요. schema 타입이 color라고 color 필터를 붙이지 않습니다.

힌트 3 — 섹션별 분리

section.id를 포함한 ID를 HTML과 CSS 선택자 양쪽에 사용하세요. CSS 선언 끝에는 세미콜론을 붙입니다.

Step 7. 편집기에서 추가하고 요구사항 검증하기

작성 위치: {% schema %}의 settings 배열 다음에 presets를 추가한다. 이후 테마 편집기의 상품 템플릿과 관리자 상품 메타필드 화면에서 확인한다.

과제: 편집기에서 ‘배송 안내’ 섹션을 추가할 수 있게 만든다. 상품 메타필드를 정의하고 값을 입력한 뒤 아래 검증표를 순서대로 확인한다.

힌트 — presets 배열과 blocks의 필요 여부 열기 닫기

힌트 1 — presets 구조

presets는 객체를 담는 배열입니다. settings와 presets 사이의 쉼표도 확인하세요.

힌트 2 — 불필요한 블록

반복 항목을 만들지 않으므로 빈 blocks를 추가할 필요는 없습니다.

힌트 3 — 상품 데이터

미리 보는 상품과 shipping_notice 값을 입력한 상품을 일치시키세요.

힌트 4 — 검증 순서

기본 문구 → 상품별 문구 우선 → 숨김 조건 → 배경색 분리 → 모바일 순서로 아래 확인표를 점검하세요.

기준 답안 열기 닫기

아래는 요구사항을 충족하는 기준 구현이다. 같은 요구사항을 만족하는 다른 구현도 정답으로 인정한다.

{% assign notice_message = product.metafields.custom.shipping_notice.value %}
{% if notice_message == blank %}
  {% assign notice_message = section.settings.fallback_message %}
{% endif %}

{% style %}
  #ShippingNotice-{{ section.id }} {
    background-color: {{ section.settings.background_color }};
  }
{% endstyle %}

{% if section.settings.show_notice == true and notice_message != blank %}
  <section id="ShippingNotice-{{ section.id }}">
    {% if section.settings.heading != blank %}
      <h2>{{ section.settings.heading | escape }}</h2>
    {% endif %}
    <p>{{ notice_message | escape }}</p>
  </section>
{% endif %}

{% schema %}
{
  "name": "배송 안내",
  "settings": [
    {
      "id": "show_notice",
      "type": "checkbox",
      "label": "표시 여부",
      "default": true
    },
    {
      "id": "heading",
      "type": "text",
      "label": "제목",
      "default": "배송 안내"
    },
    {
      "id": "fallback_message",
      "type": "text",
      "label": "기본 안내 문구"
    },
    {
      "id": "background_color",
      "type": "color",
      "label": "배경색",
      "default": "#ffffff"
    }
  ],
  "presets": [
    {
      "name": "배송 안내"
    }
  ]
}
{% endschema %}

표시를 끄면 작성한 section은 생성하지 않는다. Shopify가 섹션 바깥에 자동 생성하는 래퍼까지 없어진다는 뜻은 아니다. 최종 문구가 결정된 뒤에 출력 조건을 검사해야 기본 문구 대체와 숨김이 함께 동작한다.

요구사항별 확인 기준

조건 기대 결과
상품별 문구 없음, 기본 문구 있음 기본 문구 표시
상품별 문구와 기본 문구 모두 있음 상품별 문구 우선
표시 여부 끔 제목·문구·배경 영역 숨김
제목만 비움 문구만 표시
표시 켬, 제목 있음, 두 문구 모두 없음 안내 영역 숨김
같은 템플릿에 두 인스턴스, 서로 다른 배경색 색상이 각각 유지
모바일의 긴 안내 문구 영역 안에서 줄바꿈

공식 참고 자료

공식 테마의 main 브랜치는 변할 수 있다. 이 가이드의 실습은 현재 저장소 파일을 먼저 기준으로 삼고, 다른 테마에 적용할 때는 실제 구조를 다시 확인한다.