> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 커스텀 템플릿

> 직접 만든 JSX로 Aftersell Cart 블록의 렌더링 방식을 재정의하세요: 템플릿이 대체하는 것, 사용 가능한 범위, 스타일 지정 방법, 각 블록의 props 위치.

**커스텀 템플릿**을 사용하면 개별 블록의 렌더링 방식을 재정의할 수 있어요. 블록의 기본 UI 대신, 블록이 평소 사용하는 것과 동일한 데이터를 사용해 여러분의 JSX를 렌더링해요. 자체 블록이 아니라 여러 블록에 걸친 기능이에요: 대부분의 블록이 **Code** 탭에서 이 기능을 제공해요.

이 페이지는 **모든** 블록에 적용되는 내용을 다뤄요. 특정 블록이 전달하는 props는 [해당 블록의 레퍼런스](#props-for-each-block)로 이동하세요.

<div id="custom-template-vs-custom-code-block">
  ## 커스텀 템플릿 vs. 커스텀 코드 블록
</div>

이름은 비슷하지만 하는 일이 달라요:

* **커스텀 템플릿**은 *기존 블록의 렌더링을* 직접 만든 마크업으로 *대체하고*, 해당 블록의 자체 데이터(Header의 제목과 아이템 수, Summary의 총액 등)를 전달해요. 새로운 것을 추가하지 않고 블록 하나의 스타일을 바꿔요.
* **[Custom code](/ko/aftersell/cart/custom-code-blocks)** 블록은 장바구니 어디에나 임의의 HTML 또는 React로 된 *새 블록을 추가해요*.

기본 블록이 거의 맞지만 다른 레이아웃이나 마크업이 필요할 때는 커스텀 템플릿을, 기본 블록이 다루지 않는 것을 추가하고 싶을 때는 Custom code 블록을 사용하세요.

<div id="using-a-custom-template">
  ## 커스텀 템플릿 사용하기
</div>

1. 에디터에서 블록을 선택하고 **Code** 탭을 여세요.
2. 기본 템플릿을 편집하세요. 커스텀 템플릿은 **JSX 전용**이에요(HTML/JSX 선택은 Custom code 블록에만 있어요).
3. **Compile**을 클릭하세요. 컴파일은 타입을 제거하고 JSX를 트랜스파일하므로 **구문(syntax)** 오류를 잡아내요. 타입 오류는 컴파일을 막지 않아요 — 에디터가 입력하는 동안 인라인으로 표시하며, 블록의 props를 자동 완성하는 것과 같은 IntelliSense를 사용해요.
4. 템플릿을 켜서 장바구니가 기본 렌더링 대신 이를 사용하도록 하세요.
5. **Reset to default**는 언제든지 블록의 원래 템플릿을 복원해요.

<div id="writing-a-template-with-ai">
  ## AI로 템플릿 작성하기
</div>

Code 탭에는 **Copy AI prompt** 버튼(✦ 마법봉 아이콘)이 있어요. 클릭하면 AI 채팅 세션(Claude, ChatGPT 등)에 바로 붙여넣을 수 있는 완결된 브리프가 클립보드에 복사돼요.

프롬프트에는 AI가 해당 블록에 유효한 템플릿을 작성하는 데 필요한 모든 것이 포함돼요:

* 컴파일 규칙(단일 표현식, `export default` 없음, import 없음)
* 에디터의 IntelliSense가 표시하는 것과 일치하는, 블록이 받는 정확한 props
* 에디터가 강제하는 잠긴 함수 시그니처
* 블록별 규칙(금액 형식, 연결해야 할 핸들러, 접근성 요구 사항)
* 현재 템플릿을 붙여넣고 원하는 변경 사항을 설명하는 작성란

복사한 후 AI 세션을 열고 프롬프트를 붙여넣은 다음, 하단의 두 빈칸(현재 템플릿과 원하는 변경 사항)을 채우고 전송하세요. AI가 에디터에 다시 붙여넣고 컴파일할 수 있는 완전한 템플릿을 반환해요.

<Tip>
  작성란을 비워 두지 말고 기존 템플릿을 붙여넣으세요. AI가 이를 출발점으로 사용하므로, 이미 적용한 커스터마이징이 기본값으로 대체되지 않고 그대로 유지돼요.
</Tip>

<Note>
  프롬프트는 각 블록에 특화되어 있어요. **Copy AI prompt** 버튼은 커스텀 템플릿을 지원하는 블록에만 표시돼요.
</Note>

<Tip>
  시작점이 되는 기본 템플릿은 **블록의 기본 마크업이 그대로 작동하는 사본**이므로, 빈 페이지가 아니라 항상 올바르게 렌더링되는 참조본을 수정하게 돼요. 그 참조본이 다시 필요하면 언제든 **Reset to default**를 사용하세요.

  항상 바이트 단위로 동일하지는 않아요. Header의 기본 템플릿은 기본 마크업에는 배치 위치가 없는 `logoUrl`도 렌더링하므로, 그 템플릿을 켜는 것이 업로드한 헤더 이미지가 처음 나타나는 방법이에요.
</Tip>

<div id="what-your-template-replaces">
  ## 템플릿이 대체하는 것
</div>

템플릿은 블록의 렌더링을 **전부** 대체해요. JSX 주위에 남는 래퍼가 없으므로, 무언가를 삭제하기 전에 알아 둘 결과가 있어요:

| 잃는 것                | 의미                                                                                              |
| ------------------- | ----------------------------------------------------------------------------------------------- |
| 블록의 래퍼 요소           | 마크업을 감싸는 것이 없어요. 블록이 제공하던 패딩, 정렬, 레이아웃을 이제 직접 제공해야 해요.                                          |
| **블록의 Design 탭 설정** | 디자인 설정은 기본 래퍼에 인라인 스타일로 적용되는데, 그 래퍼가 사라져요. Design 탭에서 설정한 색상, 간격, 둥글기가 이 블록에 **더 이상 적용되지 않아요**. |
| 기본 제공 접근성 기능        | `aria-label`, 포커스 처리, 시맨틱 요소는 JSX에 포함해야만 존재해요.                                                  |

<Warning>
  **Design 탭이 사람들이 가장 많이 걸리는 부분이에요.** 커스텀 템플릿이 활성화되어 있는 동안 Design 탭의 필드는 비활성화되고 "Design" 제목 옆에 경고 아이콘이 표시돼요. 아이콘 위에 마우스를 올리면 이유를 볼 수 있어요. 대신 [인라인 스타일이나 직접 만든 CSS로](#styling-a-custom-template) 템플릿에서 블록의 스타일을 지정하세요. 커스텀 템플릿을 끄는 즉시 필드가 다시 활성화돼요.
</Warning>

유지되는 것: 장바구니 내 블록의 위치, 표시 여부 토글, 설정(여전히 받는 props에 반영됨), 장바구니의 [커스텀 CSS](/ko/aftersell/cart/custom-css) 패널, 그리고 **기본 로딩 스켈레톤**이에요.

마지막 항목이 사람들을 놀라게 해요. 블록은 템플릿에 도달하기 *전에* 장바구니가 아직 로딩 중인지 확인하므로, 로드 중에는 기본 스켈레톤이 렌더링되고 장바구니가 준비된 후에만 템플릿이 실행돼요. 로딩 상태를 직접 만들 필요가 없어요.

<div id="whats-available-inside-a-template">
  ## 템플릿 안에서 사용 가능한 것
</div>

템플릿은 하나의 함수 컴포넌트예요. **TSX**에서 컴파일되므로 타입 주석이 허용되며 컴파일 시 제거돼요. 그래서 기본 템플릿에 타입 주석이 있어요:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props: HeaderProps) {
  return <div>{/* … */}</div>;
}
```

**시그니처 줄과 닫는 중괄호는 잠겨 있어요** — 에디터가 둘 다 편집을 허용하지 않으며, 마우스를 올리면 "Locked — this line can't be edited."가 표시돼요. 그 사이에 본문을 작성해요. **Reset to default**만이 이를 교체할 수 있어요.

그 밖에 중요한 것:

* **다섯 가지 훅을 사용할 수 있어요:** `useState`, `useEffect`, `useMemo`, `useRef`, `useCallback`. 그리고 `<>…</>`를 위한 `Fragment`도 있어요.
* **import는 없어요.** 아무것도 `import`할 수 없고, 스코프에 `React` 객체가 없으므로 `React.useReducer`, `React.Children`도 없어요. 위 목록에 없는 훅은 사용할 수 없어요.
* **props는 읽기 전용이에요.** prop을 변경해도 유용한 결과가 없어요. 장바구니를 변경하려면 props에 직접 쓰지 말고 블록이 제공하는 핸들러 props(`onClose`, `increment`, `selectPlan` 등)를 사용하세요.
* **`window`에 접근할 수 있으므로**, 블록의 props가 다루지 않는 것이 필요할 때 템플릿에서 `window.aftersell.cart`를 통해 [Cart SDK](/ko/aftersell/cart/sdk-overview)를 호출할 수 있어요.

<div id="conventions-across-every-block">
  ## 모든 블록에 공통되는 규칙
</div>

세 가지 규칙이 어디서나 적용되며, 알아 두면 대부분의 추측이 사라져요:

* **`*Html` props는 미리 정제된 리치 텍스트예요.** `dangerouslySetInnerHTML`로 렌더링하세요. 이미 장바구니의 정제기(sanitizer)를 거쳤고, `{{total_price}}` 같은 머천트 토큰은 이미 치환되어 있어요.
* **`string`으로 도착하는 가격은 이미** 스토어 통화 형식으로 포맷되어 있어요. `number` 가격은 센트 단위예요. 블록은 둘 중 하나를 제공하며, 각 블록의 표에 어느 것인지 나와 있어요.
* **템플릿 안에서 `isLoading`은 항상 `false`예요.** 블록은 기본 스켈레톤을 렌더링하고 장바구니가 로드된 후에만 템플릿을 호출하므로, 이 prop은 분기용이 아니라 완결성을 위해 전달돼요.

<Note>
  일부 블록은 특정 상태에서 아무것도 반환하지 않으므로, 템플릿이 빈 데이터로 호출되는 일이 없어요. Rewards 템플릿은 빈 `milestones`를 절대 보지 않고, Subscription upgrade 템플릿은 null인 `view`를 절대 보지 않아요. 각 블록의 레퍼런스에 해당 여부가 명시되어 있으므로 빈 상태 분기를 생략할 수 있어요.
</Note>

<div id="styling-a-custom-template">
  ## 커스텀 템플릿 스타일링
</div>

시작점이 되는 기본 템플릿에는 블록의 클래스명이 있어요. 편집 내용의 스타일 지정 방법은 그 시작점에서 얼마나 멀어지는지에 따라 달라져요.

<div id="the-two-class-families">
  ### 두 가지 클래스 패밀리
</div>

기본 템플릿의 모든 요소에는 짝을 이루는 클래스명이 있으며, 이 둘은 매우 다른 역할을 해요:

| 패밀리               | 하는 일                                                                             | CSS를 작성해도 되나요?                                               |
| ----------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `cart-internal-*` | **블록의 기본 스타일을 담당해요.** 장바구니 스타일시트의 모든 규칙이 이 패밀리를 대상으로 해요.                         | 아니요. 장바구니 자체의 내부 구조이며, Custom CSS 에디터가 이를 대상으로 하는 선택자를 표시해요. |
| `cart-external-*` | **자체 스타일이 없는 연결 지점이에요.** 장바구니 스타일시트의 어떤 것도 이를 대상으로 하지 않으며, 여러분의 CSS가 사용하도록 존재해요. | 네. 블록 스타일을 변경하는 지원되는 방법이에요.                                  |

즉 `cart-internal-header__title`은 제목을 기본 제목처럼 *보이게* 만드는 것이고, `cart-external-header__title`은 모양을 바꾸고 싶을 때 잡아야 하는 손잡이예요.

<div id="small-changes-keep-both-classnames">
  ### 작은 변경: 두 클래스명 모두 유지
</div>

요소 순서를 바꾸거나, 라벨을 변경하거나, 기존 구조 안에 무언가를 추가하는 정도라면 클래스명을 그대로 두세요. 기본 외관을 그대로 유지하면서, `cart-external-*` 연결 지점을 대상으로 하는 [커스텀 CSS](/ko/aftersell/cart/custom-css)로 스타일을 변경하세요.

<div id="restructuring-drop-both-classnames">
  ### 구조 변경: 두 클래스명 모두 제거
</div>

구조를 손보는 수준을 넘어 DOM 구조 자체를 변경한다면, 마크업에서 **두** 패밀리 모두 제거하고 대신 [직접 만든 클래스명](#option-1-your-own-classnames-plus-custom-css)을 사용하세요. 각각 별도의 이유가 있어요.

**기본 CSS는 기본 DOM을 위해 작성되었으므로 `cart-internal-*`을 제거하세요.** 구조가 바뀐 마크업에 그 클래스를 유지하면, 더 이상 존재하지 않는 요소를 가정하는 레이아웃 규칙을 상속해요: 다른 자식을 기대하는 flex 컨테이너, 이동한 요소 사이의 간격, 제거한 것을 기준으로 한 위치 지정 등이에요. 보통 기본 규칙이 이기고 있는데 여러분의 CSS가 "작동하지 않는" 것으로 나타나요.

<Warning>
  **`cart-external-*`은 공유된 이름이지 여러분의 것이 아니므로 제거하세요.** 이 클래스명들은 기본 마크업에서 특정한 의미를 가지며, 커스텀 CSS는 장바구니 전체에 대해 한 번 작성돼요. 구조가 바뀐 템플릿이 이를 재사용하면, 작성하는 모든 규칙이 여러분의 구조와 기본 구조 모두를 대상으로 하게 돼요.

  커스텀 템플릿을 끄는 순간 문제가 발생해요: 블록이 기본 마크업으로 돌아가는데, CSS가 여전히 그것을 가리키며 원래 작성 대상이 아닌 DOM에 스타일을 적용해요. 자신만의 접두사를 사용하면 둘이 깔끔하게 분리되어, 템플릿을 꺼도 깔끔하게 되돌아가요.
</Warning>

만든 것에 스타일을 지정하는 두 가지 방법:

<div id="option-1-your-own-classnames-plus-custom-css">
  #### 방법 1: 직접 만든 클래스명 + 커스텀 CSS
</div>

유지하거나 재사용할 것에 가장 좋아요. 아무도 충돌하지 않을 접두사(보통 스토어나 브랜드 이름)를 클래스에 붙이세요:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props) {
  return (
    <div className="northwind-custom-header">
      <div className="northwind-custom-header__title" dangerouslySetInnerHTML={{ __html: props.title }} />
      <button type="button" className="northwind-custom-header__close" onClick={props.onClose}>
        &times;
      </button>
    </div>
  );
}
```

그런 다음 카트 에디터의 왼쪽 패널에서 **Cart settings**를 선택하고 오른쪽에서 **Custom CSS** 탭을 여세요:

```css theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.northwind-custom-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
}

.northwind-custom-header__title {
  font-size: 18px;
  font-weight: 600;
}

.northwind-custom-header__close:hover {
  opacity: 0.6;
}
```

접두사는 보기보다 중요해요. 접두사가 없으면 `.header`나 `.title` 같은 클래스가 장바구니 자체의 클래스, 다른 앱의 템플릿, 또는 미래의 블록과 충돌할 위험이 있어요.

<div id="option-2-inline-styles">
  #### 방법 2: 인라인 스타일
</div>

CSS 패널을 오갈 필요가 없고 모든 것이 한곳에 있어요:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
```

레이아웃 골격 잡기와 일회성 작업에 좋아요. 한계는 늘 그렇듯: `:hover` 같은 의사 클래스가 없고, 미디어 쿼리가 없고, 블록 간 재사용이 안 돼요. 이 중 하나라도 필요해지면 방법 1을 사용하세요.

<div id="picking-an-approach">
  ### 접근 방식 선택하기
</div>

| 상황                    | 할 일                                                     |
| --------------------- | ------------------------------------------------------- |
| 같은 구조, 다른 문구나 순서      | 두 클래스명 모두 유지하고 `cart-external-*`에 대한 Custom CSS로 스타일 변경 |
| 새 구조, 유지 관리할 스타일      | 직접 만든 접두사 클래스 사용, 장바구니 클래스 패밀리 둘 다 제거                   |
| 새 구조, 몇 가지 빠른 레이아웃 규칙 | 인라인 스타일, 장바구니 클래스 패밀리 둘 다 제거                            |
| 여러 블록에 걸친 많은 커스텀 코드   | 모든 곳에 직접 만든 접두사 클래스를 사용해, 어떤 템플릿이든 깔끔하게 끌 수 있게 하기       |

<Note>
  장바구니는 shadow root 안에 렌더링되므로 테마의 스타일시트가 안에 접근할 수 없어요. 커스텀 템플릿의 스타일은 테마가 아니라 장바구니 자체의 **Custom CSS** 패널이나 인라인 스타일에서 와야 해요. [커스텀 CSS](/ko/aftersell/cart/custom-css)를 참고하세요.
</Note>

<div id="when-a-template-fails">
  ## 템플릿이 실패할 때
</div>

고장 난 템플릿이 장바구니를 망가뜨리는 일은 없어요. 블록이 **아무것도** 렌더링하지 않고 주변의 모든 것은 계속 작동해요. 안전하지만 놓치기 쉬워요: 블록이 있어야 할 자리의 빈 공간이 증상이에요.

| 실패        | 확인 시점            | 보고 위치                                                     |
| --------- | ---------------- | --------------------------------------------------------- |
| 타입 오류     | 입력하는 동안          | 에디터의 인라인 물결선이에요. 컴파일을 막지 **않아요** — 컴파일러는 타입을 검사하지 않고 제거해요 |
| 구문 오류     | **Compile** 클릭 시 | 스토어프론트에 도달하기 전에 에디터에서                                     |
| 렌더링 중 크래시 | 라이브 후 스토어프론트에서   | `console.error('[aftersell-cart] module crashed: …')`     |

블록이 눈에 보이는 오류 없이 조용히 사라지므로, 게시하기 전에 항상 [미리보기](/ko/aftersell/cart/previewing-carts)에서 템플릿을 확인하세요. 블록이 사라졌다면 먼저 브라우저 콘솔을 여세요.

주의해야 할 두 가지가 있어요. 둘 다 그렇지 않다고 가정하는 템플릿을 크래시시키니까요:

* **null이 될 수 있는 props.** 많은 props가 정상 상황에서 `null`이에요(로고가 없을 때 `logoUrl`, 이미지가 없을 때 `imageUrl`, 단일 옵션 상품의 `variantTitle`). 사용하기 전에 확인하세요.
* **비어 있을 수 있는 배열.** `discountTags`와 `discountCodes`는 비어 있는 `[]`인 경우가 훨씬 많아요.

<div id="limitations">
  ## 제한 사항
</div>

* **커스텀 템플릿은 표시 재정의예요.** 장바구니에 대한 로직 실행(이벤트 구독, 아이템 추가, 변경에 반응)은 [커스텀 스크립트](/ko/aftersell/cart/custom-scripts)와 [Cart SDK](/ko/aftersell/cart/sdk-overview)를 사용하세요.
* **거의 모든 블록이 지원해요.** 예외는 Shopify 자체 결제 버튼을 담는 **[Express payments](/ko/aftersell/cart/express-payments-block)** 블록과 **[Cart items](/ko/aftersell/cart/cart-items-block)** 컨테이너 자체예요. 다만 그 안의 **Product** 행은 커스텀 템플릿을 지원해요.
* **템플릿은 블록의 근본적인 동작을 바꿀 수 없어요.** 블록 데이터의 표시 방식을 바꾸는 것이지, 그 뒤의 데이터나 동작을 바꾸는 것이 아니에요.

<div id="props-for-each-block">
  ## 각 블록의 props
</div>

모든 블록은 자체 데이터를 전달해요. 타입과 실습 예제가 포함된 전체 prop 표는 각 블록의 페이지에 있어요:

| 블록                                                                                    | 받는 props                                                                                                                           |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| [Header](/ko/aftersell/cart/header-block#custom-template)                             | `title`, `logoUrl`, `leftSection`, `rightSection`, `itemCount`, `onClose`, `isLoading`                                             |
| [Banner](/ko/aftersell/cart/banner-block#custom-template)                             | `text`, `shouldUseTimer`, `isTimerExpiredAndShouldHide`, `isLoading`                                                               |
| [Rewards](/ko/aftersell/cart/rewards-block#custom-template)                           | `milestones`, `rewardsMessageHtml`, `showIcons`, `isLoading`                                                                       |
| [Cart items · Product](/ko/aftersell/cart/cart-items-block#custom-template)           | 25개 props: 라인별 콘텐츠, 식별자, 수량 컨트롤                                                                                                    |
| [Subscription upgrade](/ko/aftersell/cart/subscription-upgrade-block#custom-template) | `view`, `selectPlan`, `onChange`, `oneTimeValue` 등                                                                                 |
| [Summary](/ko/aftersell/cart/summary-block#custom-template)                           | `leftHtml`, `rightHtml`, `discountCodes`, `totalPrice`, `savings` 등                                                                |
| [Checkout button](/ko/aftersell/cart/checkout-button-block#custom-template)           | `label`, `href`, `isLoading`                                                                                                       |
| [Discount code](/ko/aftersell/cart/discount-code-block#custom-template)               | `discountCodeInput`, `placeholder`, `buttonText`, `isValidating`, `isInvalid`, `setDiscountCodeInput`, `handleSubmit`, `isLoading` |
| [Empty cart](/ko/aftersell/cart/empty-cart-block#custom-template)                     | `text`, `cta`, `href`                                                                                                              |
| [Image](/ko/aftersell/cart/image-block#custom-template)                               | `imageUrl`, `altText`, `maxHeight`, `fullWidth`                                                                                    |
| [Notes](/ko/aftersell/cart/notes-block#custom-template)                               | `titleHtml`, `placeholder`, `noteInput`, `status`, `isExpanded`, `onNoteChange`, `onNoteBlur`, `onToggle` 등                        |
| [Product add-on](/ko/aftersell/cart/product-add-on-block#custom-template)             | `addonTitleHtml`, `descriptionHtml`, `priceHtml`, `imageUrl`, `format`, `isEnabled`, `handleAdd`, `handleToggle` 등                 |
| [Shipping protection](/ko/aftersell/cart/shipping-protection-block#custom-template)   | `titleHtml`, `descriptionHtml`, `priceHtml`, `imageUrl`, `format`, `isEnabled`, `handleAdd`, `handleToggle` 등                      |
| [Upsells](/ko/aftersell/cart/upsells-block#custom-template)                           | `title`, `addButtonText`, `layout`, `upsells`, `selectVariant`, `handleAdd`, 그리고 캐러셀 컨트롤                                           |

[Custom code](/ko/aftersell/cart/custom-code-blocks) 블록은 블록의 렌더링을 대체하는 것이 아니라 마크업을 **추가하는** 유일한 영역이므로 props가 달라요: 전체 장바구니와 장바구니 추가 액션이에요. [커스텀 코드 블록 → Props](/ko/aftersell/cart/custom-code-blocks#props)를 참고하세요.
