스킨 UI 확장하기
기본 스킨에 메뉴 버튼을 더하고, 대화상자로 입력을 받고, 대화 위에 영역을 붙이고, 해제 함수로 정리하는 흐름을 하나의 예제로 따라 해요.
마지막 수정 2026년 10월 17일
스킨 UI API(room.skin)로 기본 스킨(basic-v2) 안에 버튼·대화상자·영역을 더할 수 있어요. 이 문서는 "관리자가 버튼을 눌러 대화 위에 배너를 띄우는" 기능을 만들며 흐름을 설명해요. 함수와 옵션 전체는 스킨 UI API에 있어요.
원칙
- 스킨이 자리·순서·겹침을 정하고, 내 코드는 스킨이 건네 준 빈 영역(
host) 안만 다뤄요. 스킨 내부 DOM을 찾아 고치지 마세요. - 등록할 때 주는
id는 종류마다 겹치면 안 돼요. 겹치면 오류가 나요. - 등록 함수는 해제 함수를 돌려줘요. 해제 함수는 여러 번 불러도 안전해요.
- 이 API는 기본 스킨에만 있어요. 레거시 스킨(
basic)은 레거시 스킨 API를 써요.
1. 준비 시점 잡기
스킨 UI API는 채팅방 화면이 준비된 뒤에 쓸 수 있으므로 after.create 안에서 등록해요. 미리 등록 코드는 비동기 로딩과 여러 채팅방을 보세요.
U.chat('my-room').on('after.create', function (room) {
var ui = room.skin;
if (!ui.menu) return; // 기본 스킨이 아니면 그만둠
// 여기서 등록
});
2. 메뉴 버튼 더하기
ui.menu.add는 입력창 도구 모음에 버튼을 더해요. condition이 false를 돌려주면 버튼이 숨겨져요.
var removeMenu = ui.menu.add({
id: 'my-banner',
label: '배너',
title: '대화 위에 배너 띄우기',
condition: function (ctx) { return ctx.room.my.auth >= 2; }, // 부관리자 이상만
onSelect: function (ctx) { askBanner(); }
});
닉네임을 눌렀을 때 나오는 메뉴에는 ui.userMenu.add로 같은 모양의 항목을 더해요. 이때 ctx.target이 누른 사람의 사용자 객체예요. 메뉴를 숨겨도 권한 검사는 서버가 하므로, 숨기기는 화면 정리용으로만 생각하세요.
3. 대화상자로 입력 받기
ui.dialog.*는 open을 빼고 모두 Promise를 돌려줘요. prompt는 확인하면 입력한 문자열, 취소하면 null을 줘요.
function askBanner() {
ui.dialog.prompt('배너', '띄울 문구를 적어 주세요.').then(function (text) {
if (text) showBanner(text);
});
}
alert, confirm(취소하면 false), password, select도 같은 방식이에요. 도중에 닫아야 하면 ui.dialog.open으로 close가 있는 손잡이를 받아요. → 스킨 UI API
4. 영역 붙이기
ui.mount.*에 mount(host, ctx) 함수를 주면 스킨이 그 자리에 빈 영역을 만들고 host로 건네 줘요. mount가 함수를 돌려주면 영역이 없어질 때 한 번 불려요.
var removeBanner = null;
function showBanner(text) {
if (removeBanner) removeBanner(); // 이전 배너 정리
removeBanner = ui.mount.messageList({
id: 'my-banner-area',
position: 'before', // 대화 영역 위
mount: function (host, ctx) {
var doc = host.ownerDocument;
var box = doc.createElement('div');
box.style.padding = '8px 12px';
box.textContent = text;
var close = doc.createElement('button');
close.type = 'button';
close.textContent = '닫기';
close.addEventListener('click', function () { ctx.close(); });
box.appendChild(close);
host.appendChild(box);
return function () { host.replaceChildren(); };
}
});
}
host는 채팅방 iframe 안의 요소예요. 새 요소는host.ownerDocument로 만들어요.- 글자는
textContent로 넣어요. 사용자 입력을innerHTML로 넣지 마세요. ctx.close()는 이 등록을 해제해요.ctx.signal은 정리 직전에 abort되므로fetch나 이벤트 리스너 정리에 쓸 수 있어요.
messageList 외에 header, middle, userList, footer, wrap, composer, headerTitle, toolbar 자리와, 레이아웃을 밀지 않고 겹치는 overlay가 있어요. 자리별 position 값과 overlay의 layer·scope는 스킨 UI API를 보세요.
5. 해제 함수로 정리
등록한 것은 돌려받은 해제 함수로 지워요. 채팅방이 닫히거나 다시 만들어지면 스킨이 등록을 모두 정리하므로, 직접 정리할 일은 기능을 끄거나 바꿀 때예요.
function disableBanner() {
if (removeBanner) removeBanner();
removeMenu();
}
이미 해제된 것을 다시 해제해도 오류가 나지 않아요. 같은 id로 다시 등록하려면 먼저 해제해야 해요.
room.reload()나 태그 재삽입으로 채팅방이 다시 만들어지면 채팅방 아이디로 건 이벤트도 함께 지워져 after.create가 다시 오지 않아요. 다시 만들어질 때도 등록되게 하려면 U.chat('*')로 걸고 room.id를 확인하세요. → 비동기 로딩과 여러 채팅방
콜백은 동기 함수로
메뉴의 condition, describe, resolver의 resolve, message.extend의 match는 스킨이 화면을 그리는 도중에 불러요. 이 콜백들은 바로 값을 돌려주는 동기 함수여야 해요.
condition,describe,resolve,match가 Promise를 돌려주면 오류로 처리되어 그 결과가 무시돼요(메뉴 항목이면 보이지 않아요).async function으로 쓰지 마세요.- 서버에서 값을 받아야 하면 미리 받아 변수에 두고, 값이 바뀌면
ui.menu.resolver.refresh()처럼 다시 계산을 요청해요. onSelect의 반환값은 쓰지 않아요. 버튼을 누른 뒤의 비동기 작업은onSelect안에서 시작만 하면 돼요(위askBanner처럼).
전체 코드
<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
<u-chat room="my-room" style="display:inline-block; width:100%; max-width:500px; height:500px;"></u-chat>
<script>
U.chat('my-room').on('after.create', function (room) {
var ui = room.skin;
if (!ui.menu) return;
var removeBanner = null;
function showBanner(text) {
if (removeBanner) removeBanner();
removeBanner = ui.mount.messageList({
id: 'my-banner-area',
position: 'before',
mount: function (host, ctx) {
var doc = host.ownerDocument;
var box = doc.createElement('div');
box.style.padding = '8px 12px';
box.textContent = text;
var close = doc.createElement('button');
close.type = 'button';
close.textContent = '닫기';
close.addEventListener('click', function () { ctx.close(); });
box.appendChild(close);
host.appendChild(box);
return function () { host.replaceChildren(); };
}
});
}
ui.menu.add({
id: 'my-banner',
label: '배너',
title: '대화 위에 배너 띄우기',
condition: function (ctx) { return ctx.room.my.auth >= 2; },
onSelect: function () {
ui.dialog.prompt('배너', '띄울 문구를 적어 주세요.').then(function (text) {
if (text) showBanner(text);
});
}
});
});
</script>
배너는 버튼을 누른 사람의 화면에만 보여요. 모두에게 띄우려면 채널로 문구를 보내고 받는 쪽에서 showBanner를 부르세요. → 실시간 데이터 주고받기
봐도 잘 이해가 되지 않나요? 문의 게시판에 질문을 남겨 주세요.