> ## 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 SDK 이벤트: 각 이벤트가 언제 발생하는지, 무엇을 전달하는지, 어디에 사용하는지, 그리고 무한 루프를 일으키는 실수들입니다.

이벤트를 사용하면 장바구니에서 **무언가가 일어났을 때** 코드를 실행할 수 있어요. `window.aftersell.cart.events` 아래에 있어요.

구독은 설정용 호출이므로 스크립트 맨 위에서 해도 안전하며, `ready()`를 기다릴 필요가 없어요.

<div id="available-events">
  ## 사용 가능한 이벤트
</div>

| 이벤트                                           | 페이로드                                                  | 발생 시점                      |
| --------------------------------------------- | ----------------------------------------------------- | -------------------------- |
| [`cart_loaded`](#cart_loaded)                 | [`AftersellCart`](/ko/aftersell/cart/sdk-cart-object) | 장바구니가 로드될 때, 페이지당 한 번이에요.  |
| [`cart_updated`](#cart_updated)               | [`AftersellCart`](/ko/aftersell/cart/sdk-cart-object) | 첫 로드 이후 장바구니 콘텐츠가 변경될 때예요. |
| [`item_added`](#item_added)                   | `{ item }`                                            | 장바구니에 새 라인이 나타날 때예요.       |
| [`item_removed`](#item_removed)               | `{ item }`                                            | 장바구니에서 라인이 사라질 때예요.        |
| [`cart_opened`](#cart_opened-and-cart_closed) | 없음                                                    | 드로어가 열릴 때예요.               |
| [`cart_closed`](#cart_opened-and-cart_closed) | 없음                                                    | 드로어가 닫힐 때예요.               |
| [`checkout`](#checkout)                       | 없음                                                    | 결제 버튼이 클릭될 때예요.            |

<div id="subscribing">
  ## 구독하기
</div>

`events.on(event, handler)`는 핸들러를 등록하고 **구독을 해제하는 함수를 반환해요**:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.events.on('cart_updated', (state) => {
  console.log('Cart total is now', state.totalPrice);
});

// later, to stop listening:
off();
```

* `events.once(event, handler)`: 한 번 실행된 후 스스로 구독을 해제해요.
* `events.off(event, handler)`: 특정 핸들러를 제거해요.

예외를 던지는 핸들러는 격리되어 콘솔에 기록되며, 다른 핸들러는 계속 실행돼요.

***

<div id="the-two-rules">
  ## 두 가지 규칙
</div>

거의 모든 이벤트 버그는 이 둘 중 하나로 귀결돼요.

<div id="dont-change-the-cart-from-cart_updated-without-a-guard">
  ### 가드 없이 `cart_updated`에서 장바구니를 변경하지 마세요
</div>

`cart_updated` 핸들러 안에서 장바구니를 변경하면 `cart_updated`가 다시 발생해요. 그 핸들러가 장바구니를 또 변경하면 무한 루프가 생겨요. 페이지가 Shopify를 두들기는 동안 쇼핑객은 장바구니가 요동치는 것을 보게 돼요.

<Warning>
  **`cart_updated`나 `cart_loaded`에서 액션을 무조건 호출하지 마세요.** 만들려는 상태를 검사하는 가드를 두어, 두 번째 실행에서는 아무것도 하지 않도록 하세요.
</Warning>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Loops forever: every add triggers an update, which triggers another add.
window.aftersell.cart.events.on('cart_updated', (state) => {
  window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
});

// ✅ Guarded: once the gift is present, the condition is false and it stops.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const hasGift = state.items.some((line) => line.variantId === GIFT_VARIANT_ID);
  if (state.totalPrice >= 5000 && !hasGift) {
    window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
  }
});
```

장바구니에는 안전망이 하나 있어요: **동일한** 장바구니를 만드는 업데이트는 아무것도 발생시키지 않으므로, 아무것도 변경하지 않는 다시 가져오기는 사이클을 재시작하지 않아요. 이는 의도치 않은 no-op 루프로부터 보호해 줘요. 하지만 매번 실제로 장바구니를 변경하는 핸들러로부터는 보호해 주지 **않아요**.

<div id="treat-the-payload-as-read-only">
  ### 페이로드는 읽기 전용으로 다루세요
</div>

하나의 이벤트에 대한 모든 핸들러는 *같은* 객체를 받아요. 이를 변경하면 이후 핸들러가 보는 내용이 바뀌며, 스토어의 다른 앱에 속한 핸들러도 영향을 받아요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Corrupts the payload for every later handler.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items = state.items.filter((line) => line.finalLinePrice > 0);
});

// ✅ Copy first.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const paidItems = state.items.filter((line) => line.finalLinePrice > 0);
});
```

실제로 장바구니를 변경하려면 [액션](/ko/aftersell/cart/sdk-actions)을 사용하세요. 라인 렌더링 방식을 변경하려면 [`registerLineTransform`](/ko/aftersell/cart/sdk-hooks#registerlinetransform)을 사용하세요.

***

<div id="cart_loaded">
  ## cart\_loaded
</div>

장바구니가 페이지에서 처음 로드될 때 **한 번** 발생해요. 페이로드는 전체 [cart 객체](/ko/aftersell/cart/sdk-cart-object)예요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_loaded', (state) => {
  console.log('Page loaded with', state.itemCount, 'items');
});
```

**용도:** 무료 선물 조정, 위젯 초기화, 페이지 로드 시 장바구니 콘텐츠의 분석 보고처럼 장바구니의 시작 상태를 기준으로 실행해야 하는 모든 작업이에요.

**`cart_loaded`는 늦은 구독자에게 재생돼요.** 장바구니가 이미 로드된 후 구독하면 핸들러가 현재 장바구니와 함께 즉시 호출돼요. 구독 순서는 전혀 중요하지 않으므로, 스크립트가 장바구니보다 먼저 실행됐는지 걱정할 필요가 없어요.

<Tip>
  페이지 로드 시와 이후 모든 변경 시 모두 올바르게 동작해야 하는 로직은 같은 함수로 `cart_loaded`와 `cart_updated` **모두**에 구독해야 해요. 이것이 "X를 장바구니와 동기화 유지"의 표준 패턴이에요.
</Tip>

<div id="cart_updated">
  ## cart\_updated
</div>

첫 로드 **이후** 장바구니 콘텐츠가 변경될 때마다 발생해요. 드로어에서든, 여러분의 액션에서든, 테마에서든, 다른 앱에서든 마찬가지예요. 페이로드는 전체 [cart 객체](/ko/aftersell/cart/sdk-cart-object)예요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  document.querySelector('#my-total').textContent =
    window.aftersell.cart.actions.formatMoney(state.totalPrice);
});
```

**용도:** 커스텀 합계, 진행률 바, 헤더 배지, 변경 시마다의 분석 이벤트처럼 장바구니 외부의 무언가를 동기화하는 데 사용하세요.

동일한 장바구니를 만드는 업데이트는 아무것도 발생시키지 않아요. 드로어를 다시 열거나, 탭을 다시 전환하거나, 같은 콘텐츠를 반환하는 다시 가져오기는 이벤트를 발생시키지 않아요.

<Warning>
  여기서 액션을 호출하기 전에 [두 가지 규칙](#the-two-rules)을 다시 읽어 보세요.
</Warning>

<div id="item_added">
  ## item\_added
</div>

장바구니에 **새 라인**이 나타날 때 발생해요. 페이로드는 `{ item }`이며, `item`은 [장바구니 라인](/ko/aftersell/cart/sdk-cart-object#cart-lines)이에요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_added', (payload) => {
  myAnalytics.track('Added to cart', {
    id: payload.item.variantId,
    title: payload.item.title,
    quantity: payload.item.quantity,
  });
});
```

**용도:** 서드파티 분석 도구에서의 장바구니 담기 추적이에요. 이것이 SDK의 가장 흔한 사용 사례예요. [장바구니 담기 추적하기](/ko/aftersell/cart/sdk-use-case-analytics)를 참고하세요.

이벤트 도출 방식에 대해 알아야 할 두 가지가 있어요:

<Warning>
  **수량 변경은 추가가 아니에요.** 장바구니는 수량이 아니라 *라인*의 차이를 비교해 추가와 제거를 판단해요. 쇼핑객이 라인을 1에서 3으로 올리면 `item_added`가 아니라 `cart_updated`가 발생해요. 수량 증가도 포착해야 한다면 `cart_updated` 핸들러에서 이전 상태와 비교하세요.
</Warning>

또한 페이지가 로드될 때 이미 장바구니에 있던 상품에는 발생하지 않아요. 그런 상품은 `cart_loaded`를 통해 도착해요. 서로 다른 상품 여러 개를 한 번에 추가하면 라인당 한 번씩 이벤트가 발생해요.

<div id="item_removed">
  ## item\_removed
</div>

장바구니에서 라인이 사라질 때 발생해요. 페이로드는 `{ item }`으로, 사라지기 직전의 라인이므로 `key`, `variantId`, `title`을 여전히 읽을 수 있어요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_removed', (payload) => {
  console.log('Removed', payload.item.title);
});
```

**용도:** 플래그 지우기, 쇼핑객이 거절한 오퍼 다시 표시하기, 제거 사항을 분석에 보고하기처럼 추가 시 했던 작업을 되돌리는 데 사용하세요.

`item_added`와 같은 주의 사항이 있어요: 0에 도달하지 않는 수량 감소는 제거가 아니에요.

<div id="cart_opened-and-cart_closed">
  ## cart\_opened와 cart\_closed
</div>

드로어가 열리고 닫힐 때 발생해요. 페이로드는 없어요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_opened', () => {
  myAnalytics.track('Cart viewed');
});

window.aftersell.cart.events.on('cart_closed', () => {
  document.body.classList.remove('cart-is-open');
});
```

**용도:** 조회 추적, 드로어 뒤의 동영상이나 캐러셀 일시 정지, 페이지의 클래스 토글이에요.

둘 다 초기 페이지 로드에는 발생하지 않으며, 실제로 열리거나 닫힐 때만 발생해요.

<div id="checkout">
  ## checkout
</div>

쇼핑객이 결제 버튼을 클릭할 때, 브라우저가 이동하기 직전에 발생해요. 페이로드는 없어요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('checkout', () => {
  myAnalytics.track('Checkout started');
});
```

**용도:** 결제 의도 추적이에요.

<Warning>
  **이 핸들러에서 결제를 취소할 수 없어요.** 이 이벤트는 게이트가 아니라 알림이며, 코드가 무엇을 하든 페이지 이동은 일어나요. 핸들러는 빠르고 동기적으로 유지하세요: `await`나 느린 네트워크 호출은 페이지가 언로드되기 전에 끝나지 않을 수 있어요. 반드시 전송해야 하는 것은 [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)을 사용하세요.
</Warning>

***

<div id="listening-from-outside-the-sdk">
  ## SDK 외부에서 수신하기
</div>

모든 이벤트는 `window`에서 DOM `CustomEvent`로도 디스패치되므로, `window.aftersell.cart`를 건드리지 않고도 수신할 수 있어요. 테마 파일, 서드파티 앱, 장바구니와 독립적으로 로드되는 스크립트에서 유용해요.

| 버스 이벤트         | DOM 이벤트                       |
| -------------- | ----------------------------- |
| `cart_loaded`  | `aftersell:cart:cart-loaded`  |
| `cart_updated` | `aftersell:cart:cart-updated` |
| `item_added`   | `aftersell:cart:item-added`   |
| `item_removed` | `aftersell:cart:item-removed` |
| `cart_opened`  | `aftersell:cart:cart-opened`  |
| `cart_closed`  | `aftersell:cart:cart-closed`  |
| `checkout`     | `aftersell:cart:checkout`     |

이름에 주의하세요: 버스는 `snake_case`를, DOM 이벤트는 `aftersell:cart:` 접두사 뒤에 `kebab-case`를 사용해요.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.addEventListener('aftersell:cart:cart-updated', (event) => {
  console.log('Cart total is now', event.detail.totalPrice);
});
```

페이로드는 `event.detail`에 담기며 [cart 객체](/ko/aftersell/cart/sdk-cart-object)와 일치해요. 이벤트는 `window`에서 디스패치되므로 페이지 어디에 있는 리스너든 받을 수 있어요. 장바구니는 shadow root에서 렌더링되지만, shadow 경계는 이벤트 경로에 절대 포함되지 않아요. 디스패치마다 페이로드가 복제되므로, `event.detail`을 변경하는 리스너가 다른 리스너에 영향을 줄 수 없고, 예외를 던지는 리스너도 SDK를 방해할 수 없어요.

<Warning>
  **`cart-loaded`는 DOM에서 재생되지 않아요.** 버스는 `cart_loaded`를 늦은 구독자에게 재생하지만, 그 경로는 DOM 디스패치를 우회하므로 장바구니가 이미 로드된 후에 등록한 `window.addEventListener('aftersell:cart:cart-loaded')`는 절대 발생하지 않아요. 스크립트의 로드 순서가 보장되지 않으면, 재생이 되는 `window.aftersell.cart.events.on('cart_loaded', …)`를 사용하거나 `aftersell:cart:cart-updated`도 함께 수신하세요.
</Warning>

<div id="shopify-standard-cart-events">
  ### Shopify 표준 장바구니 이벤트
</div>

별도로, 장바구니는 장바구니를 변경할 때마다 `document`에서 Shopify의 [표준 장바구니 이벤트](https://shopify.dev/docs/storefronts/themes/best-practices/standard-events)를 발행하므로, 테마 코드와 다른 앱이 테마의 변경에 반응하는 것과 같은 방식으로 Aftersell의 변경에 반응할 수 있어요:

| 이벤트                            | 이벤트 인스턴스의 페이로드                                                                   |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `shopify:cart:lines-update`    | `action: 'add' \| 'update' \| 'remove'`, `context: 'cart' \| 'product'`, `lines` |
| `shopify:cart:note-update`     | `context: 'cart'`, `note`                                                        |
| `shopify:cart:discount-update` | `discountCodes: [{ code }]`                                                      |

<Warning>
  **페이로드는 `event.detail`에 없어요.** `detail`에는 `{ source: 'aftersell' }`만 담겨 있어요 — 장바구니가 루프를 도는 대신 자체 이벤트를 무시하는 데 사용하는 태그예요. 위 표의 모든 항목은 이벤트 객체에 직접 할당되므로 `event.detail.action`이 아니라 `event.action`을 읽으세요.
</Warning>

각 이벤트에는 기반 쓰기 작업이 완료될 때 Aftersell이 settle하는 `promise`도 담겨 있어요. Shopify 표준과 일치해요 — await하고, 직접 resolve하지 마세요. 이 이벤트들은 `document`에서 디스패치되고 버블링되므로 `window` 리스너도 받을 수 있어요.

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

* **[Cart 객체](/ko/aftersell/cart/sdk-cart-object)**: 위 페이로드의 전체 구조예요.
* **[액션](/ko/aftersell/cart/sdk-actions)**: 핸들러에서 장바구니를 변경하는 방법이에요.
* **[훅](/ko/aftersell/cart/sdk-hooks)**: 장바구니에 반응하는 대신 렌더링 방식을 변경하는 데 사용해요.
* **[사용 사례](/ko/aftersell/cart/sdk-use-cases)**: 분석 추적, 무료 선물 등 완전한 예제예요.
