
이제 본격적으로 쇼피파이를 커스터마이징 해봅시다!
3. 메인 페이지 커스터마이징
앞에서 프레임워크 선택까지 마쳤다면, 이제 실제로 코드를 Shopify 테마에 올릴 수 있는 형태로 묶는 작업이 남아 있습니다. 이 단계를 번들링이라고 하며, 여기서는 Vite를 사용합니다. 예시 파일 이름은 Svelte 를 기준으로 작성되었지만, Vite 를 사용하는 Vue, React 에서도 동일하게 적용할 수 있습니다
핵심은 단 하나의 JS 파일(svelte-main.js)을 만들어 Shopify 테마에서 <script src="svelte-main.js"> 한 줄로 모든 기능이 동작하게 만드는 것입니다.
Shopify 테마에서 필요한 것
Shopify 테마 쪽에 진입 파일(theme.liquid)의 body 안에 다음과 같이 svelte-main.js를 주입하는 코드를 작성합니다.
<!-- theme.liquid -->
{% if template.name == 'index' %}
<div id="svelte-main-app"></div>
<script src="{{ 'svelte-main.js' | asset_url }}" defer="defer"></script>
{% else %}
<!-- 기존 쇼피파이 페이지 코드 -->
{% endif %}
여기서 {% if template.name == 'index' %}는 메인 페이지를 구분하는 조건문입니다. svelte-main.js 가 로드될 때 기존 쇼피파이 페이지가 로드되지 않도록 기존 코드를 {% else %} 문으로 감싸 줘야 합니다. 다른 페이지는 기존 테마 그대로 나와야 하니까 지우면 안 됩니다!
스크립트 실행 흐름
번들이 로드된 뒤 내부적으로는 다음 순서로 실행됩니다.
스크립트 로드
↓
컴포넌트 로드
↓
Storefront API 호출 → 상품 데이터 fetch
↓
div#svelte-main-app에 앱 마운트
데이터 연동 (Storefront API)
Storefront API 클라이언트를 생성하는 코드를 보면 런타임 값과 빌드타임 값이 어떻게 섞여 있는지 한눈에 볼 수 있습니다.
//./src/shopify-entry.ts
declare const __STOREFRONT_TOKEN__: string; // 빌드 시 치환될 상수 선언
const QUERY = `
products(first: 20) {
edges {
node {
handle
title
description
tags
collections(first: 5) {
edges { node { handle } }
}
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
featuredImage { url altText }
}
}
}
`;
async function fetchProducts(language: string) {
const client = createStorefrontApiClient({
storeDomain: 'mystore.myshopify.com',
apiVersion: '2026-04',
publicAccessToken: __STOREFRONT_TOKEN__, // 빌드 시 문자열로 치환됨
});
const { data, errors } = await client.request(QUERY);
// ...
}
__STOREFRONT_TOKEN__은 런타임 변수가 아닙니다. Vite가 빌드할 때 소스 코드 전체에서 이 식별자를 찾아 실제 토큰 문자열로 교체하므로, 번들 결과물 안에는 이미 토큰 값이 문자열로 박혀 있습니다. Vite 설정은 아래에서 자세히 설명하겠습니다.
이 방식이 가능한 이유는 Storefront API 토큰이 Public 토큰이기 때문입니다. Admin API 토큰과 달리 클라이언트에 노출돼도 무방한 값이라 빌드 결과물에 포함시켜도 안전합니다.
언어·국가 처리
다국어 지원 쇼핑몰이라면 번들 하나로 여러 언어 페이지를 제공해야 합니다. 핵심은 쇼피파이 내부 변수를 커스텀 코드가 읽을 수 있도록 window.SHOPIFY_STORE_DATA에 전달해 주고, 이 변수를 커스텀 코드가 읽어와 Storefront API 요청에 활용하는 것입니다.
먼저 다음과 같이 svelte-main.js를 주입하는 코드 상단에 window.SHOPIFY_STORE_DATA 설정 스크립트를 삽입합니다.
<!-- theme.liquid -->
<script>
window.SHOPIFY_STORE_DATA = {
country: "{{ localization.country.iso_code }}",
language: "{{ localization.language.iso_code }}"
};
</script>
그런 다음, 커스텀 코드 내에서 각 언어별 페이지를 로드하는 로직을 작성해 주면 됩니다.
//./src/shopify-entry.ts
const SUPPORTED_LANG = ['EN', 'ES', 'KR', 'PT'] as const;
async function resolveApp(language: string) {
if (language === 'ES') return (await import('./lib/components/es/ShopifyApp.svelte')).default;
if (language === 'PT') return (await import('./lib/components/pt/ShopifyApp.svelte')).default;
if (language === 'MS') return (await import('./lib/components/ms/ShopifyApp.svelte')).default;
return (await import('./lib/components/en/ShopifyApp.svelte')).default;
}
const language = (() => {
const lang = window.SHOPIFY_STORE_DATA?.language ?? 'EN';
return SUPPORTED_LANG.includes(lang as any) ? lang : 'EN';
})();
resolveApp(language).then(App).fetchData().then(mount)
각 컴포넌트 내의 링크들은 언어 정보가 포함된 URL을 사용하여, 링크 이동 시에 언어 설정이 깨지지 않도록 주의해주세요. 쇼피파이는 (기본 언어가 아닌) 언어 코드를 첫 번째 URL 파라미터로 씁니다.
https://${SHOP_DOMAIN}/${language.toLowerCase()}/…
- 언어별 제품 정보 가져오기
언어, 국가별로 설정된 제품 정보를 불러오려면 데이터 연동 코드의 쿼리에 언어, 국가 코드를 추가해 주면 됩니다.
const QUERY = `
query Products($country: CountryCode, $language: LanguageCode) @inContext(country: $country, language: $language) {
products(first: 20) {
edges {
node {
handle
title
description
tags
collections(first: 5) {
edges { node { handle } }
}
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
featuredImage { url altText }
}
}
}
}
`;
async function fetchProducts(language: string) {
// ...
const { data, errors } = await client.request(QUERY, { variables: { country, language } });
// ...
}
Vite 설정 핵심 포인트
이 모든 것을 가능하게 하는 Vite 설정 파일의 핵심을 살펴보겠습니다.
// vite.shopify.config.ts
build: {
lib: {
entry: path.resolve('./src/shopify-entry.ts'),
name: 'MyApp',
fileName: () => 'svelte-main.js',
formats: ['iife'],
},
cssCodeSplit: false,
},
define: {
__STOREFRONT_TOKEN__: JSON.stringify(env.STOREFRONT_PUBLIC_TOKEN ?? ''),
},
1. lib 모드: HTML 없는 라이브러리 빌드
Vite의 일반 빌드는 index.html을 진입점으로 삼는 웹앱 빌드입니다. 반면 lib 모드는 HTML 없이 JS 파일 하나를 출력하는 라이브러리 빌드입니다.
2. formats: ['iife']: 즉시 실행 함수로 출력
일반 Vite 빌드는 ES 모듈(import/export) 형식으로 출력합니다. 이 경우 브라우저에서 <script type="module">로 로드해야 하고 CORS 제약도 따라옵니다.
Shopify 테마의 일반 <script> 태그에서 바로 실행하려면 모듈 문법 없이 로드 즉시 실행되는 형태가 필요합니다. IIFE(Immediately Invoked Function Expression)가 바로 그 형태입니다.
프레임워크 런타임, API 클라이언트, 각 컴포넌트 등 모든 의존성이 이 하나의 함수 안에 인라인으로 번들됩니다.
3. cssCodeSplit: false + CSS 주입 플러그인: CSS도 JS 안으로
Vite는 기본적으로 CSS를 별도 .css 파일로 분리합니다. Shopify 테마에서 JS와 CSS 파일을 따로 관리하면 번거롭습니다.
cssCodeSplit: false 설정과 CSS를 JS 안에 주입하는 플러그인(vite-plugin-css-injected-by-js 등)을 함께 사용하면, CSS가 JS 번들 안에 포함되어 스크립트 로드 시점에 <head>에 <style> 태그를 자동으로 삽입합니다.
결과적으로 Shopify 테마가 관리해야 할 파일은 svelte-main.js 단 하나입니다.
4. define: 빌드 타임 상수 주입
Vite가 빌드할 때 소스 코드 전체에서 __STOREFRONT_TOKEN__이라는 식별자를 찾아 실제 토큰 문자열로 치환합니다. 런타임에 환경 변수를 읽는 것이 아니라, 빌드 결과물 안에 문자열이 직접 박혀 있는 방식입니다.
4. 커스텀 페이지 커스터마이징
커스텀 페이지는 메인 페이지와 거의 동일한 방식으로 추가할 수 있습니다. 아래와 같이 하나의 변경점, 하나의 추가 설정만 고려하면 됩니다.
- 코드 주입 조건문 변경
<!-- theme.liquid -->
{% if page.handle == 'myPage' %}
<div id="svelte-my-page-app"></div>
<script src="{{ 'svelte-my-page.js' | asset_url }}" defer="defer"></script>
{% else %}
<!-- 기존 쇼피파이 페이지 코드 -->
{% endif %}
theme.liquid 파일에 커스텀 페이지 코드가 주입될 경로를 {% if page.handle == 'myPage' %} 형태로 지정해야 합니다. 여기서는 커스텀 페이지 코드 이름을 svelte-my-page.js 변경했습니다.
- 페이지 활성화

커스텀 페이지가 들어갈 URL을 활성화시켜 주어야 합니다. 쇼피파이 대시보드의 페이지 메뉴에 해당 URL을 추가합니다. 만약 페이지 이름이 myPage라면 제목에 myPage를 입력하고 저장하고 공개합니다. 콘텐츠는 실제로 커스텀 페이지로 덮어씌워지므로 중요하지 않습니다.
설정이 완료되었다면 이제 https://${SHOP_DOMAIN}/pages/myPage 로 접속하여 커스텀 페이지를 확인할 수 있습니다.
4. 상품 소개 커스터마이징
지금까지 다룬 메인/커스텀 페이지는 특정 URL을 JS 번들로 덮어씌우는 방식이었습니다. 상품 소개 페이지 커스터마이징은 그 방식이 다릅니다. Shopify 어드민의 각 상품 편집 화면에 HTML을 직접 붙여넣는 방식을 이용합니다. 이 때, 붙여넣는 코드는 자바스크립트 없이 순수 HTML만 사용해야 합니다.
순수 HTML이어야 하는 이유
Shopify 상품 설명 필드는 HTML 에디터를 제공하지만, <script> 태그는 보안상 허용되지 않습니다. 따라서 상호작용이 필요한 UI는 CSS만으로 구현해야 하며, 데이터를 동적으로 불러오는 로직도 넣을 수 없습니다.
모던 프레임워크로 관리하면 편리한 점
제약이 많아 보이지만, 커스텀 페이지와 같은 프레임워크로, 같은 프로젝트로 관리한다면 단점을 보완할 수 있습니다.
1. 컴포넌트를 그대로 복사해서 쓸 수 있다
Svelte나 Vue의 컴포넌트 파일은 <template> 또는 마크업 블록에 순수 HTML을 그대로 작성합니다. 상품 소개 HTML도 <html>, <head> 없이 <body> 안의 마크업만 필요하기 때문에, 컴포넌트 파일의 마크업 부분을 그대로 복사해 Shopify 상품 설명 필드에 붙여넣으면 됩니다. 별도의 변환 작업이 필요하지 않습니다.
2. 개발 환경에서 모든 상품 소개 페이지를 쉽게 조회할 수 있다
로컬 개발 서버에서 URL만 바꿔가며 상품별 소개 페이지를 전부 확인할 수 있습니다. 커스텀 페이지와 상품 소개 페이지가 같은 프로젝트 안에 있으니, 에디터 하나로 전체 Shopify 커스터마이징을 관리할 수 있습니다.
5. 주의사항
스타일 충돌
쇼피파이 테마에는 기본 스타일시트가 있고 theme.liquid에서 주입됩니다. 제가 쓰는 테마에서는 base.css 이름의 CSS 파일이 theme.liquid의 {%- render 'stylesheets' -%}로 주입되고 있습니다.
문제는 이 파일에 정의된 스타일이 우리가 주입한 UI와 충돌을 일으킬 수 있다는 점입니다. 개발 환경에서 보던 UI와 코드가 적용된 웹사이트에 보이는 UI가 다르다면 높은 확률로 이것이 원인입니다.
이 문제를 피하려면 다음 방법 중 하나로 조치할 수 있습니다.
- theme.liquid에서 기본 스타일시트가 커스텀 페이지에서 로드되지 않도록 조건문 코드를 추가하기
- 커스텀 UI에 사용된 클래스 이름에 특별한 텍스트를 추가하는 방식으로 충돌을 피하기
마치며
기본 페이지를 커스텀 페이지로 바꾸더라도 페이지가 로드되는 시점과 방식은 기존 테마와 동일하기 때문에 성능과 SEO 손실은 발생하지 않습니다.

이 점수는 저희 고객사 쇼피파이 쇼핑몰에 커스텀 UI를 적용한 뒤 측정한 PageSpeed Insights 점수입니다. 웹 빌더 특유의 느린 성능은 어쩔 수 없는 것 같습니다.
이 글에서 다룬 두 가지 커스터마이징 방식을 정리하면 다음과 같습니다.
| 커스텀 페이지 | 상품 소개 페이지 | |
|---|---|---|
| 적용 방식 | Vite로 번들링 → JS 파일 업로드 | HTML 직접 붙여넣기 |
| 자바스크립트 | 사용 가능 | 사용 불가 |
| Shopify 어드민 위치 | 테마 편집기 | 상품 편집 화면 |
| 관리 단위 | 파일 하나(svelte-main.js) | 상품별 HTML |
방식은 다르지만, 모던 프레임워크와 Vite를 기반으로 한 프로젝트 안에서 두 가지 커스터마이징을 함께 관리할 수 있다는 점이 핵심입니다. AI 코딩 도구의 도움을 받으면 코드 작성 부담도 크게 줄어듭니다.
처음에는 낯설게 느껴질 수 있지만, 한 번 환경을 갖춰 두면 Shopify 스토어를 훨씬 자유롭고 체계적으로 관리할 수 있으니 한 번 시도해보시길 권유드립니다.