> ## 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.

# 훅

> Aftersell Cart의 동작 방식을 변경하세요: 라인 변환, Storefront 데이터로 라인 강화, 구독 옵션 구성, 장바구니 담기 제어입니다.

[이벤트](/ko/aftersell/cart/sdk-events)가 장바구니에 *반응*하게 하고 [액션](/ko/aftersell/cart/sdk-actions)이 장바구니를 *변경*하게 한다면, **훅**은 장바구니 자체의 동작 방식을 바꿔요: 라인이 어떻게 렌더링되는지, 어떤 데이터를 갖는지, 장바구니 담기 시 무슨 일이 일어나는지 말이에요.

훅은 `window.aftersell.cart.hooks` 아래에 있어요.

<Note>
  훅은 쇼핑객이 **보는 것**을 변경하고, 액션은 **장바구니에 담긴 것**을 변경해요. 변환으로 무료 선물 라인을 숨기면 장바구니와 합계에는 그대로 남아요. [`removeItem`](/ko/aftersell/cart/sdk-actions#removeitemkey)으로 제거하면 실제로 빠져요.
</Note>

<Note>
  훅은 설정용 호출이므로 스크립트 맨 위에서 등록해도 안전하며, `ready()`를 기다릴 필요가 없어요. 장바구니의 **Initialization** 스크립트에서 등록하세요([커스텀 스크립트](/ko/aftersell/cart/custom-scripts) 참고).
</Note>

<div id="how-registration-works">
  ## 등록 작동 방식
</div>

모든 훅은 `register*` 메서드예요. 함수를 전달해 호출하면 여러분의 함수를 제거할 수 있는 **등록 해제 함수**를 반환해요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);

// later: off();
```

등록은 **누적 방식**이므로 여러분의 함수는 다른 모든 함수와 함께 실행돼요. 페이지에 있는 스크립트가 여러분의 것만이 아닌 경우가 많기 때문에 중요해요: 구독 앱, 번들 앱, 테마 자체가 모두 같은 훅에 등록할 수 있어요. 어느 것도 여러분의 것을 대체할 수 없고, 여러분이 등록한 것이 이후에 로드되는 무언가에 의해 조용히 삭제될 수도 없어요.

| 훅                                                                                         | 하는 일                                                                                        | 여러 개가 등록된 경우                     |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------- |
| [`registerLineTransform`](#registerlinetransform)                                         | 개별 라인을 숨기거나 레이블을 변경해요.                                                                      | 등록 순서대로 모두 실행돼요.                 |
| [`registerLineComparator`](#registerlinecomparator)                                       | 렌더링되는 라인 순서를 변경해요.                                                                          | 타이브레이커로 조합돼요.                    |
| [`registerCartEnricher`](#registercartenricher)                                           | 각 라인에 추가 Storefront 데이터를 첨부해요.                                                              | 모두 실행되며, 각 `id`는 자체 네임스페이스예요.    |
| [`registerSubscriptionOptionsTransform`](#registersubscriptionoptionstransform)           | 라인의 판매 플랜을 숨기거나 이름을 변경해요.                                                                   | 모두 실행되며, 패치는 플랜별, 필드별로 병합돼요.     |
| [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector) | 미리 선택될 플랜을 결정해요.                                                                            | `null`이 아닌 첫 번째 답이 이겨요.          |
| [`registerSkipAddToCartRule`](#registerskipaddtocartrule)                                 | 특정 폼이 장바구니를 우회하도록 허용해요. [장바구니 담기 가로채기](/ko/aftersell/cart/add-to-cart-interception)를 참고하세요. | `true`를 반환하는 규칙이 하나라도 있으면 건너뛰어요. |

예외를 던지거나 함수가 아닌 훅은 건너뛰고, 나머지는 계속 실행되며 장바구니는 계속 작동해요. 하나의 깨진 통합이 장바구니 담기, 구독 선택기, 정렬을 망가뜨릴 수 없어요.

반대로 여러분의 깨진 훅은 **조용히** 실패해요: 브라우저 콘솔에 아무것도 표시되지 않아요. 이러한 실패가 어디에서 드러나는지는 [디버깅](/ko/aftersell/cart/sdk-overview#debugging)을 참고하세요.

***

<div id="registerlinetransform">
  ## registerLineTransform
</div>

`registerLineTransform(fn)`은 모든 장바구니 라인이 렌더링되기 전에 실행돼요. 쇼핑객의 장바구니에 실제로 담긴 것은 건드리지 않고 라인을 숨기거나 표시 방식을 변경하는 데 사용하세요.

함수는 읽기 전용 라인과 setter들을 받아요. 등록 해제 함수를 반환해요.

| Setter                            | 효과                                                                                                 |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| `setHidden(bool)`                 | 드로어에서 라인을 숨겨요. 장바구니와 합계에는 그대로 남아요.                                                                 |
| `setTitle(string)`                | 표시되는 제목을 변경해요.                                                                                     |
| `setVariantTitle(string \| null)` | 표시되는 변형 레이블을 변경해요.                                                                                 |
| `setInternalProperties(obj)`      | 렌더링 전용 속성을 병합해요. Shopify에 절대 저장되지 않아요. [번들 라인 그룹화](/ko/aftersell/cart/sdk-use-case-bundles)에 사용돼요. |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Hide free gift lines from the drawer. The cart total is unaffected.
const off = window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.finalLinePrice === 0) {
    line.setHidden(true);
  }
  if (line.sellingPlan) {
    line.setVariantTitle(`Delivered ${line.sellingPlan.name.toLowerCase()}`);
  }
});

// later: off();
```

<Warning>
  변환은 렌더링되는 내용만 변경해요. 가격, 수량, 라인 정체성은 변경할 수 없어요. 그런 경우에는 [액션](/ko/aftersell/cart/sdk-actions)을 사용하세요.
</Warning>

**용도:** 구매 사은품 라인이나 앱이 삽입한 라인 숨기기, 구독 라인의 레이블 변경, 할인 상품 태그하기, 쇼핑객이 개별적으로 관리하면 안 되는 번들 구성 요소 숨기기예요.

`setInternalProperties`는 번들 그룹화의 핵심 setter예요: 각 라인에 표준 번들 속성을 찍는 것이 서드파티 앱의 개별 장바구니 라인을 하나의 상품으로 렌더링하는 방법이에요. [다른 앱의 번들 라인 그룹화하기](/ko/aftersell/cart/sdk-use-case-bundles)를 참고하세요.

<div id="registerlinecomparator">
  ## registerLineComparator
</div>

`Array.prototype.sort`가 기대하는 것과 같은 형태의 비교자예요. 숨기기와 이름 변경 이후에 실행되므로 변환된 라인을 보게 돼요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Subscriptions first, then everything else.
window.aftersell.cart.hooks.registerLineComparator((lineA, lineB) => {
  return (lineB.sellingPlan ? 1 : 0) - (lineA.sellingPlan ? 1 : 0);
});
```

비교자는 **타이브레이커로 조합돼요**: 0이 아닌 값을 처음 반환하는 비교자가 그 쌍을 결정하고, 나머지는 동점일 때만 참조돼요. 의견이 없는 쌍에는 `0`을 반환하세요. 그래야 순서를 강제하는 대신 다음 비교자에게 결정을 넘겨요.

**용도:** 구독이나 고가 상품을 맨 위로 올리기, 무료 선물과 애드온을 맨 아래로 내리기, 스폰서 상품을 첫 번째로 유지하기예요.

<div id="registercartenricher">
  ## registerCartEnricher
</div>

`registerCartEnricher(registration)`은 Shopify Storefront API에서 추가 상품 또는 변형 데이터를 가져와 일치하는 각 장바구니 라인의 `line.metadata[id]`에 첨부해요. Aftersell의 코드 변경 없이 메타필드, 태그 등 Storefront API가 노출하는 모든 것을 표시하는 데 사용하세요.

| 필드         | 타입                                | 설명                                                                        |
| ---------- | --------------------------------- | ------------------------------------------------------------------------- |
| `id`       | `string`                          | 결과의 네임스페이스로, `line.metadata[id]`에 저장돼요. 고유해야 하며, 같은 `id`로 두 번째 등록하면 무시돼요. |
| `onType`   | `'Product'` 또는 `'ProductVariant'` | 프래그먼트가 대상으로 하는 노드예요. 조인 키(상품 ID 대 변형 ID)이기도 해요.                           |
| `fragment` | `string`                          | Storefront 쿼리에 삽입되는 GraphQL 필드 선택(바깥 중괄호 없음). 중괄호 짝이 맞아야 해요.              |

**등록 해제 함수**를 반환해요.

장바구니가 로드되거나 변경될 때마다 Aftersell은 장바구니의 모든 상품 또는 변형에 대해 프래그먼트를 가져와 결과를 첨부해요. 가져오기는 논블로킹이에요: 장바구니가 즉시 렌더링되고, 데이터가 도착하면 `cart_updated`가 다시 발생해요. 느리거나 실패하는 프래그먼트가 장바구니를 지연시키거나 망가뜨리는 일은 없어요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerCartEnricher({
  id: 'pricing',
  onType: 'ProductVariant',
  fragment: `
    anchorPrice: metafield(namespace: "custom", key: "anchor_price") { value }
    subscriberPrice: metafield(namespace: "custom", key: "subscriber_price") { value }
  `,
});

// Read it once the data arrives.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    const anchor = line.metadata.pricing?.anchorPrice;
    if (anchor) console.log(line.title, 'anchor price', anchor.value);
  });
});
```

인리치먼트는 비동기이므로 항상 읽기를 가드하세요. 첫 번째 가져오기가 resolve될 때까지 `line.metadata.pricing`은 `undefined`이고, `metadata` 자체의 기본값은 `{}`예요.

**용도:** 모든 라인에 메타필드(배송 예정일, 성분 목록, "별도 배송" 플래그, 로열티 배수)를 가져와 [커스텀 코드 블록](/ko/aftersell/cart/custom-code-blocks)으로 렌더링하는 데 사용하세요. [장바구니 라인에 메타필드 데이터 표시하기](/ko/aftersell/cart/sdk-use-case-metafields)를 참고하세요.

<Note>
  각 `id`가 자체 네임스페이스이므로 여러 인리처가 문제없이 공존하며, 데이터가 절대 충돌하지 않아요.
</Note>

<Warning>
  강화된 값은 Storefront API에서 그대로 반환되며 살균 처리되지 **않아요**. 원시 HTML이 아니라 텍스트로 렌더링하세요.
</Warning>

<div id="registersubscriptionoptionstransform">
  ## registerSubscriptionOptionsTransform
</div>

라인에서 제공되는 판매 플랜을 숨기거나 이름을 변경해요. 함수는 읽기 전용 옵션과 setter들을 받으며, 아무것도 반환하지 않아요.

| Setter            | 효과                |
| ----------------- | ----------------- |
| `setHidden(bool)` | 선택기에서 플랜을 숨겨요.    |
| `setName(string)` | 표시되는 플랜 이름을 변경해요. |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSubscriptionOptionsTransform((options, context) => {
  // context: { productId, variantId }
  options.forEach((option) => {
    if (option.discountPercent === 0) option.setHidden(true);
    option.setName(option.name.replace('Every ', ''));
  });
});
```

**반환 목록이 아니라 setter인 이유는 여러 스크립트가 공존할 수 있게 하기 위해서예요.** 이 훅이 배열을 반환한다면, 플랜 하나에만 관심 있는 변환도 자연스럽게 `options.filter(...)`를 작성해 다른 모든 앱의 플랜을 조용히 삭제하게 될 거예요. setter를 사용하면 자신의 편집만 기술할 수 있어요: 패치는 플랜별, 필드별로 병합되고, 같은 플랜의 같은 필드에 대한 실제 충돌은 마지막에 쓴 쪽이 이겨요. 예외를 던지는 변환은 아무것도 기여하지 않으며, 다른 변환은 계속 적용돼요.

모든 변환은 반쯤 패치된 뷰가 아니라 *원본* 옵션을 보므로, 등록 순서가 읽는 내용에 영향을 주지 않아요.

<Note>
  플랜 순서는 Shopify가 반환한 그대로 유지되므로 변환으로 순서를 변경할 수 없어요. 어떤 플랜이 먼저 제공될지(그리고 일회성 구매 업그레이드 버튼이 어떤 플랜을 구독할지) 제어하려면, 선택한 플랜을 맨 앞으로 올리는 [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector)를 사용하세요.
</Note>

플랜을 *추가*하거나 가격을 변경할 수도 없어요: `discountPercent`에는 setter가 없어요. Shopify가 결제 시 인정하지 않을 플랜은 선택기 안의 깨진 약속에 불과하기 때문이에요.

<div id="registerdefaultsubscriptionoptionselector">
  ## registerDefaultSubscriptionOptionSelector
</div>

라인에서 미리 선택될 플랜을 결정해요. 플랜 `id`를 반환하거나, 넘어가려면 `null`을 반환하세요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerDefaultSubscriptionOptionSelector((options) => {
  const best = options
    .slice()
    .sort((optionA, optionB) => optionB.discountPercent - optionA.discountPercent)[0];
  return best ? best.id : null;
});
```

**사용 가능한 플랜의 id를 처음 반환하는 selector가 이기므로**, 추측하는 대신 관심 없는 라인에는 `null`을 반환하세요. 그래야 재정의하는 대신 다음 selector에게 결정을 넘겨요. 라인의 어떤 플랜과도 일치하지 않는 id는 `null`과 동일하게 처리되어 마찬가지로 넘어가므로, 오래된 id가 선택기를 비워 버릴 수 없어요.

함수는 `(options, context)`를 받으며, 옵션 변환과 같은 `context`예요.

<div id="registerskipaddtocartrule">
  ## registerSkipAddToCartRule
</div>

`true`를 반환하면 특정 상품 폼이 Aftersell을 완전히 우회하고 정상적으로 장바구니에 추가하도록 허용해요. 자체 리디렉션이나 처리가 필요한 폼에 유용해요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);
```

**`true`가 하나라도 있으면 건너뛰므로**, 규칙을 좁게 유지해 소유한 특정 폼만 매칭하고 나머지에는 `false`를 반환하세요. 규칙은 등록 순서로 평가되고 첫 번째 `true`에서 멈추므로 규칙에 부작용을 넣지 마세요: 여러분의 규칙이 실행되는지 여부는 이전에 등록된 것에 달려 있어요.

<Tip>
  폼의 마크업을 제어할 수 있다면 훅이 전혀 필요 없어요: `<form>`에 **`aftersell-cart-skip-atc`** 클래스를 추가하면 Aftersell이 건드리지 않아요. 마크업을 편집할 수 없거나, 결정이 코드만 아는 무언가에 달려 있을 때 이 훅을 사용하세요.
</Tip>

**용도:** 자체 리디렉션이 필요한 예약 주문 또는 견적 폼, 구독 앱의 커스텀 플로우, 결제로 바로 이동해야 하는 "바로 구매" 버튼이에요. 페이지 전체의 가로채기를 끄려면 대신 [`skip_add_to_cart_interceptor`](/ko/aftersell/cart/sdk-configure#skip_add_to_cart_interceptor)를 사용하세요. 하지만 지정한 폼에만 적용되는 이 훅이 더 권장돼요.

<div id="where-to-go-next">
  ## 다음 단계
</div>

* **[Cart 객체](/ko/aftersell/cart/sdk-cart-object)**: 변환이 받는 라인의 구조예요.
* **[이벤트](/ko/aftersell/cart/sdk-events)**: 구독할 수 있는 모든 것이에요.
* **[액션](/ko/aftersell/cart/sdk-actions)**: 장바구니 읽기와 변경이에요.
* **[사용 사례](/ko/aftersell/cart/sdk-use-cases)**: 일반적인 요청에 대한 완전한 솔루션이에요.
