Shopify 테마 학습기 2

상품별 데이터 연결에서 장바구니 요청까지 추적한 기록
calendar icon

2026년 10월 20일

조회 0

tag icon

지난 글에서는 상품 가격을 읽고 갱신하는 과정을 따라가며, 운영자가 문구와 배치를 바꿀 수 있는 안내 배너를 만들었다. 설정을 추가하면 편집기에 입력란이 생기고, Liquid로 출력하면 화면에 반영되는 흐름까지 확인했다. 그다음에는 만든 화면이 실제 상품 데이터와 어떻게 연결되는지가 궁금해졌다.

같은 배너라도 상품마다 문구가 달라야 할 수 있고, 특정 상품군에만 다른 구성이 필요할 수도 있다. 옵션을 바꿨을 때 가격이 달라지는 것과 그 옵션이 장바구니에 담기는 것도 같은 문제는 아니었다. 화면이 바뀌었다는 사실만으로 데이터까지 제대로 연결됐다고 판단하지 않기로 했다.

"화면에는 맞게 나오는데, 실제로도 그 상품과 옵션을 사용하고 있을까?"

이번에는 이미지와 링크를 추가한 배너에서 출발해 상품 메타필드, 동적 소스, 별도 템플릿을 연결했다. FAQ를 직접 만든 뒤에는 구매 가능 상태와 장바구니 요청까지 살펴봤다. 실습은 Shopify의 Generated Data Theme 1.0.0에서 진행했으며, 아래 파일명과 JavaScript 흐름은 이 테마의 구현을 기준으로 한다.


설정값이 실제 화면이 되기까지

링크를 넣을 위치부터 구분했다

배너에는 배송 안내와 교환 안내처럼 반복되는 블록이 있었다. 각 제목을 다른 페이지로 연결하려면 링크를 배너 전체 설정에 하나만 둘 수는 없었다. notice 블록의 settings에 url 타입의 link를 추가하고, 값이 있을 때만 제목을 링크로 감쌌다.

설정의 타입과 식별자를 혼동하기도 했다. URL을 입력하는 설정이라는 이유로 값을 block.settings.url에서 찾는 것은 맞지 않았다. 입력 형식은 type으로 정하고, 실제 접근 이름은 id로 정했으므로 읽을 값은 block.settings.link였다. 아무것도 입력하지 않은 경우에는 빈 주소를 가진 링크 대신 일반 제목을 남겼다.

처음에는 파일에 설정을 추가했는데 편집기에 입력란이 보이지 않았다. 개별 블록을 선택한 화면이 맞았고, 로컬 JSON의 위치도 틀리지 않았다. 확인 당시에는 개발 서버가 실행되고 있지 않았으며, 다시 실행하도록 안내받은 뒤 링크가 적용된 것을 확인했다. 코드가 로컬에 저장됐다는 것과 보고 있는 개발 테마에 반영됐다는 것을 따로 확인해야 했다.

링크가 만들어진 뒤에는 색상만 보지 않았다. 부모의 글자 색을 사용하되 밑줄을 남겼고, 키보드로 접근했을 때 외곽선이 보이도록 했다. 링크가 없는 제목은 일반 텍스트로 남는지, 링크가 있는 제목은 Enter로 이동할 수 있는지도 확인했다.

이미지 너비는 CSS만으로 설명할 수 없었다

배너 이미지는 블록마다 넣는 대신 섹션 전체에 하나를 두었다. 최상위 settings에 image_picker를 추가하고, 선택한 이미지에 image_url과 image_tag를 순서대로 적용했다. 이미지가 없을 때는 태그 자체를 만들지 않도록 조건을 붙였다.

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

여기서 실제 화면과 설명이 어긋났다. max-width: 100%만 있으면 작은 원본 이미지가 부모 너비만큼 늘어나지 않는다는 설명을 들었는데, 내가 올린 이미지는 부모가 넓어지면 함께 커졌다. 당시 나는 업로드한 이미지가 225px이라고 보고 있었지만, 원본 파일의 치수를 별도 도구로 검사한 상태는 아니었다. 그래서 이 문제는 원본 크기를 단정하기보다 실제 생성된 태그부터 확인해야 했다.

확인한 미리보기 HTML에는 width="1200" height="1200"이 들어 있었다. CSS의 상한선만 적용된 이미지가 아니라 HTML에도 표시 크기 정보가 있는 상태였다. 이 조건을 빼고 원본 너비와 부모 너비만 비교한 설명은 실제 코드에 맞지 않았다. display: block이 이미지를 늘린다는 해석도 정확하지 않았고, HTML 속성과 계산된 CSS를 함께 봐야 했다.

최종적으로는 최대 너비를 편집기에서 조절할 수 있도록 range 설정을 추가했다. 섹션 ID로 범위를 제한한 CSS 변수에 설정값을 넣고, 이미지에는 부모 너비와 설정값 중 작은 값을 상한선으로 적용했다. 이 방식으로 최대 너비를 400px로 설정한 화면과, 부모 영역이 그보다 좁아졌을 때의 축소를 확인했다.

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

상품과 화면 구성의 연결 범위

상품별 문구는 상품에 저장했다

배너 설정에 고정 문구를 넣으면 그 설정을 쓰는 화면에서는 같은 문구를 읽는다. 하지만 왁스에는 사용 전 안내를, 스노보드에는 사용 후 관리 안내를 표시하고 싶었다. 이 경우에는 문구를 상품에 저장하는 쪽이 맞아서 제품 메타필드를 만들었다.

정의한 식별자는 custom.learning_notice, 타입은 단일 값의 한 줄 텍스트였다. 정의를 만드는 단계와 개별 상품에 값을 입력하는 단계는 별개였다. 먼저 두 상품에 서로 다른 문구를 저장하고, 기존 상품 정보 snippet에서 현재 상품의 메타필드를 읽도록 했다.

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

처음에는 문구가 나오지 않았다. 로컬 코드가 저장됐고 응답 HTML에도 새로 추가한 위치가 있었지만, 그곳은 빈 <p></p>였다. 상품과 저장 상태를 확인한 뒤에는 문구가 표시됐다. 정확히 어떤 조치가 해결 원인이었는지는 확정하지 않았지만, 적어도 화면 배치 문제와 값이 비어 있는 문제를 구분할 근거는 생겼다.

두 상품에서 서로 다른 문구가 나오는 것을 확인한 뒤, 값 없는 상품에서는 안내 태그가 만들어지지 않는지도 봤다. 같은 상품 안의 옵션별 문구를 만든 것은 아니었다. 이번 데이터는 상품에 속했으므로 옵션을 바꾸는 것과 다른 상품으로 이동하는 것은 구분해야 했다.

연결은 특정 배너의 특정 설정에 저장됐다

다음에는 같은 메타필드를 테마 편집기의 동적 소스로 연결했다. 배너의 안내 문구 설정에서 상품별 안내 문구를 선택했지만, Liquid 코드는 여전히 section.settings.message를 출력했다. 직접 메타필드를 읽는 표현식으로 코드를 바꾸지 않아도, 그 설정이 현재 상품의 값을 가져오도록 연결할 수 있었다.

이때 가장 혼동했던 것은 연결의 범위였다. “템플릿에서 한 번 연결한다”는 설명만으로는 새 배너에도 자동으로 연결되는 것처럼 들릴 수 있었다. 실제로 배너를 하나 더 추가하면 그 배너에는 따로 연결해야 했다. 정확한 단위는 배너 섹션 인스턴스의 message 설정이었다.

바꾼 대상확인한 동작 또는 적용 범위
연결해둔 배너에서 미리보기 상품만 변경바뀐 상품의 메타필드 값을 읽음
배너를 새로 추가새 인스턴스의 설정에 별도 연결 필요
해당 상품의 메타필드 값을 비움가져올 문구가 없어짐
공용 섹션 코드 수정그 파일을 사용하는 배너에 영향

템플릿은 배너 구성과 설정이 저장되는 곳이고, 상품은 실제 문구를 가진다. 두 역할을 합쳐서 설명하면 어디까지 바뀌는지 판단하기 어려웠다. 기존 배너를 유지한 채 미리보기 상품만 바꾸니 두 상품의 문구가 각각 나왔고, 배너의 안내 문구에도 빈 값 조건을 추가했다.

미리보기와 실제 배정도 나눴다

특정 상품군에만 다른 구성을 쓰는 연습으로 기본 상품 템플릿을 바탕으로 learning-special을 만들었다. 여기에 특별 상품 안내 배너를 추가하고 기본 템플릿으로 돌아가 비교했다. 새 배너는 별도 템플릿에만 보였으며, 기본 템플릿의 구성은 유지됐다.

다만 상품 관리자의 테마 템플릿 목록에는 기본 템플릿만 보였다. 개발 테마에서 새 템플릿을 만들고 미리보기 상품을 고르는 것과, 관리자에서 상품의 실제 템플릿을 배정하는 것은 달랐다. 이 실습을 위해 테마를 게시하지는 않았고, 개발 테마에서 지정한 상품과 특별 안내 배너가 함께 보이는 것까지만 확인했다.


화면의 동작과 구매 요청

FAQ는 기본 HTML로 만들었다

배너에서는 설정을 하나씩 따라 만들었다면, FAQ에서는 요구사항을 보고 schema와 출력 구조를 작성했다. 섹션 제목은 text, 반복 블록의 질문은 text, 답변은 richtext로 정했다. 섹션·블록의 이름인 name과 입력란의 표시 이름인 label을 바꿔 쓴 부분을 고치면서 구조를 다시 확인했다.

펼침과 접힘은 별도 JavaScript 없이 details와 summary로 구성했다. 질문은 escape 처리하고, 서식이 있는 답변은 HTML이 렌더링되도록 출력했다. 질문이나 답변 중 하나만 입력된 항목은 빈 제목으로 남기지 않고 전체를 숨겼다.

{% 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 %}

키보드로 질문에 접근했을 때 포커스가 보이는지와 Enter 또는 Space로 열고 닫히는지를 확인했다. 블록을 여러 개 추가해 순서를 바꾸고, 모바일 너비에서 긴 질문과 답변이 줄바꿈되는 것도 봤다. 답변에 일부러 링크를 추가하는 검증은 이번에는 생략했다. 기본 기능을 확인한 범위를 전체 접근성 검증으로 확대해서 쓰지는 않기로 했다.

가격 갱신과 구매 상태 갱신을 연결했다

상품 페이지에는 선택한 옵션이 구매 가능한지 알려주는 문구를 추가했다. 처음 출력할 때는 product.selected_or_first_available_variant.available을 읽고, 옵션이 바뀌면 기존 가격 갱신과 같은 응답에서 안내 영역을 가져왔다. 응답 HTML의 요소와 현재 화면의 요소를 각각 찾은 뒤 내부 내용을 교체했다.

ID가 같으면 같은 것을 다시 읽는 것이 아닌지 의문이 생겼다. 하지만 document에서 찾은 요소는 현재 화면에 있고, 파싱한 html에서 찾은 요소는 서버가 새로 보낸 문서에 있었다. 같은 이름표를 가진 별도 요소였으므로 내용은 달라질 수 있었다. 이 테마의 빠른 구매 팝업에서는 ID 중복을 피하려고 접두사를 붙이기 때문에 원래 섹션 ID를 따로 보관하는 코드도 있었다.

구매 버튼은 옵션 변경 직후 먼저 비활성화됐다. 이후 응답 속 버튼에 disabled가 있는지 보고 현재 버튼을 갱신했다. 구매 가능한 옵션에서 불가 옵션으로, 다시 가능한 옵션으로 이동하며 안내와 버튼이 함께 바뀌는 것을 확인했다. 이전 버튼의 상태와 응답 속 버튼의 상태를 나누어 읽으니 흐름이 선명해졌다.

빠르게 옵션을 바꾸는 상황에는 이미 오래된 응답을 거르는 코드가 있었다. 요청 당시 옵션 ID를 저장하고, 응답이 왔을 때 현재 선택과 다르면 그 응답의 화면 반영을 중단했다. 빠른 전환에서 화면이 바뀌는 것은 관찰했지만, 응답 순서를 강제로 뒤집는 테스트까지 한 것은 아니었다.

화면의 선택과 전송할 선택은 별도로 갱신됐다

"가격은 새 옵션으로 바뀌었는데, 장바구니에는 무엇이 들어갈까?"

상품 폼의 숨겨진 입력란에는 선택한 옵션 ID가 들어 있었다. updateVariantInput()이 이 값을 현재 옵션 ID로 바꾸고, 제출할 때 FormData가 전송값을 구성했다. 가격과 안내 문구만 갱신하고 이 입력값을 그대로 두면, 사용자가 보고 있는 옵션과 요청에 담긴 옵션이 달라질 수 있었다.

개발자 도구의 Network에서 장바구니 추가 요청을 직접 열었다. 요청의 id가 주소창 variant 값과 일치하는지 확인하고, 수량을 2로 설정했을 때 quantity가 2로 전송되는 것도 봤다. 그다음 장바구니 안에서 수량을 1로 바꾸자 추가 요청과 다른 변경 요청에 line과 quantity가 함께 들어갔다.

요청에서 확인한 값의미
추가 요청의 id담을 옵션의 고유 ID
추가 요청의 quantity 2해당 옵션 2개 추가
변경 요청의 line 1장바구니의 첫 번째 항목
변경 요청의 quantity 1그 항목의 수량을 1개로 설정
삭제 요청의 quantity 0그 항목의 수량을 0으로 변경

실패 처리에서는 같은 response라는 이름 때문에 다시 구분이 필요했다. JSON으로 파싱한 뒤의 response.status는 HTTP 응답 객체를 그대로 읽는 것이 아니었다. 이 API의 오류 JSON에 들어오는 422 같은 값이 조건문에서 참으로 평가되어 오류 분기로 들어가는 구조였다.

오류 이벤트를 알리는 publish, 문구를 표시하는 함수, 오류 상태를 기록하는 대입, 콜백을 끝내는 return도 역할이 달랐다. this.error = true가 실행을 멈추는 것은 아니었고, 바로 다음 return이 그 콜백의 성공 처리로 내려가지 않게 했다. 이후 finally에서는 로딩을 정리하되 오류 상태일 때 버튼의 aria-disabled를 제거하지 않는 조건이 있었다. 실제 서버 오류를 발생시키는 화면 검증은 이번에는 생략하고 코드 흐름만 확인했다.


결과

이번에는 편집 가능한 섹션을 만드는 데서 한 단계 더 나아갔다. 문구가 저장되는 상품, 데이터를 읽는 설정, 설정을 가진 섹션 인스턴스, 화면을 구성하는 템플릿을 구분했다. 상품 화면에서는 표시된 상태와 실제 요청에 담기는 값까지 연결해 볼 수 있었다.

작업남은 결과
배너 확장블록별 링크, 이미지 선택, 최대 너비 설정과 빈 문구 처리
상품별 문구메타필드 직접 출력과 배너 설정의 동적 소스 연결
별도 구성과 FAQ상품 템플릿 미리보기, 반복 가능한 접힘 항목
옵션·장바구니구매 상태 안내 갱신과 ID·수량 요청 추적
팀 학습 자료문제·힌트·접힌 정답·확인 결과를 담은 README

화면에서는 링크 이동, 이미지 선택과 제거, 최대 너비와 모바일 축소, 상품별 문구, FAQ와 키보드 동작을 확인했다. 구매 가능 상태 전환과 장바구니 요청값도 직접 봤다. 커밋 전에는 변경된 Liquid 파일 3개의 검증, JavaScript 구문, 문서 링크와 공백 검사를 실행해 통과했다. 학습 코드·문서·스크린샷을 묶은 커밋에는 9개 파일이 포함됐다.

확인하지 않은 범위도 남겼다. 개발 테마의 별도 템플릿을 실제 상품에 배정하지 않았고, 서버 오류 화면 검증과 응답 순서 강제 역전 테스트는 하지 않았다. 삭제 요청의 quantity 0은 확인했지만 삭제 후 화면 결과를 별도로 확정해서 기록하지는 않았다. 전체 Theme Check를 이번에 다시 실행한 것은 아니며, 실제 주문이나 운영 배포도 하지 않았다.

학습 기록은 나중에 팀의 기초 가이드로 쓸 수 있도록 정리했다. 각자 자신의 개발 테마에서 따라 하되, 이 글의 파일명과 함수가 모든 테마에 그대로 있다고 가정하지 않도록 했다. 다음 과정은 컬렉션과 검색 결과가 함께 사용하는 상품 카드를 찾고, 공용 코드를 수정했을 때의 영향을 확인하는 것이다.


느낀점

  • 설정의 범위를 정확히 말해야 했다. 템플릿에서 연결한다는 표현만으로는 새 배너에도 적용되는지 알기 어려웠다. 특정 섹션 인스턴스의 특정 설정이라고 구분하니 바뀌는 대상이 명확해졌다.
  • 생성된 HTML도 실제 코드였다. CSS만 읽어서는 이미지가 커지는 이유를 설명할 수 없었다. 필터가 만들어낸 크기 속성을 확인해야 화면과 설명이 맞았다.
  • 보이는 값과 보내는 값은 따로 확인해야 했다. 옵션 가격이 바뀌었다고 장바구니 ID까지 바뀌었다고 단정할 수 없었다. 숨겨진 입력값과 Network 요청을 연결해서 봐야 했다.
  • 검증하지 않은 것은 남겨두는 편이 나았다. 실제 오류 화면을 확인하지 않았는데 오류 처리를 완성했다고 쓰지 않았다. 다음 사람이 기록을 읽고 어디까지 확인됐는지 판단할 수 있어야 했다.

테마 공부의 목표는 설정 항목을 많이 추가하는 데 있지 않았다. 수정 요청을 받았을 때 데이터와 설정 중 어디를 바꾸고, 어떤 화면까지 영향을 주는지 설명하는 쪽에 가까웠다. 작은 실습을 이어갔지만 이번에는 그 경계를 읽는 연습을 했다는 점이 남았다.

다음에는 상품 상세 화면을 벗어나 공용 상품 카드로 범위를 넓혀볼 생각이다. 긴 글을 읽어주셔서 감사하다.

좋아요 —
hamburger icon

무링의 개발 블로그

FDE를 향하는 개발자


댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다.

로그인

연관된 글