Shoplive 숏폼 SDK 연동 가이드
쇼핑 숏폼(Shorts)을 앱에 붙이는 방법을 구현하고 싶은 케이스(미션) 단위로 안내합니다.
풀스크린 재생 · 컬렉션 임베드 · 플로팅 프리뷰 · 상품/공유 연동까지 필요한 방식만 골라 적용하면 됩니다.
코드 예제는 상단의 iOS / Android 탭으로 전환해 볼 수 있습니다.
개요 & 요구사항
Shoplive 숏폼 SDK는 커머스에 최적화된 세로형 숏폼을 앱에 붙이는 SDK입니다. 영상 재생·프리로드·무한 스크롤·상품 오버레이·좋아요/댓글/북마크 같은 숏폼 UI는 SDK가 통째로 제공하고, 앱은 어떤 숏폼을 보여줄지(조회 조건)와 탭했을 때 어디로 보낼지(라우팅)만 책임집니다.
모듈 구성
미션 지도 — 무엇을 구현하고 싶나요?
| 구현하고 싶은 것 | 미션 |
|---|---|
| 버튼을 누르면 숏폼이 풀스크린으로 열리고 위아래로 넘겨진다 | 미션 1 · 풀스크린으로 띄우기 |
| 홈·기획전 화면 안에 숏폼 컬렉션을 넣는다 | 미션 2 · 컬렉션 붙이기 |
| 상품 상세에서 작은 창으로 관련 숏폼을 띄운다 | 미션 3 · 프리뷰(플로팅) 띄우기 |
| 로그인 회원으로 좋아요·댓글·북마크를 남기게 한다 | 미션 4 · 사용자 정보 연결 |
| 상품 탭 → 우리 상세 페이지로 보낸다 / 공유 시트를 우리 것으로 띄운다 | 미션 5 · 이벤트 · 상품 · 공유 |
| 우리 서버가 정한 순서·목록 그대로 재생한다 (개인화 피드) | 미션 6 · 내 목록으로 재생 |
| 좋아요·공유 버튼을 숨기거나, 카드 표시 항목을 바꾼다 | 미션 7 · UI 커스터마이징 |
제공 형태 (플랫폼 매핑)
| 형태 | iOS | Android |
|---|---|---|
| 풀스크린 재생 | ShopLiveShortform.play(requestData:) | ShopLiveShortform.play(context, collectionData) |
| 컬렉션 (카드/세로/가로) | CardTypeViewBuilder · ListViewBuilder → getView() | ShopLiveShortformCardTypeView 등 XML View |
| 화면 안 전체 피드 | ShopLiveShortsCollectionView (UIView) | ShopLiveShortformDetailTypeView (XML View) |
| 플로팅 프리뷰 | ShopLiveShortform.showPreview(requestData:) | ShopLiveShortform.showPreview(activity, data) |
| 이벤트 수신 | ShopLiveShortformReceiveHandlerDelegate | ShopLiveShortformHandler |
build(...).getView()), Android는 XML에 뷰를 놓고 설정한 뒤 submit() 합니다.
조회 조건(태그·브랜드·SKU·그룹)과 이벤트 모델은 두 플랫폼이 동형이라, 한쪽에서 익힌 개념이 그대로 넘어갑니다.요구사항
| 항목 | iOS | Android |
|---|---|---|
| 최소 버전 | iOS 11.0+ | shoplive-media3(기본 엔진) 사용 시 minSdk 23+ · shoplive-exoplayer 사용 시 19+ |
| 언어 / 툴체인 | Swift 5 | Kotlin · JDK 17. compileSdk 36 |
| SDK 버전 | 1.8.13 (ShopliveShortformSDK) | 1.8.14 (shoplive-short-form) |
| 필수 동반 모듈 | ShopliveSDKCommon (common-ios) | shoplive-common · shoplive-network · 플레이어 엔진 |
설치
iOS
// ① Swift Package Manager
// Xcode ▸ File ▸ Add Package Dependencies…
dependencies: [
.package(url: "https://github.com/shoplive/shortform-ios", .upToNextMajor(from: "1.8.13")),
.package(url: "https://github.com/shoplive/common-ios", .upToNextMajor(from: "1.8.13")),
]
// ② CocoaPods — Podfile
// pod 'ShopliveShortformSDK', '1.8.13'
// pod 'ShopliveSDKCommon', '1.8.13'
// ③ import
import ShopliveSDKCommon // accessKey · 사용자 인증 · 공용 모델
import ShopLiveShortformSDK // 숏폼 재생 · 컬렉션 · 프리뷰
// settings.gradle / build.gradle — mavenCentral() 필요
dependencies {
implementation("cloud.shoplive:shoplive-short-form:1.8.14") // 숏폼
implementation("cloud.shoplive:shoplive-common:1.8.14") // 공통 (accessKey · 인증)
implementation("cloud.shoplive:shoplive-network:1.8.14") // 네트워크
// 재생 엔진 — 아래 둘 중 <b>하나만</b> 선언하세요 (엔진 버전은 SDK 버전과 별개 체계)
implementation("cloud.shoplive:shoplive-media3:1.10.1.3") // media3 (기본) — minSdk 23
// implementation("cloud.shoplive:shoplive-exoplayer:2.19.1.20") // ExoPlayer (레거시) — minSdk 19
}
android {
defaultConfig { multiDexEnabled = true }
}
shoplive-short-form은 공통·네트워크·플레이어 엔진을 compileOnly로 참조합니다.
즉 앱 모듈에서 함께 선언하지 않으면 런타임에 클래스가 없습니다. 기본은 shoplive-media3,
레거시 ExoPlayer를 쓰는 앱이면 shoplive-exoplayer를 대신 넣으세요.사용하는 클래스가 어디서 오는지
| 클래스 | 모듈 (iOS) | 패키지 (Android) |
|---|---|---|
ShopLiveCommon · ShopLiveCommonError | ShopliveSDKCommon | cloud.shoplive.sdk.common |
ShopLiveShortform · 리스트 뷰 · 프리뷰 | ShopLiveShortformSDK | cloud.shoplive.sdk.shorts |
| 상품 · 숏폼 응답 모델 | ShopliveSDKCommon (ProductData 등) | cloud.shoplive.sdk.network.response |
빠른 시작 (Quick Start)
설치를 마친 뒤, 아래 코드로 첫 숏폼을 재생할 수 있습니다.
accessKey는 Shoplive에서 발급합니다. 값이 없다면 Shoplive 담당자 또는 Shoplive 어드민에서 발급·확인하세요.
숏폼은 캠페인 키가 필요 없고, 조회 조건(태그 · 브랜드 · SKU · 그룹 ID)으로 재생할 목록을 정합니다.풀스크린 재생 — 2줄
// 1) 앱 시작 시 1회 (AppDelegate)
ShopLiveCommon.setAccessKey(accessKey: "발급받은_ACCESS_KEY")
// 2) 재생 — 조건 없이 부르면 전체 숏폼
ShopLiveShortform.play(requestData: ShopLiveShortformCollectionData(delegate: nil))
// 1) 앱 시작 시 1회 (Application.onCreate)
ShopLiveCommon.setAccessKey("발급받은_ACCESS_KEY")
// 2) 재생 — 조건 없이 부르면 전체 숏폼
ShopLiveShortform.play(context)
이 두 줄이면 숏폼 컬렉션을 불러와 세로 풀스크린으로 재생하고, 상품 카드·좋아요·댓글·공유 오버레이까지 동작합니다. 각 줄의 상세: ① 사용자 정보 연결 · ② 미션 1.
setAccessKey가 반드시 먼저 실행돼야 합니다.
호출 전이면 9000 NOT_INITIALIZED_ACCESS_KEY 에러가 전달됩니다 (에러 처리).
AppDelegate / Application.onCreate에서 처리하는 것을 권장합니다.조건을 붙여 재생
let data = ShopLiveShortformCollectionData(
tags: ["봄신상", "아우터"],
tagSearchOperator: .AND, // 모든 태그를 만족 (.OR = 하나라도)
brands: ["shoplive"],
shuffle: true,
skus: ["SKU-1001"],
shortsCollectionId: "COLLECTION_ID",
isMuted: false,
delegate: self // 이벤트 수신 — 미션 5
)
ShopLiveShortform.play(requestData: data)
val data = ShopLiveShortformCollectionData().apply {
tags = listOf("봄신상", "아우터")
tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
brands = listOf("shoplive")
shuffle = true
skus = listOf("SKU-1001")
shortsCollectionId = "COLLECTION_ID"
isMuted = false
handler = myHandler // 이벤트 수신 — 미션 5
}
ShopLiveShortform.play(context, data)
다음 단계화면 용어 가이드 · 미션 1부터 시작 →
화면 용어 가이드
이 섹션은 화면의 각 요소를 부르는 이름을 정리한 것입니다. 이후 미션·옵션·이벤트 설명에서 같은 용어를 사용합니다. 인증 필요 표시는 로그인 사용자로 식별되어야 동작하는 요소입니다 (미션 4).
상세 화면 (풀스크린 재생)
위아래 스와이프로 넘기는 세로 풀스크린입니다. 1 ·3·6이 모두 이 화면으로 이어집니다.
| 번호 | 요소 | 노출 조건 | 설명 · 관련 API |
|---|---|---|---|
| ① | 영상 영역 | 상시 | 세로 영상. 채움 방식은 setResizeMode (CENTER_CROP/FIT/AUTO) — 미션 7 |
| ② | 크리에이터 · 브랜드 | 상시 | 콘솔에서 설정한 작성자/브랜드 정보 |
| ③ | 제목 · 설명 · 해시태그 | 상시 | shortsDetail.title / description / tags |
| ④ | 상품 카드 · 상품 배너 | 상시 | 탭 시 앱으로 콜백 — 미션 5 (handleProductItem / getOnClickProductListener) |
| ⑤ | 좋아요 | 인증 필요 | isLikeButtonVisible로 숨김 가능 — 미션 7 |
| ⑥ | 댓글 | 인증 필요 | isCommentButtonVisible |
| ⑦ | 북마크 | 인증 필요 | isBookmarkVisible |
| ⑧ | 공유 | 상시 | 탭 시 앱이 공유 시트를 띄웁니다 — 미션 5 (handleShare / onShare) |
| ⑨ | 음소거 토글 | 상시 | 초기값은 isMuted.이벤트로 VIDEO_MUTED/VIDEO_UNMUTED 수신 |
| ⑩ | 닫기(뒤로) | 상시 | 앱에서 강제로 닫으려면 ShopLiveShortform.close() |
컬렉션 (앱 안 임베드)
앱의 기존 화면에 붙이는 컬렉션입니다. 카드 한 장에 무엇을 보여줄지 옵션으로 켜고 끕니다 (미션 7).
| 타입 | 모양 | 쓰는 곳 |
|---|---|---|
카드형 (CardType) | 그리드. 열 개수 조절 가능 | 기획전 · 브랜드관 전용 영역 |
세로형 (Vertical) | 아래로 이어지는 컬렉션 | 탭 하나를 통째로 숏폼으로 |
가로형 (Horizontal) | 옆으로 넘기는 캐러셀 | 홈 피드 중간 띠 배너 |
전체 피드 (DetailType / ShortsCollectionView) | 상세 화면을 그대로 뷰로 | 탭 전체가 숏폼인 앱 |
| 카드 안 요소 | 기본값 | 끄는 방법 |
|---|---|---|
| 썸네일 / 자동재생 영상 | 재생 ON | disablePlayVideos() |
| 제목 | 표시 | setVisibleTitle(false) |
| 설명 | 표시 | setVisibleDescription(false) |
| 브랜드 | 표시 | setVisibleBrand(false) |
| 상품 수 | 표시 | setVisibleProductCount(false) |
| 조회수 | 표시 | setVisibleViewCount(false) |
프리뷰 (플로팅)
다른 화면 위에 떠 있는 작은 창입니다. 탭하면 상세 화면으로 확대됩니다 (미션 3).
| 요소 | 설명 · 관련 API |
|---|---|
| 위치 | 네 모서리 중 하나 — previewPosition / position (기본 bottomRight) |
| 크기 · 모서리 | iOS previewScale · previewRadius / Android width · height · radius |
| 여백 | iOS previewEdgeInset / Android marginTop 등 4방향 |
| 닫기 버튼 | useCloseButton (기본 표시) |
| 스와이프로 닫기 | iOS enableSwipeOut / Android setEnabledSwipeOut |
| 탭 동작 | 기본은 상세 화면 확대. iOS useCustomAction + clickEventCallBack, Android setUseCustomAction + setOnClickListener로 앱이 가로챌 수 있음 |
이전 ← 빠른 시작다음 → 데모 앱으로 확인하기
데모 앱으로 확인하기
SDK와 함께 제공되는 데모 앱은 이 가이드의 미션과 같은 케이스 메뉴(미션 지도)로 구성되어 있습니다. 코드를 쓰기 전에 데모에서 동작을 먼저 보면 이해가 빠릅니다.
- 기본 모드 — Shoplive 데모 설정: 설치 직후 별도 입력 없이 전체 케이스를 데모 데이터로 확인할 수 있습니다.
- 내 정보 모드 — 고객사 설정 입력: 설정 화면에 발급받은
accessKey와 (선택) 사용자 JWT를 넣으면 내 숏폼과 내 사용자로 같은 케이스를 체험할 수 있습니다. - 옵션 다이얼로그에서 태그 · 브랜드 · SKU · 셔플 · 카드 타입 · 프리뷰 위치를 바꿔가며 미션 7 의 옵션이 화면에 어떻게 반영되는지 바로 볼 수 있습니다.
이전 ← 화면 용어 가이드다음 → 미션 1 · 숏폼 풀스크린으로 띄우기
미션 1 · 숏폼 풀스크린으로 띄우기
- 앱 시작 시
setAccessKey가 호출되어 있는지 확인합니다 (빠른 시작). - 진입 버튼의 액션에서 조회 조건을 담은 데이터를 만들어
play를 호출합니다. - (선택) 델리게이트/핸들러를 붙여 상품 탭·공유 이벤트를 받습니다 — 미션 5.
@IBAction func openShorts() {
let data = ShopLiveShortformCollectionData(
tags: ["봄신상"],
tagSearchOperator: .AND,
shuffle: true,
delegate: self // 선택: 이벤트 수신
)
ShopLiveShortform.play(requestData: data)
}
fun openShorts() {
val data = ShopLiveShortformCollectionData().apply {
tags = listOf("봄신상")
tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
shuffle = true
handler = myHandler // 선택: 이벤트 수신
}
ShopLiveShortform.play(context, data)
}
shortsId를 함께 넘기면 그 숏폼이 첫 화면으로 열리고, 나머지는 뒤로 이어집니다.
푸시·딥링크로 특정 숏폼을 여는 경우가 여기에 해당합니다.ShopLiveShortform.intent(context, data)가 Intent를 돌려줍니다.
직접 startActivity로 띄우면 트랜지션·플래그를 앱이 정할 수 있습니다.닫기
ShopLiveShortform.close() // 앱이 강제로 닫을 때 (예: 로그아웃, 딥링크 전환)
ShopLiveShortform.close() // 앱이 강제로 닫을 때 (예: 로그아웃, 딥링크 전환)
완료 확인
- 버튼 탭 후 1~2초 내 영상이 재생된다
- 위아래 스와이프로 다음/이전 숏폼으로 넘어간다
- 상품이 등록된 숏폼이면 하단에 상품 카드가 보인다 (화면 용어 ④)
- 조건을 잘못 넣어 결과가 0건이면 빈 화면 안내가 뜬다 — 조건을 좁히지 않았는지 확인
연관: 이벤트 · UI 옵션다음 → 미션 2 · 컬렉션 붙이기
미션 2 · 컬렉션 붙이기
컬렉션은 세 가지 모양이 있습니다 — 카드형(그리드) · 세로형 · 가로형(캐러셀). 세 타입 모두 같은 조회 조건과 같은 표시 옵션을 씁니다. 모양만 고르면 됩니다.
1단계 · 뷰 만들어 붙이기
// iOS — 빌더로 만들고 getView() 로 꺼내 붙입니다.
private var builder: ShopLiveShortform.CardTypeViewBuilder?
override func viewDidLoad() {
super.viewDidLoad()
let builder = ShopLiveShortform.CardTypeViewBuilder()
let listView = builder.build(
cardViewType: .type1,
listViewDelegate: self,
shortsCollectionDelegate: self, // 상품·공유 이벤트 — 미션 5
enableSnap: false,
enablePlayVideo: true,
playOnlyOnWifi: false,
cellSpacing: 8,
cellRadius: 16,
viewCountVisibility: true
).getView()
listView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(listView)
// … 오토레이아웃 제약 …
builder.setHashTags(tags: ["봄신상"], tagSearchOperator: .AND)
builder.submit() // ← 조건 확정 후 반드시 호출
self.builder = builder
}
// Android — XML 에 뷰를 놓고, 코드에서 설정한 뒤 submit()
// res/layout/fragment_home.xml
// <cloud.shoplive.sdk.shorts.ShopLiveShortformCardTypeView
// android:id="@+id/shortsCardTypeView"
// android:layout_width="match_parent"
// android:layout_height="0dp" />
with(binding.shortsCardTypeView) {
setSpanCount(2) // 그리드 열 개수
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
handler = myHandler // 상품·공유 이벤트 — 미션 5
setHashTags(listOf("봄신상"), ShopLiveShortformTagSearchOperator.AND)
submit() // ← 조건 확정 후 반드시 호출
}
listViewDelegate는 필수 메서드가 2개입니다 (iOS)
ShopLiveShortformListViewDelegate는 NSObject를 상속해야 하고,
onListViewError(error:)와 onShortsSettingsInitialized() 둘 다 optional이 아닙니다.
하나라도 빠지면 컴파일되지 않습니다. onShortsSettingsInitialized()는 CardTypeViewBuilder로 만든 카드형 뷰에서만 호출됩니다.
extension MyViewController: ShopLiveShortformListViewDelegate {
func onListViewError(error: Error) {
// 목록 조회 실패 — 재시도 UI 등
}
func onShortsSettingsInitialized() {
// 서버 설정 수신 완료 — 이 시점 이후에 옵션을 다시 적용하면 안전합니다
}
}submit()을 빠뜨리면 컬렉션이 비어 있습니다
조회 조건(setHashTags · setBrands · setSkus · setShortsCollectionId)은
설정만으로는 반영되지 않습니다. 마지막에 submit()을 불러야 서버 조회가 일어납니다.
조건을 바꿔 다시 불러올 때도 마찬가지입니다 (iOS는 reloadItems()도 가능).2단계 · 모양 고르기
// 세로형 / 가로형은 ListViewBuilder 로
let builder = ShopLiveShortform.ListViewBuilder()
let listView = builder.build(
cardViewType: .type1,
listViewType: .horizontal, // .vertical | .horizontal
playableType: .CENTER, // 가로형에서 어떤 셀을 재생할지
listViewDelegate: self,
shortsCollectionDelegate: self
).getView()
builder.submit()
// 세로형 — ShopLiveShortformVerticalTypeView
with(binding.shortsVerticalTypeView) {
setSpanCount(2)
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
submit()
}
// 가로형 — ShopLiveShortformHorizontalTypeView
with(binding.shortsHorizontalTypeView) {
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
setPlayableType(ShopLiveShortform.PlayableType.CENTER)
enableSnap()
submit() // ← 가로형도 submit() 이 필요합니다
}
submit(collectionData, response)은 이미 받아둔 응답을 넣는 형태입니다.
ShopLiveShortform.get(context, collectionData, listener)로 먼저 조회한 뒤 넘기세요 —
홈 화면에서 여러 캐러셀을 한 번의 조회로 채울 때 유리합니다.3단계 · 화면 전환 시 재생 제어
컬렉션 자동재생은 화면이 보이지 않을 때 꺼야 배터리와 데이터가 절약됩니다.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
builder?.enablePlayVideos()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
builder?.disablePlayVideos()
}
override func viewWillTransition(to size: CGSize, with c: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: c)
builder?.notifyViewRotated() // 회전 시 레이아웃 재계산
}
override fun onResume() {
super.onResume()
binding.shortsCardTypeView.enablePlayVideos()
}
override fun onPause() {
super.onPause()
binding.shortsCardTypeView.disablePlayVideos()
}
4단계 (선택) · 화면 전체를 숏폼 피드로
풀스크린을 따로 띄우는 대신 앱의 탭 하나를 통째로 숏폼 피드로 만들 수 있습니다.
let feed = ShopLiveShortsCollectionView(
requestData: ShopLiveShortformCollectionData(tags: ["봄신상"], delegate: self)
)
view.addSubview(feed)
feed.resultHandler = { result in
if case .didScrollToShortsId(let shortsId) = result {
print("현재 숏폼: \(shortsId ?? "")")
}
}
feed.action(.play) // .pause · .setMuted(true) · .setScrollEnabled(false) 등
// res/layout — <cloud.shoplive.sdk.shorts.ShopLiveShortformDetailTypeView … />
with(binding.shortsDetailTypeView) {
setMuted(false)
setUIComponentPadding(bottom = bottomNavHeight) // 하단 탭바에 가리지 않게
setHashTags(listOf("봄신상"), ShopLiveShortformTagSearchOperator.AND)
submit() // ← 조건 확정 후 반드시 호출
}
// 시스템 뒤로가기를 피드에 전달
override fun onBackPressed() {
binding.shortsDetailTypeView.onBackPressed()
}
완료 확인
- 컬렉션에 썸네일이 채워지고, 보이는 셀이 자동 재생된다
- 카드를 탭하면 풀스크린 화면로 연결된다
- 다른 탭으로 이동하면 재생이 멈춘다 (
disablePlayVideos) - 조건을 바꾸고
submit()하면 컬렉션이 새로 채워진다
연관: 표시 항목 끄기다음 → 미션 3 · 프리뷰 띄우기
미션 3 · 프리뷰(플로팅) 띄우기
프리뷰는 상품과 숏폼을 잇는 동선에 씁니다. 상품 상세에 들어온 사용자에게 그 상품이 나오는 숏폼을 작은 창으로 보여주고, 관심을 보이면 풀스크린으로 올리는 흐름입니다.
- 상품 화면에 진입할 때
productId또는skus로 관련 숏폼을 지정합니다. - 위치·크기·닫기 버튼 등 표시 옵션을 정합니다.
- 화면을 벗어날 때 프리뷰를 내립니다.
let preview = ShopLiveShortformPreviewData(
productId: "PRODUCT_ID",
isMuted: true,
previewPosition: .bottomRight,
previewRadius: 12,
previewEdgeInset: .init(top: 0, left: 0, bottom: 24, right: 16),
useCloseButton: true,
enableSwipeOut: true,
maxCount: 10,
delegate: self
)
ShopLiveShortform.showPreview(requestData: preview)
// 내릴 때
ShopLiveShortform.close()
ShopLiveShortform.hidePreview() // 중복 방지: 띄우기 전 한 번 내림
ShopLiveShortform.showPreview(
activity,
ShopLiveShortformPreviewData().apply {
productId = "PRODUCT_ID"
isMuted = true
setPosition(ShopLivePreviewPositionConfig.BOTTOM_RIGHT)
setRadius(12f)
setMarginBottom(24)
setMarginRight(16)
setUseCloseButton(true)
setIsAutoClosed(true)
maxCount = 10
handler = myHandler
}
)
// 내릴 때
ShopLiveShortform.hidePreview()
// 현재 떠 있는지 확인
if (ShopLiveShortform.isShowingPreview()) { /* … */ }
hidePreview(), iOS는 close() 입니다.useCustomAction: true와 clickEventCallBack을 함께 주면
프리뷰 탭 시 풀스크린으로 올리는 대신 앱이 원하는 화면으로 보낼 수 있습니다.완료 확인
- 상품 상세 진입 시 지정한 모서리에 작은 창이 뜬다
- 탭하면 풀스크린 화면로 확대된다
- 닫기 버튼(또는 스와이프)으로 사라진다
- 다른 화면으로 이동하면 프리뷰가 남아 있지 않다
연관: 상품 탭 이벤트다음 → 미션 4 · 사용자 정보 연결
미션 4 · 사용자 정보 연결
setAuthToken · setUser 는 지금 시청자가 누구인지 Shoplive 에 알려 주기만 해서, 좋아요·댓글·북마크와 시청 이력이 그 회원 기준으로 남게 합니다. Shoplive 는 사용자의 자격 증명을 확인하지 않습니다. JWT(setAuthToken)는 그 정보가 고객사 서버가 발급했고 바뀌지 않았는지(서명)를 Shoplive 가 확인할 수 있고, 프로필(setUser)은 그 확인 없이 정보를 그대로 넘깁니다.1단계 · 사용자 정보를 넣어야 할까? — 판단 기준
| 상황 | 필요 여부 |
|---|---|
| 숏폼을 보기만 한다 (재생·스크롤·상품 탭) | 필요 없음 — 비로그인으로 동작 |
| 좋아요 · 댓글 · 북마크를 남긴다 | 필요 — 사용자 식별이 없으면 기록이 남지 않음 |
| 회원별 시청 이력·개인화가 필요하다 | 필요 |
2단계 · 로그인 상태에 따라 고르기 — 권장 방식
| 사용자 | 권장 | 이유 |
|---|---|---|
| 비로그인 | 아무것도 호출하지 않음 | 게스트 식별자는 서버가 발급합니다. 로그인했던 사용자를 게스트로 되돌릴 때는 아래 로그아웃 처리로 사용자 정보를 초기화합니다 |
| 로그인 (권장) | setAuthToken — 서버가 서명한 JWT | 위변조할 수 없어 좋아요·댓글·북마크·시청 이력을 그 회원 것으로 믿고 쓸 수 있습니다 |
| 로그인 (백엔드 작업이 어려울 때) | setUser — 프로필(userId·이름 등)을 직접 전달 | 서명이 없는 클라이언트 값이라 위조할 수 있습니다. 식별·통계에는 충분하지만 권한 검증에는 쓰지 마세요 |
| 로그아웃 · 사용자 정보 초기화 | setAuthToken(nil/null) + setUser(nil/null) (iOS·Android 공통) | 설정한 사용자 정보(JWT·프로필)를 지워 이전 사용자 기록이 섞이지 않게 합니다. 이름은 setAuthToken 이지만 넘기는 값은 사용자 정보(JWT)입니다 |
setAuthToken → 로그아웃 시 사용자 정보 초기화.// ① 로그인 · 보안 인증 (권장) — 백엔드가 서명한 JWT (4단계)
ShopLiveCommon.setAuthToken(authToken: myJwtFromServer)
// ② 로그인 · 간편 — 프로필 직접 전달 (서명 없음 · 권한 검증에 쓰지 않음)
ShopLiveCommon.setUser(user: ShopLiveCommonUser(
userId: hashedUserId, // 원문 회원 ID 대신 해시값 (3단계)
userName: "홍길동",
age: 29,
gender: .male,
userScore: 3
))
// ③ 비로그인 — 호출하지 않아도 됩니다
// 로그아웃 — 사용자 정보 초기화 (JWT·프로필을 지운다)
ShopLiveCommon.setAuthToken(authToken: nil)
ShopLiveCommon.setUser(user: nil)
// 현재 로그인 상태
let isLoggedIn = ShopLiveCommon.isLoggedIn()
// ① 로그인 · 보안 인증 (권장) — 백엔드가 서명한 JWT (4단계)
ShopLiveCommon.setAuthToken(myJwtFromServer)
// ② 로그인 · 간편 — 프로필 직접 전달 (서명 없음 · 권한 검증에 쓰지 않음)
ShopLiveCommon.setUser(
ShopLiveCommonUser(userId = hashedUserId).apply { // 원문 회원 ID 대신 해시값 (3단계)
setUserName("홍길동")
setAge(29)
setGender(ShopLiveCommonUserGender.MALE)
}
)
// ③ 비로그인 — 호출하지 않아도 됩니다
// 로그아웃 — 사용자 정보 초기화 (JWT·프로필을 지운다)
ShopLiveCommon.setAuthToken(null)
ShopLiveCommon.setUser(null)
clearAuth() 는 플랫폼마다 지우는 범위가 다릅니다 Android 는 사용자 정보(사용자·JWT·userId)만 지웁니다. iOS 는 accessKey·게스트 식별자·UTM 까지 공통 상태 전체를 초기화하므로, 쓴다면 다음에 쓰기 전에 accessKey 를 다시 설정해야 합니다. 사용자 정보만 지우려면 위처럼 setAuthToken · setUser 에 nil(null) 을 넘기세요.3단계 · ID 해싱 — 회원 ID 원문을 노출하지 않기
userId는 Shoplive 서버·콘솔에 저장되고 다른 사용자에게 노출될 수 있는 값입니다.
앱 내부 회원 ID를 해시해서 넣고, 앱은 그 해시값으로 자기 회원을 역추적할 수 있게 매핑 테이블을 두세요.앱과 백엔드가 동일한 방식으로 해싱한 값을 userId로 사용합니다. JWT 의 userId 와 프로필의 userId 모두 같습니다.
같은 사용자는 항상 같은 값이 나오도록 일관된 salt·알고리즘을 쓰는 것이 핵심입니다(좋아요·북마크 유지, 통계·재방문 식별).
해싱 코드 예시 보기 (Swift · Kotlin)
import CryptoKit
func hashUserId(_ raw: String) -> String {
let salted = raw + "APP_SALT"
let digest = SHA256.hash(data: Data(salted.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
import java.security.MessageDigest
fun hashUserId(raw: String): String {
val salted = raw + "APP_SALT"
return MessageDigest.getInstance("SHA-256")
.digest(salted.toByteArray())
.joinToString("") { "%02x".format(it) }
}
setAuthToken)를 사용하세요. salt 정책은 Shoplive 팀과 합의된 값을 권장합니다.4단계 · 보안 인증용 JWT 만들기 (고객사 백엔드)
setAuthToken에 넘길 JWT는 Shoplive가 발급한 Secret Key로 고객사 백엔드가 서명합니다. 앱은 백엔드에서 JWT를 받아 setAuthToken으로 넘기기만 합니다.
라이브(Player) SDK와 같은 JWT입니다.
- Shoplive 담당자에게서 Secret Key를 받습니다 — base64로 인코딩된 문자열입니다
- 로그인한 사용자 정보로 아래 payload를 만들고 HS256으로 서명합니다
- 앱이 요청할 때마다(숏폼 화면에 들어가기 직전마다) JWT 를 새로 발급해 내려 줍니다. 앱에 JWT 를 저장해 두고 다시 쓰지 않습니다
exp 까지 계속 쓸 수 있습니다. 앱이 setAuthToken 을 부를 때마다 백엔드에 새 JWT 를 요청하게 하세요.| claim | 필수 | 설명 | 예 |
|---|---|---|---|
exp | 필수 | 만료 시각 (Unix 초) | 1516239022 |
userId | 필수 | 로그인 사용자의 고유 값 (최대 255바이트). 해시 값을 써도 됩니다(3단계) | sample_user_id |
iat | 발급 시각 (Unix 초) | 1516195822 | |
name | 채팅에 보일 이름. 금칙어는 별표(*)로 바뀝니다 | 라이브팬 | |
gender | 성별 — m · f · 빈 값(미지정) | f | |
age | 나이. 통계에는 5세 단위로 묶여 표시됩니다 | 27 | |
userScore | 사용자 등급 (-100 ~ 100). 등급별 입장·이벤트 추첨 제한의 기준 | 10 | |
userType | 사용자 유형. 입장 제한(Entry Type)을 건 방송에서 특정 유형만 입장시킬 때 | VIP | |
profile | 채팅 프로필 이미지 URL (64×64px 권장) | https://…/profile.png | |
custom | 임의 데이터 (최대 1KB) | {"grade":"vip"} |
서명 예시입니다. 두 코드는 같은 토큰을 만듭니다 — Secret Key가 base64 문자열이라는 점만 주의하세요.
// Node.js — npm i jsonwebtoken
const jwt = require("jsonwebtoken");
const secret = Buffer.from(process.env.SHOPLIVE_SECRET_KEY, "base64"); // base64 를 풀어 바이트로 쓴다
const token = jwt.sign(
{ userId: hashedUserId, name: "라이브팬", gender: "f", age: 27, userScore: 10 },
secret,
{ algorithm: "HS256", expiresIn: "12h" } // exp · iat 를 자동으로 채운다
);
// Java — io.jsonwebtoken (jjwt)
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey) // base64 문자열을 그대로 넘긴다
.setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000L))
.setIssuedAt(new Date())
.claim("userId", hashedUserId)
.claim("name", "라이브팬")
.claim("gender", "f")
.claim("age", 27)
.claim("userScore", 10)
.compact();
setAuthToken 으로 넘기기만 합니다. 프로필(setUser)은 서명이 없어 권한 검증에 쓸 수 없습니다.5단계 · 호출 시점
- 앱 로그인 직후 — 숏폼 화면에 들어가기 전에 설정
- 숏폼 화면에 들어갈 때마다 — 백엔드에서 새로 발급한 JWT로 다시
setAuthToken(저장해 둔 토큰을 다시 쓰지 않음) - 로그아웃 시 — 사용자 정보를 초기화해야 이전 사용자 기록이 섞이지 않음
완료 확인
- 로그인 후 좋아요를 누르고 앱을 재실행해도 눌린 상태가 유지된다
- 로그인 후 댓글 작성이 차단되지 않는다
- 로그아웃 → 사용자 정보를 초기화한 뒤 다시 들어가면 좋아요·북마크 상태가 초기화된다(게스트로 시청된다)
이전 ← 미션 3다음 → 미션 5 · 이벤트 · 상품 · 공유
미션 5 · 이벤트 · 상품 · 공유
SDK는 화면만 그리고, 결정은 앱에 넘깁니다. 상품을 탭했을 때 어떤 화면으로 보낼지, 공유를 눌렀을 때 어떤 링크를 만들지는 모두 앱이 정합니다. 그 통로가 델리게이트(iOS) / 핸들러(Android) 입니다.
델리게이트 구조 — 크게 3가지
| 무엇 | iOS | Android |
|---|---|---|
| 상품 탭 | handleProductItem(shortsId:shortsSrn:product:) | getOnClickProductListener() |
| 상품 배너 탭 | handleProductBanner(shortsId:shortsSrn:scheme:) | getOnClickBannerListener() |
| 공유 탭 | handleShare(shareMetadata:) · handleShare(shareUrl:) | onShare(context, data) · onShare(context, url) |
| 일반 이벤트 | onEvent(messenger:command:payload:) | onEvent(context, messenger, command, payload) |
| 에러 | onError(error:) | onError(context, error) |
| 화면 표시/해제 | onDidAppear() · onDidDisAppear() | onCreate() · onDestroy() |
| 숏폼 전환 | onShortsAttached/Detached(data:) | onShortsAttached/Detached(data) |
1단계 · 연결하기
// 데이터 객체에 delegate 를 심어 넘깁니다 (재생·프리뷰·리스트 공통)
let data = ShopLiveShortformCollectionData(delegate: self)
ShopLiveShortform.play(requestData: data)
extension MyViewController: ShopLiveShortformReceiveHandlerDelegate {
// 필요한 것만 구현 — 전부 optional
}
// 핸들러를 먼저 선언합니다
val myHandler = object : ShopLiveShortformHandler() {
// 필요한 것만 override
}
// ① 데이터/뷰마다 지정 — 이 방식이 기본입니다
val data = ShopLiveShortformCollectionData().apply { handler = myHandler }
binding.shortsCardTypeView.handler = myHandler
// ② 전역 — 하이브리드(WebView) 브리지 경로에서만 읽힙니다
ShopLiveShortform.setHandler(myHandler)
2단계 · 상품 탭 → 우리 상세 페이지로
func handleProductItem(shortsId: String, shortsSrn: String, product: ProductData) {
// product.url · sku · productId · customerProductId 중 앱이 쓰는 키로 라우팅
guard let urlStr = product.url, let url = URL(string: urlStr) else { return }
navigationController?.pushViewController(ProductDetailViewController(url: url), animated: true)
}
func handleProductBanner(shortsId: String, shortsSrn: String, scheme: String) {
// 배너는 콘솔에서 지정한 스킴/URL 이 그대로 넘어옵니다
openDeepLink(scheme)
}
override fun getOnClickProductListener() = ShopLiveShortformProductListener { context, data, product ->
// product.url · sku · productId 중 앱이 쓰는 키로 라우팅
product.url?.takeIf { it.startsWith("http") }?.let { url ->
ProductDetailActivity.start(context, url)
}
}
override fun getOnClickBannerListener() = ShopLiveShortformUrlListener { context, data, url ->
openDeepLink(context, url)
}
customerProductId를 쓰세요
productId는 Shoplive 내부 식별자입니다. 앱의 상품 상세로 보내려면 sku 나 url로 라우팅하는 편이 안전합니다.iOS는 콘솔에 등록해 둔 고객사 상품 ID(
customerProductId)도 콜백으로 받지만, Android의 상품 콜백 모델에는 이 필드가 없습니다.3단계 · 공유 탭 → 우리 공유 시트
공유 버튼을 누르면 SDK가 앱에 콜백을 넘깁니다. 콘솔·웹이 공유 URL을 내려주면 handleShare(shareUrl:),
아니면 handleShare(shareMetadata:)가 호출됩니다. 앱이 자체 딥링크·동적 링크를 만들어 공유 시트를 띄우면 됩니다.
handleShare(shareUrl:)를 구현하지 않으면 SDK가 기본 공유 시트를 띄웁니다
shareMetadata 쪽만 구현하고 shareUrl 쪽을 비워 두면, URL 경로로 들어온 공유는
SDK가 iOS 기본 공유 시트를 대신 띄웁니다. 우리 공유 시트만 쓰려면 두 오버로드를 모두 구현하세요.func handleShare(shareMetadata: ShopLiveShareMetaData) {
// title · descriptions · thumbnail · shortsId · shortsSrn
let link = makeMyDeepLink(shortsId: shareMetadata.shortsId)
let vc = UIActivityViewController(activityItems: [shareMetadata.title ?? "", link],
applicationActivities: nil)
// 숏폼은 별도 윈도우 위에 뜨므로, 그 윈도우 기준으로 present 해야 합니다
ShopLiveShortform.getCurrentKeyWindow()?.rootViewController?.present(vc, animated: true)
}
override fun onShare(context: Context, data: ShopLiveShortformShareData) {
// shortsId · srn · title · description · thumbnail
val link = makeMyDeepLink(data.shortsId)
val send = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "${data.title}\n$link")
}
context.startActivity(Intent.createChooser(send, null))
}
present 하면
공유 시트가 숏폼 뒤에 가려 보이지 않습니다. 반드시 ShopLiveShortform.getCurrentKeyWindow()가 돌려주는
윈도우 기준으로 띄우세요.4단계 · 일반 이벤트 받기
func onEvent(messenger: ShopLiveShortformMessenger?, command: String, payload: String?) {
switch command {
case "VIDEO_MUTED": isMuted = true
case "VIDEO_UNMUTED": isMuted = false
case "DETAIL_ON_PLAYER_SHOWN": analytics.log("shorts_detail_open")
case "COLLECTION_CLICK_ITEM": analytics.log("shorts_card_click")
default: break // 모르는 커맨드는 무시
}
}
override fun onEvent(
context: Context,
messenger: ShopLiveShortformMessageListener?,
command: String,
payload: Map<String, Any?>
) {
when (command) {
"VIDEO_MUTED" -> isMuted = true
"VIDEO_UNMUTED" -> isMuted = false
else -> Unit // 모르는 커맨드는 무시
}
}
default를 꼭 두세요
이벤트 커맨드 목록은 SDK 업데이트로 추가될 수 있습니다. 아는 값만 처리하고
나머지는 무시하도록 작성해야 SDK를 올려도 앱이 깨지지 않습니다. 목록은 타입 & 상태 참고.완료 확인
- 상품 카드를 탭하면 우리 앱의 상품 상세로 이동한다
- 공유를 탭하면 우리 링크가 들어간 공유 시트가 뜬다 (숏폼 화면에 가리지 않음)
- 음소거 토글 시
VIDEO_MUTED/VIDEO_UNMUTED가 로그에 찍힌다 - 오류 상황에서
onError의code/message가 잡힌다 (에러 처리)
이전 ← 미션 4다음 → 미션 6 · 내 목록으로 재생
미션 6 · 내 목록으로 재생
미션 1의 조회 조건(태그·브랜드)은 Shoplive가 목록을 정하는 방식입니다. 개인화 추천처럼 우리 서버가 순서를 정해야 한다면 숏폼 ID 배열을 직접 넘기세요.
- 우리 API로 첫 페이지의 숏폼 ID 목록을 받습니다.
- ID 목록과 시작 지점(
currentId)을 넘겨 재생합니다. - SDK가 목록 끝에 닿으면 다음 페이지를 요청합니다 — 앱이 받아 채워줍니다.
// 1·2) 첫 페이지 재생
let ids = firstPage.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) }
ShopLiveShortform.play(
shortformIdsData: ShopLiveShortformIdsData(ids: ids, currentId: ids.first?.shortsId),
dataSourceDelegate: self,
shortsCollectionDelegate: self
)
// 3) 페이지네이션 — 아래로 / 위로 각각
extension MyFeed: ShortsCollectionViewDataSourcRequestDelegate {
func onShortformListDownwardPagination(
completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
) {
myAPI.next { page, error in
guard let page = page else { completion((nil, error)); return }
let more = ShopLiveShortformIdsMoreData(
ids: page.items.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) },
hasMore: page.hasMore
)
completion((more, nil))
}
}
func onShortformListUpwardPagingation(
completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
) {
completion((nil, nil)) // 위로 더 불러올 게 없으면 nil
}
}
// 1·2·3) 재생 + 페이지네이션 (코루틴 버전)
val idsData = ShopLiveShortformIdsData().apply {
ids = firstPage.map { ShopLiveShortformIdData(it.id) }
currentId = firstPage.firstOrNull()?.id
handler = myHandler
}
ShopLiveShortform.play(
context,
idsData,
moreSuspendListener = { // 아래로 더 불러오기
val page = myApi.next()
ShopLiveShortformIdsMoreData().apply {
ids = page.items.map { ShopLiveShortformIdData(it.id) }
hasMore = page.hasMore
}
},
previousSuspendListener = { null } // 위로 더 불러올 게 없으면 null
)
// 코루틴을 쓰지 않는다면 Future 버전도 있습니다
// ShopLiveShortform.play(context, idsData, moreFutureListener, previousFutureListener)
hasMore = false를 꼭 내려주세요
마지막 페이지에서 hasMore를 false로 주지 않으면
SDK가 계속 다음 페이지를 요청합니다. 빈 배열 + hasMore=false로 끝을 알리세요.ShopLiveShortformIdData.payload에 추천 사유·랭킹 같은 앱 데이터를 담아 두면,
이벤트 콜백에서 그대로 돌려받아 로깅에 쓸 수 있습니다.완료 확인
- 우리 서버가 준 순서 그대로 재생된다
currentId로 지정한 숏폼이 첫 화면이다- 끝까지 내리면 다음 페이지가 이어붙는다
- 마지막 페이지 이후 추가 요청이 멈춘다 (
hasMore=false)
이전 ← 미션 5다음 → 미션 7 · UI 커스터마이징
미션 7 · UI 커스터마이징 & 노출 제어
방법 ① 상세 화면 버튼 숨기기
좋아요·댓글·북마크·공유는 개별로 끌 수 있습니다. 우리 서비스에 없는 기능은 꺼두는 편이 혼란이 적습니다.
ShopLiveShortform.setVisibileDetailViews(options: ShopLiveShortformVisibleDetailData(
isBookMarkVisible: false,
isShareButtonVisible: true,
isCommentButtonVisible: false,
isLikeButtonVisible: true
))
ShopLiveShortform.setVisibleDetailTypeViews(
ShopLiveShortformVisibleDetailTypeData(
isBookmarkVisible = false,
isShareButtonVisible = true,
isCommentButtonVisible = false,
isLikeButtonVisible = true
)
)
play · showPreview를 호출하기 전에 지정해야
그 화면부터 반영됩니다.방법 ② 영상 채움 방식
ShopLiveShortform.setResizeMode(mode: .CENTER_CROP) // .FIT | .AUTO
ShopLiveShortform.setResizeMode(ShopLiveResizeMode.CENTER_CROP) // FIT | AUTO
| 값 | 동작 | 쓰는 곳 |
|---|---|---|
CENTER_CROP | 화면을 꽉 채우고 남는 부분은 잘라냄 | 세로 영상만 올라오는 서비스 |
FIT | 영상 전체가 보이도록 맞춤 (여백 생김) | 가로/정방형 영상이 섞이는 서비스 |
AUTO | 영상 비율에 따라 SDK가 선택 | 기본값 · 비율이 뒤섞일 때 |
방법 ③ 컬렉션 카드 표시 항목
builder.setVisibleTitle(isVisisble: true)
builder.setVisibleDescription(isVisible: false)
builder.setVisibleBrand(isVisible: true)
builder.setVisibleProductCount(isVisible: true)
builder.setVisibleViewCount(isVisible: false)
builder.setCellSpacing(spacing: 8)
builder.setCellCornerRadius(radius: 16)
builder.setCellBackgroundColor(color: .white)
builder.setCardViewType(type: .type2)
builder.reloadItems() // 변경 반영
with(binding.shortsCardTypeView) {
setVisibleTitle(true)
setVisibleDescription(false)
setVisibleBrand(true)
setVisibleProductCount(true)
setVisibleViewCount(false)
setRadius(16f.toPx(context))
setCardBackgroundColor(Color.WHITE)
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE2)
submit() // 변경 반영
}
방법 ④ 플랫폼 전용 옵션
| 옵션 | 플랫폼 | 설명 |
|---|---|---|
setSystemBarColor(data) | Android | 상세 화면의 상태바·내비게이션바 색을 앱 테마에 맞춤 |
setSoftInputMode(mode) | Android | 댓글 입력 시 키보드 동작 (ADJUST_RESIZE 기본) |
setMaxCount(n) | Android (컬렉션) | 컬렉션에 보여줄 최대 개수 제한 |
setEnableResumeOnForeGround(enable:) | iOS | 앱이 포그라운드로 돌아올 때 자동 재생 재개 여부 |
avAudioSessionCategoryOptions | iOS (빌더) | 다른 앱 오디오와의 믹싱 정책 |
setPlayOnlyWifi(true) | 공통 | Wi-Fi에서만 컬렉션 자동재생 — 데이터 민감 사용자 배려 |
완료 확인
- 끈 버튼이 상세 화면에 보이지 않는다
- 영상이 의도한 방식(꽉 채움 / 전체 보임)으로 표시된다
- 카드에서 끈 항목이 사라지고, 켠 항목만 남는다
- Android에서 상태바 색이 앱 테마와 어긋나지 않는다
이전 ← 미션 6다음 → 부록 · 부가 기능
부가 기능
미션에 들어가지 않는, 필요할 때만 쓰는 기능들입니다.
숏폼 컬렉션 데이터만 받아오기 (Android)
화면을 띄우지 않고 데이터만 받아 앱이 직접 그리거나, 가로형 캐러셀 여러 개를 한 번에 채울 때 씁니다.
ShopLiveShortform.get(context, collectionData, object : ShopLiveShortformCollectionListener {
override fun onData(data: ShopLiveShortformCollectionResponse) {
binding.shortsHorizontalTypeView.submit(collectionData, data)
}
override fun onError(e: Exception) { /* … */ }
})
// iOS 는 동등한 공개 API 가 없습니다.
// 목록만 필요한 경우 고객사 서버에서 Shoplive Open API 로 조회한 뒤
// 미션 6(내 목록으로 재생)의 ID 방식으로 넘기는 구성을 권장합니다.
get()은 LifecycleOwner가 필요합니다
내부적으로 lifecycleScope를 쓰기 때문에 Activity/Fragment 컨텍스트여야 하고,
아니면 조용히 아무 일도 일어나지 않습니다. applicationContext를 넘기지 마세요.영상 캐시 제어 (iOS)
숏폼은 재생한 영상을 디스크에 캐시합니다. 오래 쓰는 앱에서는 이 용량이 쌓이므로, 설정 화면의 "캐시 지우기" 같은 동선이 있다면 함께 비워 주세요.
// 지금 쌓인 캐시를 비웁니다
AVAssetDownloadManager.shared.deleteCaches()
// 앱 종료 시 자동으로 비우도록 설정 (앱 시작 시 1회)
AVAssetDownloadManager.shared.enableDeleteCacheOnTerminate(isEnabled: true)
// Android 는 동등한 공개 캐시 제어 API 가 없습니다.
// 재생 엔진(media3 / ExoPlayer)의 캐시 정책을 따릅니다.
유입 트래킹 (referrer · UTM)
어느 화면에서 숏폼으로 들어왔는지 기록해 전환을 분석합니다.
ShopLiveShortformCollectionData · ShopLiveShortformPreviewData ·
ShopLiveShortformIdsData의 init은 referrer 인자를 생략해도 전역 값을 그 값(nil)으로 덮어씁니다.
setReferrer로 먼저 설정했더라도 이후 데이터 객체를 만들면 사라지므로,
데이터 객체를 쓰는 화면에서는 항상 init의 referrer:로 넘기세요.ShopLiveShortform.setReferrer(referrer: "home_banner") // 전역
// 또는 데이터 생성 시: ShopLiveShortformCollectionData(referrer: "home_banner", …)
ShopLiveCommon.setUtmSource(utmSource: "naver")
ShopLiveCommon.setUtmMedium(utmMedium: "cpc")
ShopLiveCommon.setUtmCampaign(utmCampaign: "spring_sale")
ShopLiveCommon.setUtmContent(utmContent: "banner_a")
val data = ShopLiveShortformCollectionData().apply { referrer = "home_banner" }
// 리스트 뷰: binding.shortsCardTypeView.setReferrer("home_tab")
ShopLiveCommon.setUtmSource("naver")
ShopLiveCommon.setUtmMedium("cpc")
ShopLiveCommon.setUtmCampaign("spring_sale")
ShopLiveCommon.setUtmContent("banner_a")
하이브리드(WebView) 연동
앱 안의 웹뷰에서 숏폼을 띄우는 구성입니다. 웹 페이지의 숏폼 호출을 네이티브 SDK가 받아 처리합니다.
// 웹뷰 생성 직후 연결
ShopLiveShortform.BridgeInterface.connect(myWebView)
// 숏폼 풀스크린 위에 앱 화면(예: 상품 상세)을 띄울 때
ShopLiveShortform.BridgeInterface.present(viewController: productDetailVC)
// 화면을 벗어날 때
ShopLiveShortform.BridgeInterface.disconnect()
// 웹뷰 생성 직후 브리지를 자바스크립트 인터페이스로 등록
val bridge = ShopLiveShortformBridgeInterface(myWebView, null)
myWebView.addJavascriptInterface(bridge, "ShopLiveBridgeInterface")
bridge.observe(activity) // ComponentActivity 수명주기에 맞춰 메시지 수신
// 화면을 벗어날 때
bridge.removeObserver()
present(viewController:)는 연결용이 아닙니다 (iOS)
연결은 connect · disconnect로 합니다. present(viewController:)는 넘긴 화면을 숏폼 풀스크린 위에 띄웁니다(overFullScreen).데이터 필드 사전
조회 조건 — 어떤 숏폼을 보여줄지
아래 조건들은 재생 · 컬렉션 · 프리뷰에서 모두 같은 의미로 쓰입니다.
| 필드 | 타입 | 의미 |
|---|---|---|
shortsId | String | 특정 숏폼을 첫 화면으로 지정 (딥링크 진입에 사용) |
shortsCollectionId | String | 콘솔에서 만든 컬렉션 단위로 조회 |
tags | String[] | 해시태그로 필터 |
tagSearchOperator | AND · OR | AND=모든 태그 만족 · OR=하나라도 (타입) |
brands | String[] | 브랜드 식별자로 필터 |
skus | String[] | 상품 SKU가 연결된 숏폼만 |
productId | String | 특정 상품과 연관된 숏폼 (주로 프리뷰) |
shuffle | Bool | 순서를 섞어 매번 다르게 노출 |
isMuted | Bool | 재생 시작 시 음소거 여부 |
referrer | String | 유입 지점 기록 (부록) |
tags와 brands를 함께 주면 둘 다 만족하는 숏폼만 나옵니다.
결과가 0건이면 조건을 하나씩 빼며 좁혀보세요.ShopLiveShortformCollectionData — 풀스크린 재생 · 컬렉션 조회
| 구분 | iOS | Android |
|---|---|---|
| 생성 | ShopLiveShortformCollectionData(…, delegate:) — init 인자 | ShopLiveShortformCollectionData().apply { … } — 프로퍼티 |
| 이벤트 수신 | delegate | handler |
| 조회 조건 | 위 조회 조건 표와 동일 | |
ShopLiveShortformPreviewData — 플로팅 프리뷰
조회 조건에 표시 옵션이 더해진 형태입니다.
| 옵션 | iOS | Android | 기본값 |
|---|---|---|---|
| 위치 | previewPosition | setPosition(…) | bottomRight |
| 크기 | previewScale | setWidth · setHeight | iOS 화면 폭 기준 자동 · Android 긴 변 180dp |
| 모서리 둥글기 | previewRadius | setRadius | 10 |
| 바깥 여백 | previewEdgeInset | setMargin{Top,Bottom,Left,Right} | 16 |
| 닫기 버튼 | useCloseButton | setUseCloseButton | true |
| 스와이프로 닫기 | enableSwipeOut | setEnabledSwipeOut | true |
| 최대 개수 | maxCount | maxCount | 제한 없음 |
| 탭 가로채기 | useCustomAction + clickEventCallBack | setUseCustomAction + setOnClickListener | false |
| 탭 시 햅틱 | — | setCanVibrated | — |
| 볼륨키 사용 | isEnabledVolumeKey | isEnabledVolumeKey | false |
| 자동 닫힘 | — | setIsAutoClosed | — |
| 전환 애니메이션 | — | setCanTransition | — |
setWidth · setHeight · setMargin… · setRadius에는 dp가 아니라 px 값을 넣습니다. 기본값(긴 변 180 · 여백 16 · 둥글기 10)은 dp 기준이라 기기 밀도에 맞춰 px로 바뀝니다.ShopLiveShortformIdsData — 내 목록으로 재생
| 필드 | 타입 | 의미 |
|---|---|---|
ids | ShopLiveShortformIdData[] | 재생할 숏폼 ID 목록 (순서 = 재생 순서) |
currentId | String | 첫 화면으로 띄울 숏폼 ID |
isMuted | Bool | 시작 시 음소거 (Android) |
referrer | String | 유입 지점 |
| ShopLiveShortformIdData / IdsMoreData | 의미 |
|---|---|
IdData.shortsId | 숏폼 ID |
IdData.payload | 앱이 실어 보내는 임의 데이터 (추천 사유·랭킹 등) |
IdsMoreData.ids | 이어붙일 다음 페이지 ID 목록 |
IdsMoreData.hasMore | false 면 SDK가 추가 요청을 멈춤 |
컬렉션 옵션
| 옵션 | iOS (빌더) | Android (뷰) |
|---|---|---|
| 카드 모양 | setCardViewType(type:) | setViewType(type) |
| 열 개수 | — | setSpanCount(n) |
| 스냅 스크롤 | enableSnap() / disableSnap() | enableSnap() / disableSnap() |
| 자동 재생 | enablePlayVideos() / disable… | enablePlayVideos() / disable… |
| Wi-Fi에서만 재생 | setPlayOnlyWifi(isEnabled:) | setPlayOnlyWifi(isEnabled) |
| 셀 간격 | setCellSpacing(spacing:) | 레이아웃/데코레이션 |
| 모서리 둥글기 | setCellCornerRadius(radius:) | setRadius(radius) |
| 배경색 | setCellBackgroundColor(color:) | setCardBackgroundColor(colorInt) |
| 표시 항목 | setVisibleTitle/Description/Brand/ProductCount/ViewCount | 동일 |
| 재생 대상 (가로형) | setPlayableType(type:) | setPlayableType(type) |
| 최대 개수 | — | setMaxCount(n) |
| 맨 위로 | scrollToTop() | scrollToTop(isSmooth) |
| 조건 반영 | submit() · reloadItems() | submit() |
상세 화면 노출 옵션
| 필드 | 기본값 | 설명 |
|---|---|---|
isLikeButtonVisible | true | 좋아요 — 인증 필요 |
isCommentButtonVisible | true | 댓글 — 인증 필요 |
isBookMarkVisible (Android: isBookmarkVisible) | true | 북마크 — 인증 필요 |
isShareButtonVisible | true | 공유 — 앱이 처리 |
사용자 정보 (ShopLiveCommonUser)
| 필드 | 필수 | 설명 |
|---|---|---|
userId | ✔ | 회원 식별자 — 원문 대신 해시값 권장 (미션 4) |
userName | 표시 이름 | |
age | 나이 | |
gender | iOS .male/.female/.netural · Android MALE/FEMALE/NEUTRAL | |
userScore | 등급·점수 (개인화에 활용) | |
custom | 앱이 추가로 싣는 임의 값 |
ProductData — 상품 콜백으로 받는 값
| 필드 | 설명 |
|---|---|
productId | Shoplive 내부 상품 ID |
customerProductId | 고객사 상품 ID — iOS 전용 (Android 콜백 모델에는 없음) |
sku | 재고 관리 코드 |
url | 콘솔에 등록된 상품 URL |
name · brand · descriptions | 표시용 정보 |
imageUrl | 상품 이미지 |
currency · originalPrice · discountPrice · discountRate | 가격 정보 |
showPrice | 가격 노출 여부 (콘솔 설정) |
stockStatus | 재고 상태 |
공유 메타데이터
| 필드 | 설명 |
|---|---|
shortsId · shortsSrn (Android srn) | 숏폼 식별자 — 딥링크에 실어 보낼 값 |
title · description | 공유 문구에 쓸 제목·설명 |
thumbnail | OG 이미지로 쓸 썸네일 URL |
타입 & 상태
카드 / 리스트 타입
| 타입 | iOS | Android |
|---|---|---|
| 카드 모양 | CardViewType.type0 / type1 / type2 | CardViewType.CARD_TYPE0 / 1 / 2 |
| 컬렉션 방향 | ListViewType.vertical / horizontal | 뷰 클래스로 구분 (Vertical / Horizontal) |
PlayableType — 컬렉션에서 무엇을 재생할지
| 값 | 동작 | 쓰는 곳 |
|---|---|---|
FIRST | 첫 번째 셀만 재생 (기본) | 배터리·데이터 절약 |
CENTER | 화면 가운데 셀을 재생 | 가로형 캐러셀 |
ALL | 보이는 셀을 모두 재생 | 임팩트 우선 · 성능 주의 |
PreviewPosition
| iOS | Android |
|---|---|
.topLeft · .topRight · .bottomLeft · .bottomRight · .default |
TOP_LEFT · TOP_RIGHT · BOTTOM_LEFT · BOTTOM_RIGHT |
.default는 bottomRight와 같습니다.ShopLiveResizeMode
| 값 | 설명 |
|---|---|
CENTER_CROP | 꽉 채우고 넘치는 부분은 잘라냄 |
FIT | 영상 전체가 보이도록 맞춤 |
AUTO | 영상 비율에 따라 자동 선택 (기본) |
TagSearchOperator
| 값 | 의미 |
|---|---|
AND | 지정한 태그를 모두 가진 숏폼 |
OR | 태그 중 하나라도 가진 숏폼 |
이벤트 커맨드
미션 5 의 onEvent로 전달되는 값입니다. 이 목록은 확장될 수 있으므로
아는 값만 처리하고 나머지는 무시하도록 작성하세요.
| 커맨드 | 시점 |
|---|---|
VIDEO_MUTED / VIDEO_UNMUTED | 음소거 토글 |
DETAIL_ON_PLAYER_SHOWN (iOS)DETAIL_ON_PLAYER_SHOW (Android) | 상세(풀스크린) 진입 — 플랫폼마다 문자열이 다릅니다(Android는 끝의 N 없음) |
DETAIL_EMPTY | 조회 결과 0건 — 조건이 너무 좁거나 콘솔에 해당 숏폼이 없음 |
DETAIL_ON_PLAYER_DISMISS | 상세 이탈 |
COLLECTION_SHOW | 컬렉션 노출 |
COLLECTION_CLICK_ITEM | 컬렉션 카드 탭 |
PREVIEW_SHOWN / PREVIEW_HIDDEN | 프리뷰 표시 / 사라짐 |
PREVIEW_CLICK_SHOW | 프리뷰를 탭해 상세로 확대 |
PREVIEW_CLICK_CLOSE | 프리뷰 닫기 버튼 탭 |
String?), Android는 Map(Map<String, Any?>)으로 옵니다.
공통 로직을 두려면 앱에서 한쪽 형태로 정규화하세요.에러 처리
에러는 onError (델리게이트/핸들러) 와 컬렉션 전용 onListViewError (iOS)로 전달됩니다.
두 플랫폼 모두 같은 코드 체계를 씁니다.
코드 대역
| 대역 | 의미 |
|---|---|
| 9000번대 | 초기화·설정 문제 — 앱이 고쳐야 함 |
| 9300번대 | 인코딩 — 주로 에디터 SDK |
| 9500번대 | 미지원 미디어·기능 |
| 9900번대 | 네트워크·파싱 |
| 10000 | 분류되지 않은 예외 |
자주 만나는 에러
| 코드 | 이름 | 원인 · 조치 |
|---|---|---|
9000 | NOT_INITIALIZED_ACCESS_KEY | setAccessKey 전에 SDK를 호출함 → Application.onCreate/AppDelegate로 이동 |
9001 | NOT_INITIALIZED_SHARE_URL | 콘솔에 공유 URL이 설정되지 않음 → Shoplive 어드민 확인 |
9002 | NOT_INITIALIZED_CONFIG | 숏폼 설정을 아직 못 받음 → 네트워크 확인 후 재시도 |
9050 | REQUIRED_COMPONENT_ACTIVITY | (Android) ComponentActivity가 아닌 컨텍스트를 넘김 → applicationContext 대신 액티비티 전달 |
9310 | REMOVED_VIDEO | 삭제·비공개된 숏폼을 열려 함 → 딥링크의 shortsId 유효성 확인 |
9501 | UNSUPPORTED_MEDIA | 기기가 지원하지 않는 코덱 |
9900 | FAILED_NETWORK | 네트워크 실패 → 재시도 UI 제공 |
9901 | FAILED_JSON_PARSING | 응답 파싱 실패 → SDK/서버 버전 불일치 의심 |
9902 | FAILED_FETCH_DATA | (Android) 데이터 조회 실패 → 조회 조건·권한 확인 |
10000 | UNEXPECTED_ERROR | 분류되지 않은 예외 → 로그와 함께 문의 |
func onError(error: Error) {
guard let e = error as? ShopLiveCommonError else { return }
log("shortform error \(e.codes) \(e.message ?? "")")
// 9900 대역이면 재시도 UI, 9000 대역이면 개발 중 assert 권장
}
override fun onError(context: Context, error: ShopLiveCommonError) {
log("shortform error ${error.code} ${error.message}")
when (error.code) {
ShopLiveCommonErrorCode.FAILED_NETWORK -> showRetry()
ShopLiveCommonErrorCode.NOT_INITIALIZED_ACCESS_KEY -> check(false) { "accessKey 미설정" }
else -> Unit
}
}
onError를 비워두면 컬렉션이 조용히 비어 보일 뿐 원인을 알 수 없습니다.
최소한 로그로 남기고, 개발 빌드에서는 눈에 띄게(토스트 등) 표시하세요.고객사 직접 설정 항목
코드가 아니라 Shoplive 어드민·백엔드에서 준비해야 동작하는 것들입니다. 여기가 비어 있으면 코드가 맞아도 화면이 비어 보입니다.
| 항목 | 어디서 | 용도 |
|---|---|---|
| accessKey | Shoplive 발급 | 모든 호출의 전제 (빠른 시작) |
| 숏폼 콘텐츠 등록 | Shoplive 어드민 | 영상·썸네일·제목이 없으면 컬렉션이 0건 |
| 태그 · 브랜드 · 그룹 | Shoplive 어드민 | 앱의 조회 조건과 문자열이 정확히 맞아야 함 |
| 상품 연결 (SKU · 상품 URL) | Shoplive 어드민 | 상품 카드 노출 및 라우팅 근거 |
| 공유 URL | Shoplive 어드민 | 미설정 시 9001 |
| JWT 시크릿 | Shoplive 담당자 → 고객사 백엔드 | 사용자 정보 연결. 앱에 넣지 말 것 |
| 딥링크 도메인 | 고객사 | 공유 링크를 앱으로 되돌리기 위해 |
봄신상으로 등록해 두고 앱에서 봄 신상으로 조회하면 결과가 0건입니다.
상수로 묶어 양쪽이 같은 값을 쓰게 하세요.트러블슈팅 & FAQ
컬렉션이 비어 있어요
순서대로 확인하세요. ① submit()을 호출했는가 (미션 2) ② 조회 조건이 너무 좁지 않은가 —
태그·브랜드·SKU는 AND로 겹칩니다 ③ 콘솔에 해당 조건의 숏폼이 실제로 있는가 (설정 항목)
④ onError에 무엇이 찍히는가.
영상이 재생되지 않고 썸네일만 보여요 (Android)
재생 엔진 모듈(shoplive-media3 또는 shoplive-exoplayer)이 앱 의존성에 없을 가능성이 큽니다.
숏폼 모듈은 엔진을 compileOnly로 참조하므로 앱에서 직접 선언해야 합니다 (설치).
좋아요를 눌러도 다음에 들어가면 풀려 있어요
사용자 식별이 없는 상태(비로그인)입니다. setAuthToken 또는 setUser 를
숏폼 화면 진입 전에 호출하세요 (미션 4).
공유 시트가 숏폼 화면 뒤에 가려요 (iOS)
풀스크린 숏폼은 SDK 전용 UIWindow 위에 뜹니다.
ShopLiveShortform.getCurrentKeyWindow()가 돌려주는 윈도우 기준으로 present 하세요 (미션 5).
다른 화면으로 갔는데 프리뷰가 계속 떠 있어요
프리뷰는 앱 위에 떠 있는 별도 레이어라 화면 전환으로 자동으로 사라지지 않습니다.
화면을 벗어날 때 Android hidePreview(), iOS close()를 호출하세요 (미션 3).
컬렉션을 끝까지 내려도 다음 페이지가 계속 요청돼요
마지막 페이지에서 hasMore를 false로 내려주지 않았습니다 (미션 6).
다른 탭으로 가도 숏폼 영상 소리가 계속 나요
컬렉션 자동재생이 켜진 채 화면을 벗어난 경우입니다. onPause/viewDidDisappear에서
disablePlayVideos()를 호출하세요 (미션 2).
앱에서 숏폼을 만들어 올리게 하고 싶어요
이 SDK의 범위가 아닙니다. 숏폼 에디터 SDK 가이드를 참고하세요.
라이브(Player) SDK와 같이 써도 되나요?
됩니다. accessKey와 사용자 인증은 공통 모듈에서 한 번만 설정하면 두 SDK가 함께 씁니다.
단, 두 SDK의 버전대를 맞춰 받으세요.