Shopify 테마 구조 파악하기

퍼블리싱을 맡고 처음 연 Shopify 테마 저장소 - 디렉토리 구조부터 화면에서 파일을 역추적하는 방법까지
calendar icon

2026년 9월 7일

조회 21

tag icon

Shopify 스토어의 퍼블리싱을 맡게 됐다. 테마를 pull 받고 에디터로 열었는데, 첫인상은 솔직히 막막했다.

디렉토리는 일곱 개뿐인데 그 안이 만만치 않았다. sections 에 52개, snippets 에 60개, assets 에 86개. 파일 이름만 봐서는 뭐가 페이지고 뭐가 컴포넌트인지 감이 안 왔다.

더 헷갈렸던 건 .liquid 와 .json 이 섞여 있다는 점이었다. templates 안은 죄다 .json 인데 정작 마크업은 한 줄도 없었다. 페이지를 만들려면 어디를 건드려야 하는지부터가 막혔다.

"페이지 파일이 어디 있는 거지?"

React를 쓰던 감각으로는 잘 안 잡혔다. 그래서 무작정 파일을 고치기 전에 구조부터 정리하기로 했다. 이 글은 그 과정에서 정리한 것들이다.


조립도와 부품

가장 먼저 풀린 매듭이 이거였다. templates 는 페이지가 아니라 페이지의 조립도다.

templates/index.json 을 열어보면 이렇게 생겼다.

{
  "sections": {
    "featured_collection": {
      "type": "featured-collection",
      "settings": { "columns_desktop": 3, "products_to_show": 10 }
    }
  },
  "order": ["...", "featured_collection", "..."]
}

마크업이 없다. 어떤 부품을 어떤 순서로, 어떤 설정값으로 쓸지만 적혀 있다. 실제 HTML은 type 이 가리키는 sections/featured-collection.liquid 에 있다.

그러니까 sections 디렉토리는 페이지가 아니라 부품 창고다. 52개의 부품이 쌓여 있고, 조립도가 그중 몇 개를 꺼내 쓴다.

파일정체마크업
templates/*.json페이지 조립도없음
sections/*.liquid부품있음
snippets/*.liquid부품 안의 조각있음

.json 은 데이터고 .liquid 는 코드다

이 경계를 잡고 나니 나머지가 따라왔다.

.json 조립도는 관리자 테마 에디터가 자동으로 쓴다. 운영자가 섹션을 드래그해서 순서를 바꾸면 그 결과가 이 파일에 저장되는 것이다. 실제로 파일 맨 위에 이런 주석이 붙어 있다.

IMPORTANT: The contents of this file are auto-generated.

그래서 .json 은 손으로 편집하지 않는 게 원칙이다. 관리자에서 한 번 저장하면 덮어써진다.

퍼블리셔가 만드는 건 .liquid 부품과 .css 다. 배치는 관리자에서 클릭으로 한다.

처음엔 번거로워 보였다. 값을 코드에 박으면 끝날 일을 굳이 JSON으로 빼놓은 셈이니까. 그런데 이 구조가 주는 게 두 가지 있었다.

첫째, 운영자가 문구나 이미지를 바꾸는 데 개발자가 필요 없다. 값이 코드가 아니라 스토어 데이터에 있으니 관리자에서 저장하면 그대로 반영된다. 커밋도 배포도 없다. 값을 마크업에 하드코딩하는 순간 그 한 글자를 고치는 데 개발자가 불려간다.

둘째, 부품 하나로 인스턴스를 여러 개 만들 수 있다. featured-collection.liquid 는 파일이 하나뿐인데, 홈에서는 3열로 추천 상품을 보여주고 다른 페이지에서는 다른 컬렉션을 다른 열 수로 보여줄 수 있다. 설정값이 파일이 아니라 조립도에 붙어 있기 때문이다. 값을 코드에 박았다면 페이지 수만큼 파일을 복사해야 했을 것이다.

결국 시안을 마크업으로 옮기는 일과 콘텐츠를 채우는 일을 분리하려고 이렇게 되어 있었다.


헤더와 푸터는 템플릿에 없다

한참 찾았다. templates/index.json 에 헤더가 없어서 홈 화면의 헤더를 어디서 고치는지 몰랐다.

답은 layout/theme.liquid 에 있었다.

{% sections 'header-group' %}

<main id="MainContent">
  {{ content_for_layout }}
</main>

{% sections 'footer-group' %}

세 덩어리다. 헤더와 푸터는 모든 페이지 공통이라 레이아웃이 직접 부르고, 가운데 content_for_layout 만 URL에 따라 바뀐다.

React의 root layout 과 구조가 거의 같다.

<Header />
<main>{children}</main>
<Footer />

{{ content_for_layout }} 이 {children} 이고, URL 매칭으로 자동 주입되는 것까지 똑같다. /collections/xxx 로 들어오면 templates/collection.json 이 그 자리에 들어온다.

다른 점은 하나였다. React는 <Header /> 안의 구조가 코드에 박혀 있지만, Shopify는 그 자리에 JSON 조립도를 끼워넣는다. sections/header-group.json 이 그것이다. 운영자가 공지 바를 켜고 끄면 이 JSON이 바뀐다.

이름이 group인데 위치가 sections 라 헷갈렸는데, Shopify 규칙상 섹션 그룹 파일은 sections 에 둬야 한다.


section 과 snippet 을 가르는 건 크기가 아니다

처음엔 "section 은 큰 컴포넌트, snippet 은 작은 컴포넌트" 로 이해했다. 틀렸다.

줄 수를 세어봤다.

파일종류줄 수
snippets/icon-cart.liquidsnippet10
sections/announcement-bar.liquidsection105
snippets/card-product.liquidsnippet382
sections/main-product.liquidsection2081

상품 카드 snippet이 공지 바 section 보다 세 배 넘게 크다. 크기는 기준이 아니었다.

진짜 기준은 {% schema %} 블록이 있느냐였다. 그게 있으면 관리자 에디터에 노출되고, 없으면 코드에서만 부를 수 있다.

sectionsnippet
{% schema %}있다없다
에디터 노출드래그로 추가·삭제안 됨
설정값 저장templates/*.json없음

즉 section 은 "재사용 단위" 가 아니라 "운영자에게 권한을 넘기는 단위" 다. 새로 만들 때 어느 쪽으로 뺄지 고민되면 "운영자가 이걸 스스로 바꿔야 하나" 로 판단하면 된다.

snippet 은 바깥 변수를 못 본다

render 로 snippet 을 부르는데, 인자 목록이 유난히 길어서 이유가 궁금했다.

{% render 'card-product',
  card_product: product,
  media_aspect_ratio: section.settings.image_ratio,
  show_vendor: section.settings.show_vendor,
  section_id: section.id
%}

render 안은 스코프가 격리되기 때문이었다. 바깥의 product 도 section.settings 도 안 보인다. 그래서 필요한 걸 전부 명시적으로 넘겨야 한다. React 의 props 와 같은 개념이고, 실제로 card-product.liquid 맨 위에는 받는 props 목록이 주석으로 적혀 있다.

다만 검증이 전혀 없다. 인자 이름에 오타를 내도 에러가 안 나고 그냥 nil 이 된다. 화면이 안 나오는데 에러도 없으면 먼저 인자 이름 오타부터 의심하는 게 맞다.


화면에서 파일을 역추적하기

구조를 알아도 "지금 보는 이 영역이 어느 파일이냐" 는 별개 문제였다. 여기서 제일 많이 헤맸다.

shopify-section id 를 타고 올라간다

모든 섹션은 렌더링될 때 이런 래퍼로 감싸인다.

<div id="shopify-section-template--18309637636369__featured_collection">

__ 뒤가 조립도 JSON의 키다. templates/index.json 에서 featured_collection 을 찾으면 "type": "featured-collection" 이 나오고, 그게 곧 파일명이다.

DevTools 에서 요소를 찍고 id 가 shopify-section 으로 시작하는 조상까지 올라가면 된다. 이 방법을 알고 나서 탐색 시간이 확 줄었다.

모든 id 가 Shopify 가 만든 건 아니다

id="ProductGridContainer" 를 보고 같은 이름의 파일을 찾았는데 없었다. 한참 뒤졌다.

"파일명이 아니라 그냥 개발자가 붙인 id 였다."

grep 해보니 두 군데에 하드코딩되어 있었다. 컬렉션 페이지와 검색 결과 페이지다.

sections/main-collection-product-grid.liquid:120
sections/main-search.liquid:196

그리고 이 id 는 장식이 아니었다. assets/facets.js 가 필터를 적용할 때 이 컨테이너의 innerHTML 만 통째로 교체한다. 안쪽의 id="product-grid" 에서는 data-id 를 읽어서 어느 섹션을 다시 렌더링할지 결정한다.

section: document.getElementById('product-grid').dataset.id,

디자인이 급하다고 이 id 나 data-id 를 지웠으면 필터링이 조용히 깨졌을 것이다. 마크업을 건드리기 전에 JS 에서 grep 해보는 습관이 필요했다.

덤으로 알게 된 것도 있다. 이 컨테이너 안은 통째로 교체되니까, 초기 로드 때 카드에 이벤트를 붙여두면 필터를 거는 순간 전부 날아간다.

import 된 CSS 가 그 파일의 전부가 아니다

card-product.liquid 맨 위에 이렇게 적혀 있었다.

{{ 'component-rating.css' | asset_url | stylesheet_tag }}

당연히 카드 스타일이 여기 있는 줄 알고 열었는데 없었다. 이름 그대로 별점용 CSS 였다.

카드 스타일의 본체는 assets/component-card.css 에 있었고, 이 파일은 snippet 이 아니라 snippet 을 부르는 섹션들이 각자 로드하고 있었다. grep 해보니 10개 섹션이 같은 줄을 갖고 있었다.

sections/main-collection-product-grid.liquid
sections/featured-collection.liquid
sections/main-search.liquid
sections/related-products.liquid
...

snippet 은 여러 섹션에서 재사용되니 공통 CSS 는 섹션 쪽에 두는 구조였다. 그리고 이게 CSS 를 잘게 쪼개는 이유이기도 했다. 그 섹션이 없는 페이지에는 CSS 도 안 실린다.

결국 CSS 를 찾을 때는 파일 상단의 import 를 믿지 말고 클래스명으로 grep 하는 게 확실했다.

grep -rn "\.card__heading" assets/

이렇게 해보니 component-card.css 말고 component-article-card.css 에도 있었다.

grep 은 같이 고칠 파일을 알려주는 게 아니다

처음엔 "두 파일을 같이 고쳐야 하나" 싶었다. 그런데 열어보니 아니었다.

/* component-card.css */
.card__heading { margin-top: 0; margin-bottom: 0; font-size: ...; }

/* component-article-card.css */
.article-card .card__heading { margin-bottom: 0.6rem; }

두 규칙이 건드리는 속성이 다르다. 뒤쪽은 margin-bottom 하나만 덮어쓴다. 그러니 내가 component-card.css 에 font-style 을 추가하면, 명시도가 더 높은 .article-card .card__heading 이 있어도 font-style 은 덮이지 않고 그대로 블로그 카드까지 내려간다.

CSS 는 규칙 단위가 아니라 속성 단위로 이긴다. 같은 속성을 더 높은 명시도로 이미 쓰고 있을 때만 두 곳을 함께 봐야 하고, 그 경우는 DevTools 에서 취소선으로 바로 보인다.

그러니까 grep 이 알려준 건 "같이 고칠 파일" 이 아니라 "내 변경이 어디까지 번지는가" 였다. 상품 카드 제목을 손보려던 건데 블로그 카드와 컬렉션 카드까지 같이 바뀐다는 뜻이다.

"고쳐야 할 곳을 찾는 줄 알았는데, 안 고쳐야 할 곳을 찾은 거였다."

범위를 좁히는 방법은 마크업에 이미 준비되어 있었다.

<div class="card-wrapper product-card-wrapper ...">      <!-- card-product.liquid -->
<div class="card-wrapper collection-card-wrapper ...">   <!-- card-collection.liquid -->

카드 종류마다 구분용 클래스가 붙어 있다. .product-card-wrapper .card__heading 처럼 앞에 하나만 얹으면 상품 카드에만 적용된다. 전부 바꿀 생각이면 그냥 두면 되고, 시안이 상품 카드만 가리키면 좁혀야 한다. grep 은 그 판단을 하라고 알려주는 것이지, 파일을 더 고치라고 알려주는 게 아니었다.


컬럼 수는 CSS 에 없었다

상품 그리드가 어디서는 2열, 어디서는 3열로 나왔다. CSS 를 뒤졌는데 답이 없었다.

마크업을 보니 클래스명 자체가 설정값으로 조립되고 있었다.

class="grid product-grid
       grid--{{ section.settings.columns_mobile }}-col-tablet-down
       grid--{{ section.settings.columns_desktop }}-col-desktop"

columns_desktop 이 3이면 grid--3-col-desktop 이 붙는 식이다. 그리고 그 값은 관리자에서 슬라이더로 조절하는 것이라, CSS 를 고칠 문제가 아니라 설정값 문제였다.

페이지마다 다른 이유도 여기 있었다. 컬럼 수는 전역 설정이 아니라 섹션 인스턴스별 설정이다. 홈의 추천 상품은 templates/index.json 에 "columns_desktop": 3 으로 저장되어 있고, 컬렉션 페이지는 templates/collection.json 에 따로 저장된다.

브레이크포인트도 같이 정리했다. assets/base.css 를 보니 grid--*-col-desktop 규칙 전체가 @media (min-width: 990px) 안에 들어 있었다.

화면 폭적용
989px 이하columns_mobile (1 또는 2)
990px 이상columns_desktop (1~5)

태블릿 구간에 별도 열 수가 없다. 그래서 폭을 줄이면 990px 을 지나는 순간 열 수가 한 번에 떨어진다.

이제 그리드가 이상하면 DevTools 에서 <ul> 의 class 속성부터 읽는다. grid--3-col-desktop 인지 grid--4-col-desktop 인지가 거기 다 나와 있다.


schema 가 결국 핵심이었다

구조를 다 파악하고 나니 남는 건 {% schema %} 였다. 섹션 파일 맨 아래에 붙는 JSON 블록인데, 이게 관리자 화면을 만든다.

{
  "name": "Lookbook",
  "settings": [
    { "type": "checkbox", "id": "full_width", "default": true, "label": "Full width" }
  ],
  "blocks": [ { "type": "heading", "name": "Heading", "limit": 3, "settings": [] } ],
  "presets": [ { "name": "Lookbook" } ]
}

settings 는 섹션 전체 설정이고, blocks 는 운영자가 원하는 만큼 추가·삭제·드래그할 수 있는 자식이다. 마크업 쪽은 for 와 case 로 받는다.

주의할 지점을 몇 개 적어둔다.

  • presets 가 없으면 에디터의 섹션 추가 목록에 아예 안 뜬다. 새 섹션이 관리자에서 안 보이면 십중팔구 이거다.
  • {{ block.shopify_attributes }} 를 빠뜨리면 에디터에서 블록을 클릭해도 해당 요소로 스크롤·하이라이트가 안 된다.
  • id 는 스토어에 반영된 뒤에 바꾸면 안 된다. 이미 저장된 값이 유실된다.
  • JSON 이라 후행 콤마 하나에 섹션 전체가 안 뜬다.

시안대로 픽셀을 맞추는 것만 생각하면 이 블록을 대충 쓰기 쉽다. 그런데 여기를 성실하게 안 쓰면 운영자가 문구 하나 바꿀 때마다 개발자를 찾게 된다. 텍스트와 이미지와 링크는 하드코딩 대신 section.settings 로 빼두는 것이, 다른 환경에서 퍼블리싱하던 것과 가장 다른 지점이었다.


결과

정리하고 나니 머릿속 지도가 이렇게 남았다.

theme.liquid ─┬─ header-group.json ─┐
              ├─ [URL별 template]  ─┼→ sections/*.liquid → snippets/*.liquid
              └─ footer-group.json ─┘
                  조립도(.json)         컴포넌트(.liquid)    하위 조각(.liquid)
                  관리자 영역            퍼블리셔 영역         퍼블리셔 영역

그리고 작업할 때 쓰는 순서도 정해졌다.

  1. DevTools 에서 shopify-section-* 조상을 찾아 섹션 파일을 특정한다
  2. 클래스명으로 assets 를 grep 해서 CSS 사용처를 전부 확인한다
  3. 고치기 전에 그 클래스나 id 를 JS 에서도 grep 한다
  4. 운영자가 바꿔야 할 값이면 CSS 가 아니라 schema 로 뺀다

파일을 열기 전에 grep 을 먼저 돌리는 것으로 바뀌었다는 게 제일 큰 변화다.

느낀점

  • 파일명과 화면의 요소는 생각보다 안 맞는다. ProductGridContainer 를 파일명으로 착각해서 한참 뒤졌다. 이름으로 추측하는 대신 grep 을 먼저 돌리는 게 항상 빨랐다.
  • import 문은 그 파일 스타일의 전부가 아니다. snippet 상단의 CSS import 를 믿고 들어갔다가 헛걸음했다. 재사용되는 조각일수록 스타일은 부르는 쪽에 흩어져 있다.
  • grep 은 고칠 곳이 아니라 번지는 범위를 알려준다. 같은 클래스가 여러 파일에 있다고 다 고쳐야 하는 게 아니었다. 정말 봐야 할 건 이 한 줄이 어느 화면까지 영향을 주느냐였다.
  • CSS 로 해결할 문제가 아닌 것들이 있다. 그리드 컬럼 수를 CSS 에서 찾느라 시간을 썼는데, 답은 관리자 설정값이었다. 클래스명이 동적으로 조립되는 구조를 먼저 봤어야 했다.
  • schema 는 부가 기능이 아니라 설계의 일부다. 이걸 안 쓰면 겉보기엔 똑같은 결과물이 나오지만, 운영 단계에서 개발자가 계속 불려간다.

아직 실제 시안을 붙여보지는 않았다. 구조를 파악하는 데만 시간을 썼는데, 처음에 무작정 파일을 고치기 시작했으면 훨씬 오래 걸렸을 것 같다. 남의 코드베이스에 들어갈 때는 손을 대기 전에 지도를 그리는 게 결국 빠른 길이었다.

좋아요 —
hamburger icon

무링의 개발 블로그

FDE를 향하는 개발자


댓글

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

로그인

연관된 글