Saleor Core 한국 로컬라이제이션 분석: Tax, Address, Plugin/App 시스템

분석 대상: /tmp/saleor-core/ (Saleor Core 소스코드) 분석 일자: 2026-03-27


1. Tax 모듈 분석 (Taxes)

1.1 모델 구조

TaxClass

  • name: 세금 분류명 (예: “Standard”, “Reduced”, “Zero”)
  • ModelWithMetadata 상속 — metadata 필드 활용 가능

TaxClassCountryRate

  • tax_class: FK → TaxClass (null 허용 — null이면 해당 국가의 기본 세율)
  • country: CountryField
  • rate: Decimal (퍼센트 값, 예: 10.00 = 10%)
  • 제약조건: (country, tax_class) 유니크, tax_class=null일 때 country 유니크

TaxConfiguration (채널별 1:1)

  • channel: OneToOne → Channel
  • charge_taxes: bool (기본 True)
  • tax_calculation_strategy: “FLAT_RATES” 또는 “TAX_APP”
  • display_gross_prices: bool (기본 True — 세금 포함가 표시)
  • prices_entered_with_tax: bool (기본 True — 입력가격이 세금 포함인지)
  • tax_app_id: 외부 Tax App 식별자
  • use_weighted_tax_for_shipping: bool (배송비에 가중 세율 적용)

TaxConfigurationPerCountry (국가별 예외)

  • tax_configuration: FK → TaxConfiguration
  • 위와 동일한 필드를 국가별로 오버라이드

1.2 세금 계산 전략: Flat Rate vs Tax App

TaxCalculationStrategy:
  FLAT_RATES  — 내장 로직으로 국가별 세율 적용
  TAX_APP     — 외부 앱(Avalara 등)에 [[📒 Saleor Sync Events Tax|sync webhook]]으로 위임

Flat Rate 계산 핵심 로직

saleor/tax/calculations/__init__.pycalculate_flat_rate_tax():

def calculate_flat_rate_tax(money, tax_rate, prices_entered_with_tax):
    tax_rate = Decimal(1 + tax_rate / 100)
    if prices_entered_with_tax:
        net = money.amount / tax_rate      # 세금 역산
        gross = money.amount
    else:
        net = money.amount
        gross = money.amount * tax_rate    # 세금 가산
    return TaxedMoney(net=Money(net, currency), gross=Money(gross, currency))

Tax App (Dynamic) 계산

  • WebhookEventSyncType.CHECKOUT_CALCULATE_TAXES / ORDER_CALCULATE_TAXES sync webhook 발생
  • 외부 앱이 TaxData 객체 반환 (shipping_price_gross/net, shipping_tax_rate, lines[])
  • 각 line에 TaxLineData(tax_rate, total_gross_amount, total_net_amount) 포함

1.3 Checkout 세금 계산 흐름

flowchart TD
    A[Checkout 생성/수정] --> B{charge_taxes?}
    B -->|No| C[세금 0으로 설정]
    B -->|Yes| D{tax_calculation_strategy?}
    D -->|FLAT_RATES| E[update_checkout_prices_with_flat_rates]
    D -->|TAX_APP| F[Sync Webhook: CHECKOUT_CALCULATE_TAXES]

    E --> E1[국가 코드 결정 shipping > billing > channel default]
    E1 --> E2[TaxClassCountryRate에서 기본 세율 조회]
    E2 --> E3[각 라인별 TaxClass → 국가별 세율 조회]
    E3 --> E4[calculate_flat_rate_tax로 net/gross 산출]
    E4 --> E5[배송비 세율 계산 가중치 or TaxClass 기반]
    E5 --> E6[subtotal + shipping = total]

    F --> F1[Tax App이 TaxData 반환]
    F1 --> F2[라인별 net/gross 적용]
    F2 --> F6[subtotal + shipping = total]

1.4 한국 부가세(VAT) 적용 평가

현재 지원 가능한 부분

  • 10% 단일 세율: TaxClassCountryRate에 country=“KR”, rate=10 설정하면 즉시 적용 가능
  • 세금 포함가 표시: display_gross_prices=True, prices_entered_with_tax=True (한국 관행과 일치)
  • Flat Rate 전략: 한국처럼 단일 VAT 국가에 적합

한국 특화 개발이 필요한 부분

항목현재 상태필요 작업
10% VAT 기본 세율수동 설정 필요초기 데이터 migration으로 자동화
세금계산서 발행미지원Tax App 또는 별도 Invoice App 개발 필요
사업자등록번호Address/Order에 필드 없음metadata 활용 또는 커스텀 필드 추가
면세 품목 분류TaxClass로 가능 (rate=0)“면세”, “영세율” TaxClass 생성
현금영수증미지원Payment App에서 처리
원단위 절사quantize_price로 가능KRW 통화 설정 시 decimal_places=0 확인 필요 (Price Calculation)

세금계산서 구현 전략

  • INVOICE_REQUESTED async webhook 활용
  • 외부 Tax Invoice App에서 국세청 전자세금계산서 API 연동
  • Order metadata에 사업자등록번호(business_registration_number) 저장
  • invoice_request plugin hook 또는 App webhook으로 PDF 생성

2. Address 시스템 분석 (Address)

2.1 Address 모델 필드

필드타입한국 매핑
first_nameCharField(256)이름
last_nameCharField(256)
company_nameCharField(256)회사명
street_address_1CharField(256)도로명주소 (예: 테헤란로 521)
street_address_2CharField(256)상세주소 (예: 파르나스타워 37층)
cityCharField(256)시/군/구
city_areaCharField(128)읍/면/동 (address-level3)
postal_codeCharField(20)우편번호 (5자리)
countryCountryField”KR”
country_areaCharField(128)시/도 (address-level1)
phonePhoneNumberField전화번호 (E.164 형식)
validation_skippedBooleanField검증 건너뛰기 플래그

2.2 주소 검증 시스템

Saleor는 i18naddress 패키지(Google’s i18n address data 기반)를 사용한다.

검증 흐름:

  1. CountryAwareAddressForm.validate_address() 호출
  2. i18naddress.normalize_address(data) — 국가별 규칙으로 정규화
  3. i18naddress.get_validation_rules({"country_code": "KR"}) — 한국 규칙 로드
  4. 실패 시 InvalidAddressError 발생, 에러 필드에 매핑

국가별 규칙 오버라이드 메커니즘:

  • saleor/account/i18n_rules_override.py에서 i18naddress.load_validation_data 패치
  • 현재 일본(JP)만 오버라이드 (city 필드 문제 수정)
  • 한국도 동일 방식으로 오버라이드 가능

i18n 필드 매핑:

I18N_MAPPING = [
    ("name", ["first_name", "last_name"]),
    ("street_address", ["street_address_1", "street_address_2"]),
    ("city_area", ["city_area"]),
    ("country_area", ["country_area"]),   # 시/도
    ("company_name", ["company_name"]),
    ("postal_code", ["postal_code"]),
    ("city", ["city"]),                    # 시/군/구
]

2.3 한국 주소 호환성 평가

Google i18n Address의 한국 주소 포맷

Google의 AddressValidationMetadata에서 한국(KR)은 다음과 같이 정의된다:

  • fmt: %N%n%O%n%A%n%C%n%S%n%Z (이름, 회사, 도로명, 시, 도, 우편번호)
  • required_fields: country_area(시/도), city(시/군/구), street_address, postal_code
  • country_area_type: “do_si” (도/시)

현재 호환되는 부분

  • 기본 필드 구조가 한국 주소와 잘 매핑됨
  • country_area → 시/도 (서울특별시, 경기도 등)
  • city → 시/군/구 (강남구, 수원시 등)
  • street_address_1 → 도로명주소
  • postal_code → 5자리 우편번호

도로명주소 API 연동 포인트

flowchart LR
    A[사용자 주소 입력] --> B[도로명주소 검색 UI]
    B --> C[juso.go.kr API 호출]
    C --> D[결과 목록 표시]
    D --> E[선택된 주소 매핑]
    E --> F1["street_address_1 = 도로명주소"]
    E --> F2["city = 시/군/구"]
    E --> F3["country_area = 시/도"]
    E --> F4["postal_code = 우편번호"]
    E --> F5["city_area = 읍/면/동 (선택)"]
    F1 & F2 & F3 & F4 & F5 --> G[street_address_2 = 상세주소 직접 입력]

연동 방식 옵션:

  1. 프론트엔드 전용 (권장): Storefront에서 juso.go.kr API 호출, Saleor에는 매핑된 데이터만 전달
  2. Backend App: Address validation App이 주소 정규화 수행
  3. i18n_rules_override: 한국 주소 포맷 커스텀 (country_area_choices에 시/도 목록 추가)

필요한 수정사항

항목현재필요 작업난이도
한국 주소 필드 순서Google i18n 기본값i18n_rules_override에 KR fmt 패치낮음
도로명주소 검색미지원Storefront에서 juso.go.kr API 연동중간
시/도 선택지Google 데이터 기반필요 시 VALID_ADDRESS_EXTENSION_MAP 활용낮음
성+이름 순서이름 먼저UI에서 한국일 때 순서 변경낮음

3. Plugin 시스템 분석

3.1 아키텍처 개요 (Extending Overview)

Plugin은 Saleor 내장 확장 시스템이다. BasePlugin 클래스를 상속하고, PluginsManager가 등록된 플러그인들의 hook 메서드를 체이닝 방식으로 호출한다.

중요: 대부분의 plugin hook에 “deprecated” 표시가 있다. Saleor는 Plugin 시스템을 App/Webhook 시스템으로 전환 중이다.

3.2 내장 Plugin 목록

디렉토리역할
avatax/Avalara AvaTax 세금 계산
openid_connect/OpenID Connect 인증
admin_email/관리자 이메일 알림
user_email/사용자 이메일 알림
sendgrid/SendGrid 이메일
webhook/App Webhook 디스패처 (핵심)

3.3 Plugin Hook 메서드 카탈로그

세금 관련 (Tax Hooks)

Hook설명
calculate_checkout_line_totalCheckout 라인 총액 계산
calculate_checkout_line_unit_priceCheckout 라인 단가 계산
calculate_checkout_shippingCheckout 배송비 계산
calculate_checkout_totalCheckout 총액 계산
calculate_checkout_subtotalCheckout 소계 계산
calculate_order_line_totalOrder 라인 총액 계산
calculate_order_line_unitOrder 라인 단가 계산
calculate_order_shippingOrder 배송비 계산
calculate_order_totalOrder 총액 계산
get_checkout_line_tax_rateCheckout 라인 세율 조회
get_checkout_shipping_tax_rateCheckout 배송 세율 조회
get_order_line_tax_rateOrder 라인 세율 조회
get_order_shipping_tax_rateOrder 배송 세율 조회
get_taxes_for_checkoutCheckout 전체 세금 데이터 [deprecated]
get_tax_code_from_object_meta객체에서 세금 코드 추출
get_tax_rate_type_choices세금 유형 선택지 목록

결제 관련 (Payment Hooks)

Hook설명
authorize_payment결제 승인
capture_payment결제 캡처
confirm_payment결제 확인
check_payment_balance잔액 확인
get_client_token클라이언트 토큰
get_payment_config결제 설정
get_supported_currencies지원 통화 목록
initialize_payment결제 초기화
list_payment_sources결제 수단 목록
list_stored_payment_methods저장된 결제 수단 [deprecated]
stored_payment_method_request_delete저장 결제 수단 삭제 [deprecated]
payment_gateway_initialize_tokenizationPG 토큰화 초기화 [deprecated]
payment_method_initialize_tokenization결제 수단 토큰화 초기화 [deprecated]
payment_method_process_tokenization결제 수단 토큰화 처리 [deprecated]

엔티티 이벤트 훅 (모두 deprecated — App webhook으로 대체)

Hook대상
account_confirmed, account_deleted, etc.Account
address_created, address_updated, address_deletedAddress
app_installed, app_updated, app_deletedApp
attribute_created/updated/deletedAttribute
attribute_value_created/updated/deletedAttributeValue
category_created/updated/deletedCategory
channel_created/updated/deleted/status_changed/metadata_updatedChannel
checkout_created/updated/fully_paid/fully_authorized/metadata_updatedCheckout
collection_created/updated/deleted/metadata_updatedCollection
customer_created/updated/deleted/metadata_updatedCustomer
draft_order_created/updated/deletedDraft Order
fulfillment_created/canceled/approved/metadata_updatedFulfillment
gift_card_created/updated/deleted/status_changed/metadata_updated/export_completedGift Card
invoice_delete/request/sentInvoice
menu_created/updated/deletedMenu
menu_item_created/updated/deletedMenuItem
order_created/confirmed/cancelled/expired/fulfilled/paid/fully_paid/refunded/fully_refunded/updated/metadata_updated/bulk_createdOrder
page_created/updated/deletedPage

인증 관련

Hook설명
authenticate_user사용자 인증
external_authentication_url외부 인증 URL
external_logout외부 로그아웃
external_obtain_access_tokens외부 토큰 획득
external_refresh외부 토큰 갱신
external_verify외부 인증 검증

기타

Hook설명
notify알림 전송

3.4 PluginsManager 동작 방식

class PluginsManager(PaymentInterface):
    plugins_per_channel: dict[str, list[BasePlugin]]
    global_plugins: list[BasePlugin]
    all_plugins: list[BasePlugin]
  • 채널별로 독립된 플러그인 인스턴스 관리
  • 각 hook 호출 시 체이닝: 이전 플러그인의 반환값이 다음 플러그인의 previous_value로 전달
  • CONFIGURATION_PER_CHANNEL = True이면 채널별 설정 가능

4. App/Webhook 시스템 분석

4.1 App 모델

class App(ModelWithMetadata):
    uuid: UUID (유니크)
    name: CharField(60)
    type: "LOCAL" | "THIRDPARTY"
    identifier: CharField(256)  # 앱 고유 식별자
    is_active: bool
    permissions: M2M → Permission
    manifest_url: URLField       # 앱 매니페스트 URL
    app_url: URLField            # 앱 UI URL
    # ... 기타 메타 정보
  • AppType.LOCAL: Saleor 인스턴스 내부 앱
  • AppType.THIRDPARTY: 외부 앱 (Saleor App Store 등)
  • Permission 기반 접근 제어 — 앱에 필요한 권한만 부여

4.2 Webhook 모델

class Webhook(models.Model):
    app: FK → App
    target_url: URLField (http, https, awssqs, gcpubsub)
    is_active: bool
    secret_key: 서명 검증용
    subscription_query: [[📒 Saleor GraphQL|GraphQL]] subscription query
    custom_headers: JSON
    filterable_channel_slugs: ArrayField  # 특정 채널만 필터
 
class WebhookEvent(models.Model):
    webhook: FK → Webhook
    event_type: CharField  # WebhookEventAsyncType 또는 WebhookEventSyncType

4.3 Webhook Event 타입 전체 목록

Async Events (비동기 — 이벤트 발생 후 알림)

카테고리이벤트필요 권한
Accountaccount_confirmation_requested, account_email_changed, account_change_email_requested, account_set_password_requested, account_confirmed, account_delete_requested, account_deletedMANAGE_USERS
Addressaddress_created, address_updated, address_deletedMANAGE_USERS
Appapp_installed, app_updated, app_deleted, app_status_changedMANAGE_APPS
Attributeattribute_created/updated/deleted, attribute_value_created/updated/deletedNone
Categorycategory_created/updated/deletedMANAGE_PRODUCTS
Channelchannel_created/updated/deleted/status_changed/metadata_updatedMANAGE_CHANNELS
Checkoutcheckout_created/updated/fully_authorized/fully_paid/metadata_updatedMANAGE_CHECKOUTS
Collectioncollection_created/updated/deleted/metadata_updatedMANAGE_PRODUCTS
Customercustomer_created/updated/deleted/metadata_updatedMANAGE_USERS
Draft Orderdraft_order_created/updated/deletedMANAGE_ORDERS
Fulfillmentfulfillment_created/canceled/approved/metadata_updated/tracking_number_updatedMANAGE_ORDERS
Gift Cardgift_card_created/updated/deleted/sent/status_changed/metadata_updated/export_completedMANAGE_GIFT_CARD
Invoiceinvoice_requested/deleted/sentMANAGE_ORDERS
Menumenu_created/updated/deleted, menu_item_created/updated/deletedMANAGE_MENUS
Orderorder_created/confirmed/paid/fully_paid/refunded/fully_refunded/updated/cancelled/expired/fulfilled/metadata_updated/bulk_createdMANAGE_ORDERS
Pagepage_created/updated/deletedMANAGE_PAGES
Page Typepage_type_created/updated/deletedMANAGE_PAGE_TYPES_AND_ATTRIBUTES
Permission Grouppermission_group_created/updated/deletedMANAGE_STAFF
Productproduct_created/updated/deleted/metadata_updated/export_completedMANAGE_PRODUCTS
Product Mediaproduct_media_created/updated/deletedMANAGE_PRODUCTS
Product Variantproduct_variant_created/updated/deleted/metadata_updated/out_of_stock/back_in_stock/stock_updatedMANAGE_PRODUCTS
Promotionpromotion_created/updated/deleted/started/endedMANAGE_DISCOUNTS
Promotion Rulepromotion_rule_created/updated/deletedMANAGE_DISCOUNTS
Salesale_created/updated/deleted/toggleMANAGE_DISCOUNTS
Shippingshipping_price_created/updated/deleted, shipping_zone_created/updated/deleted/metadata_updatedMANAGE_SHIPPING
Staffstaff_created/updated/deleted/set_password_requestedMANAGE_STAFF
Transactiontransaction_item_metadata_updatedHANDLE_PAYMENTS
Translationtranslation_created/updatedMANAGE_TRANSLATIONS
Vouchervoucher_created/updated/deleted/codes_created/codes_deleted/metadata_updated/code_export_completedMANAGE_DISCOUNTS
Warehousewarehouse_created/updated/deleted/metadata_updatedMANAGE_PRODUCTS
기타notify_user [deprecated], observability, thumbnail_created, shop_metadata_updated각각 다름

Sync Events (동기 — 요청-응답 패턴)

이벤트설명필요 권한
payment_list_gateways결제 게이트웨이 목록HANDLE_PAYMENTS
payment_authorize결제 승인HANDLE_PAYMENTS
payment_capture결제 캡처HANDLE_PAYMENTS
payment_refund환불HANDLE_PAYMENTS
payment_void결제 취소HANDLE_PAYMENTS
payment_confirm결제 확인HANDLE_PAYMENTS
payment_process결제 처리HANDLE_PAYMENTS
checkout_calculate_taxesCheckout 세금 계산HANDLE_TAXES
order_calculate_taxesOrder 세금 계산HANDLE_TAXES
transaction_charge_requested거래 충전 요청HANDLE_PAYMENTS
transaction_refund_requested거래 환불 요청HANDLE_PAYMENTS
transaction_cancelation_requested거래 취소 요청HANDLE_PAYMENTS
shipping_list_methods_for_checkout배송 방법 목록MANAGE_SHIPPING
checkout_filter_shipping_methods배송 방법 필터MANAGE_CHECKOUTS
order_filter_shipping_methods주문 배송 방법 필터MANAGE_ORDERS
payment_gateway_initialize_sessionPG 세션 초기화HANDLE_PAYMENTS
transaction_initialize_session거래 세션 초기화HANDLE_PAYMENTS
transaction_process_session거래 세션 처리HANDLE_PAYMENTS
list_stored_payment_methods저장 결제 수단 목록HANDLE_PAYMENTS
stored_payment_method_delete_requested저장 결제 수단 삭제HANDLE_PAYMENTS
payment_gateway_initialize_tokenization_sessionPG 토큰화 세션HANDLE_PAYMENTS
payment_method_initialize_tokenization_session결제 수단 토큰화 세션HANDLE_PAYMENTS
payment_method_process_tokenization_session결제 수단 토큰화 처리HANDLE_PAYMENTS

4.4 Sync Webhook 디스패치 흐름

flowchart TD
    A[Checkout/Order 세금 계산 요청] --> B{tax_calculation_strategy?}
    B -->|TAX_APP| C[PluginsManager.get_taxes_for_checkout/order]
    C --> D[WebhookPlugin.get_taxes_for_checkout]
    D --> E[tax_app_id로 대상 App 결정]
    E --> F[sync webhook 전송 target_url로]
    F --> G[Tax App이 TaxData JSON 반환]
    G --> H[응답 파싱 → TaxData 객체]
    H --> I[라인별 net/gross/tax_rate 적용]

    B -->|FLAT_RATES| J[내장 Flat Rate 계산]

5. Plugin vs App: 비교 및 권장사항

5.1 핵심 차이 (Architecture)

항목Plugin (Legacy)App (현재 권장)
배포Saleor 코어에 포함독립 서비스로 배포
코드 위치saleor/plugins/ 내부외부 서비스 (어떤 언어든 가능)
통신Python 함수 직접 호출HTTP Webhook (sync/async)
상태대부분 deprecated 표시적극 개발 중
확장성모놀리식마이크로서비스
설정DB PluginConfigurationApp manifest + DB
업그레이드Saleor 버전에 종속독립적 버전 관리

5.2 한국 시장 개발 권장 방식

App(Webhook) 방식을 권장한다. 이유:

  1. Plugin 시스템은 deprecated 예정
  2. 독립 배포 가능 — Saleor 업그레이드에 영향 없음
  3. 한국 PG사/세금계산서/도로명주소 등 로컬 서비스 연동에 적합
  4. 필요한 sync webhook이 이미 모두 존재 (세금, 결제, 배송)

6. 한국 시장 로컬라이제이션 종합 권장사항

6.1 Tax App (세금계산서 + VAT)

flowchart TD
    subgraph "Korean Tax App"
        A[CHECKOUT_CALCULATE_TAXES webhook] --> B[10% VAT 적용]
        B --> C[면세/영세율 품목 체크]
        C --> D[TaxData 반환]

        E[ORDER_CONFIRMED webhook] --> F[사업자등록번호 확인]
        F --> G{세금계산서 필요?}
        G -->|Yes| H[국세청 전자세금계산서 API]
        G -->|No| I[현금영수증 발행]

        J[INVOICE_REQUESTED webhook] --> K[세금계산서 PDF 생성]
    end

6.2 개발 우선순위

순위항목구현 방식예상 난이도
110% VAT 기본 세율 설정Flat Rate (TaxClassCountryRate KR=10)매우 낮음
2KRW 통화 설정Channel 설정매우 낮음
3한국 주소 포맷i18n_rules_override + 프론트엔드낮음
4도로명주소 API 연동Storefront 프론트엔드중간
5한국 PG 결제 AppSync webhook App (Toss/NicePay 등)높음
6전자세금계산서 발행 AppAsync webhook App (국세청 API)높음
7현금영수증 발행Payment App 확장중간
8면세/영세율 품목 관리TaxClass 추가 + Admin UI낮음

6.3 Flat Rate로 충분한 경우

대부분의 한국 B2C 커머스에서는 Flat Rate 전략으로 충분하다:

  • 한국은 단일 VAT 10%
  • TaxClass “면세”(rate=0), “표준”(rate=10) 두 가지면 대부분 커버
  • Tax App은 세금계산서 발행이 필요한 B2B에서만 필요

별도 Tax App이 필요한 경우:

  • B2B 세금계산서 발행
  • 면세/과세 혼합 거래의 자동 분류
  • 국세청 API 연동

관련 문서

핵심 개념

확장 시스템

결제 및 기타

상위 인덱스