레이아웃 시스템
VPE 2.0의 레이아웃 시스템은 JSON으로 컨트롤바, 버튼 배치, 영역 구성을 선언적으로 정의합니다. 화면 환경별(PC/모바일/전체화면)과 콘텐츠 유형별(VOD/Live)을 각각 분리해 관리할 수 있습니다.
인터랙티브 데모
아래 버튼으로 레이아웃을 전환하여 컨트롤바 구성의 차이를 확인해 보세요.
레이아웃 타입
layout prop은 다음 3가지 형태로 전달할 수 있습니다.
// 1) 단일 레이아웃 — 단순 케이스 (라이브/VOD/디바이스 분기 없이 하나의 컨트롤바 구성)
type ControlBarLayout = {
order?: string[];
top?: Group[];
upper?: Group[];
center?: Group[];
lower?: Group[];
bottom?: Group[];
};
// 2) 라이브 / VOD 분기
type ControlBarLayoutVariant = {
live: ControlBarLayout;
vod: ControlBarLayout;
};
// 3) 반응형 (PC / Mobile / Fullscreen 분기)
type ControlBarLayoutResponsive = {
pc: { live: ControlBarLayout; vod: ControlBarLayout };
mobile: { live: ControlBarLayout; vod: ControlBarLayout };
fullscreen: { live: ControlBarLayout; vod: ControlBarLayout };
breakpoint?: number; // 기본 768
};구성 구조
반응형 레이아웃의 최상위 키는 pc, mobile, fullscreen으로 나뉘며, 각 환경 내부에 vod와 live 레이아웃을 정의합니다.
{
"pc": {
"vod": { ... },
"live": { ... }
},
"mobile": {
"vod": { ... },
"live": { ... }
},
"fullscreen": {
"vod": { ... },
"live": { ... }
}
}섹션과 순서
order 배열이 렌더링 순서를 결정합니다. 기본 섹션은 top, upper, center, lower, bottom입니다.
{
"order": ["top", "upper", "center", "lower", "bottom"],
"top": [
{ "items": ["MetaDesc"] },
{ "wrapper": "Blank", "items": [], "align": "left" },
{ "items": ["ShareBtn"] }
],
"center": [
{ "items": ["BigPlayBtn"], "align": "center" }
],
"bottom": [
{ "items": ["ProgressBar"] },
{ "items": ["PlayBtn", "VolumeBtn", "TimeDisplay"], "align": "left" },
{ "items": ["SettingBtn", "PIPBtn", "FullscreenBtn"], "align": "right" }
]
}행(Row) 구성
각 섹션은 여러 개의 Row(=Group)로 구성됩니다. Row는 items 배열을 통해 버튼이나 컴포넌트를 배치하고, align과 wrapper로 정렬과 묶음 방식을 조정합니다.
| 키 | 타입 | 설명 |
|---|---|---|
| key | string | Group 식별자. 런타임 layout() 머지 시 같은 key의 Group끼리 교체됩니다. (권장) |
| items | (string | ReactNode)[] | Row에 배치할 컨트롤 목록 (빌트인 키 또는 커스텀 컴포넌트) |
| align | left | center | right | Row 내 컨텐츠 정렬 방식 |
| wrapper | "Group" | "Blank" | Row에 적용할 래퍼 타입 (Group=묶음, Blank=빈 공간/간격) |
| cap | number | Row 내 표시 아이템 수 제한 |
| noPadding | boolean | Group 좌우 패딩 제거 |
기본 레이아웃 예제
각 Group에 key를 지정하면 런타임 머지 시 특정 Group만 안전하게 교체할 수 있습니다.
const layout: ControlBarLayout = {
top: [],
center: [{ key: "bigPlayBtn", items: ["BigPlayBtn"] }],
bottom: [
{ key: "play", items: ["PlayBtn"], wrapper: "Group", noPadding: true },
{ key: "volume", items: ["VolumeBtn"], wrapper: "Group" },
{ key: "time", items: ["TimeBtn"], wrapper: "Group" },
{ key: "blank", wrapper: "Blank", items: [] },
{ key: "right", items: ["SubtitleBtn", "FullscreenBtn"], cap: 2, wrapper: "Group" },
],
};React 적용
레이아웃 JSON은 layout prop으로 전달합니다. React에서는 커스텀 컴포넌트를 함수 호출 형태로 주입할 수 있습니다.

"use client";
import Hls from "hls.js";
import { VpePlayer } from "@sgrsoft/vpe-react-sdk";
function Logo() {
return (
<div style={{ padding: "0 15px" }}>
<a href="https://www.ncloud.com/v2/product/media/videoPlayerEnhancement" target="_blank">
<img
src="https://player.vpe.naverncp.com/images/ncp-logo-white.webp"
style={{ height: 24 }}
alt="NCloud"
/>
</a>
</div>
);
}
const vpeLayout = {
pc: {
vod: {
order: ["top", "upper", "center", "seekbar", "lower", "bottom"],
top: [{ items: ["MetaDesc"] }],
upper: [],
center: [{ items: ["BigPlayBtn"], align: "center" }],
lower: [],
bottom: [
{ items: ["PlayBtn", "PrevBtn", "NextBtn"], wrapper: "Group" },
{ items: ["VolumeBtn"], wrapper: "Group" },
{ items: ["TimeBtn"], wrapper: "Group" },
{ wrapper: "Blank", items: [] },
{ items: [Logo()], wrapper: "Group" },
{ items: ["SubtitleBtn", "PipBtn", "SettingBtn", "FullscreenBtn"], cap: 2, wrapper: "Group" },
],
},
},
};
export default function PlayerPage() {
return (
<VpePlayer
hls={Hls}
accessKey="YOUR_ACCESS_KEY"
layout={vpeLayout}
options={{
playlist: [
{
file: "https://example.com/video/master.m3u8",
poster: "https://example.com/poster.jpg",
},
],
}}
/>
);
}레이아웃 실시간 변경 (ref)
playerRef.current?.layout(nextLayout, merge?)를 호출해 재생 중에도 레이아웃을 동적으로 변경할 수 있습니다. 두 번째 인자 merge의 기본값은 true입니다.
머지 규칙 (merge: true)
top/upper/center/lower/bottom/order중 next에 정의되지 않은 section은 base 유지- section 내 Group 머지는
key기반:- base와 next에 같은 key의 Group → next로 교체
- base에만 있는 key → base 유지
- next에만 있는 key → 결과 끝에 append
- next에 key 없는 Group → 결과 끝에 append (신규 추가로 간주)
- 양쪽 Group 모두 key가 없으면 → section 통째 교체 (하위 호환)
예시 — 특정 Group만 교체
// base (default pcLayout.vod)
// bottom: [
// { key: "play", items: ["PlayBtn"] },
// { key: "volume", items: ["VolumeBtn"] },
// { key: "time", items: ["TimeBtn"] },
// { key: "blank", wrapper: "Blank", items: [] },
// { key: "right", items: ["SubtitleBtn", "FullscreenBtn"] },
// ]
playerRef.current?.layout({
bottom: [{ key: "right", items: ["SubtitleBtn", "PipBtn", "SettingBtn", "FullscreenBtn"] }],
});
// 결과: play/volume/time/blank 유지, right만 교체예시 — 새 Group 추가
playerRef.current?.layout({
bottom: [{ key: "extra", items: ["ShareBtn"] }],
});
// 결과: 기존 5개 Group + 끝에 extra(ShareBtn) append예시 — section 전체 교체 (merge: false)
playerRef.current?.layout(
{ bottom: [{ items: ["PlayBtn"] }] },
false, // 통째로 교체
);UMD / HTML 적용
UMD 환경에서는 HTML 문자열을 그대로 레이아웃 아이템으로 전달할 수 있습니다.
const Logo = `
<div style="padding:0 15px;">
<a href="https://www.ncloud.com/v2/product/media/videoPlayerEnhancement" target="_blank">
<img src="https://player.vpe.naverncp.com/images/ncp-logo-white.webp" style="height:24px;"/>
</a>
</div>`;
const vpeLayout = {
pc: {
vod: {
order: ["top", "upper", "center", "seekbar", "lower", "bottom"],
top: [{ items: ["MetaDesc"] }],
upper: [],
center: [{ items: ["BigPlayBtn"], align: "center" }],
lower: [],
bottom: [
{ items: ["PlayBtn", "PrevBtn", "NextBtn"], wrapper: "Group" },
{ items: ["VolumeBtn"], wrapper: "Group" },
{ items: ["TimeBtn"], wrapper: "Group" },
{ wrapper: "Blank", items: [] },
{ items: [Logo], wrapper: "Group" },
{ items: ["SubtitleBtn", "PipBtn", "SettingBtn", "FullscreenBtn"], cap: 2, wrapper: "Group" },
],
},
},
};
window.vpeLayout = vpeLayout;UI Editor 활용
레이아웃 JSON은 UI Editor에서 시각적으로 편집하고 결과 JSON을 그대로 적용할 수 있습니다.