스킨 UI API
기본 스킨(basic-v2)에 메뉴·대화상자·영역·꾸미기를 더하는 room.skin API의 전체 목록이에요.
마지막 수정 2026년 10월 19일
room.skin으로 기본 스킨(basic-v2) 화면에 버튼·메뉴·대화상자·영역을 더하고 닉네임·메시지 표시를 바꿔요. 이 문서는 정본 목록이에요. 처음이라면 스킨 UI 확장하기부터 보세요.
레거시 스킨(basic)에는 이 API가 없어요. 레거시 스킨은 레거시 스킨 API를 보세요.
공통 규칙
U.chat('*').on('after.create#my-ui', function (room) {
if (room.id !== 'my-room') return;
var ui = room.skin;
var remove = ui.menu.add({
id: 'hello',
label: '인사',
onSelect: function (ctx) { ctx.room.action.send('안녕하세요!'); },
});
// 필요 없어지면 remove();
});
- 등록 시점:
after.create안에서 등록해요. 채팅방을 다시 설치하면(room.reload(), 태그 재삽입) 스킨 등록이 모두 지워지고after.create가 다시 와요. 이때 그 방에만 건 이벤트(U.chat('my-room').on)도 지워지므로, 위 예처럼U.chat('*')에 걸고room.id로 고르면 다시 설치될 때마다 자동으로 다시 등록돼요. - 아이디: 등록마다
id가 필요하고, 같은 종류 안에서 겹치면 오류를 던져요. - 해제 함수:
add류 함수는 해제 함수를 돌려줘요. 처음 부르면true, 그 뒤로는false를 돌려주며 여러 번 불러도 안전해요. - 동기 콜백만:
condition,describe,resolve,match등 콜백은 동기 함수여야 해요. Promise를 돌려주면 오류로 처리되고 그 항목은 빠져요. 콜백 안의 예외는 콘솔에 기록되고 그 항목만 건너뛰어요.onSelect는 반환값을 보지 않으므로 비동기로 써도 되지만, 그 안의 예외는 직접 처리해요. - 표시만 바꿈: 메뉴를 숨기거나 꾸며도 권한은 바뀌지 않아요. 권한 검사는 서버가 해요.
- 채팅방이 파괴된 뒤
room.skin을 부르면 오류를 던져요.
메뉴
| API | 위치 |
|---|---|
ui.menu |
입력창 도구 모음의 버튼 |
ui.userMenu |
닉네임을 눌렀을 때 나오는 메뉴 |
| 함수 | 반환 | 설명 |
|---|---|---|
add(item) |
해제 함수 | 항목 추가 |
remove(id) |
true/false |
항목 제거. 없던 아이디면 false |
resolver.add({ id, resolve }) |
해제 함수 | 항목 목록 고치기. 아래 메뉴 resolver |
resolver.remove(id) |
true/false |
resolver 제거 |
resolver.refresh() |
— | 조건이 바뀌었을 때 목록을 다시 계산 |
항목 필드
| 키 | 형식 | 설명 |
|---|---|---|
id |
문자열 | 필수. 고유 아이디 |
label |
문자열 | 표시 글자(텍스트). 없으면 title, 그것도 없으면 id |
title |
문자열 | 마우스를 올렸을 때 설명 |
order |
숫자 | 작을수록 앞. 기본 0 |
group |
문자열 | userMenu: 같은 그룹 항목을 한 줄에 모아요 |
column |
0/1 |
userMenu: 같은 그룹 안에서 왼쪽(0)·오른쪽(1) 두 칸으로 나눠요 |
separator |
불리언 | userMenu: 구분선. 선택할 수 없어요 |
disabled |
불리언 | 비활성(보이지만 누를 수 없음) |
pressed |
불리언 | menu: 눌림 상태로 표시(켜기·끄기 버튼) |
padding |
'none'/'normal' |
menu: 버튼 안쪽 여백. 기본 'normal' |
condition(ctx) |
함수 | false를 돌려주면 숨김 |
describe(ctx) |
함수 | 그때그때 바뀌는 필드(label, disabled, pressed 등)를 객체로 돌려줘요. null이면 숨김 |
onSelect(ctx) |
함수 | 눌렀을 때 |
콜백의 ctx
| API | ctx |
|---|---|
ui.menu |
{ room, skin }, onSelect에서는 event도 |
ui.userMenu |
{ room, skin, target, messageUid, message, event } — target은 누른 사람의 사용자 객체, 메시지의 닉네임을 눌렀으면 messageUid와 message가 있어요 |
ui.userMenu.add({
id: 'profile',
label: '프로필 보기',
condition: function (ctx) { return !!ctx.target.id; },
onSelect: function (ctx) {
window.open('/profile/' + encodeURIComponent(ctx.target.id));
},
});
기본 항목
| API | 아이디(order) |
|---|---|
ui.menu |
skin.bold(-60), skin.italic(-50), skin.underline(-40), skin.color(-30), skin.emoticon(-20), skin.upload(-10), skin.brand(100), skin.scroll(110) |
ui.userMenu |
skin.whisper, skin.mute, skin.ban처럼 skin.으로 시작해요. order는 -1000부터 10씩 |
메뉴 resolver
resolver는 이미 등록된 항목만 다뤄요. 숨기기, 순서 바꾸기, 표시 값 바꾸기, 비활성으로 만들기를 할 수 있고 새 항목을 만들거나 비활성 항목을 켤 수는 없어요.
ui.userMenu.resolver.add({
id: 'hide-ignore',
resolve: function (ctx) {
return ctx.current.filter(function (item) { return item.id !== 'skin.ignore'; });
},
});
| 항목 | 설명 |
|---|---|
resolve(ctx) |
ctx는 메뉴 ctx에 base(resolver 적용 전 목록), current(앞 resolver까지 적용한 목록)를 더한 것. 배열을 돌려주면 그 목록으로 바꾸고, null이면 그대로 둬요 |
| 돌려준 배열 | 아이디가 없는 항목, 등록되지 않은 아이디, 겹치는 아이디는 버려요. order로 다시 정렬해요 |
대화상자
| 함수 | 반환(Promise 결과) |
|---|---|
ui.dialog.alert(title, content) |
확인 true, 닫기 null |
ui.dialog.confirm(title, content) |
확인 true, 취소 false |
ui.dialog.prompt(title, content, value) |
입력한 문자열, 취소 null |
ui.dialog.password(title, content, value) |
입력한 문자열(가려서 입력), 취소 null |
ui.dialog.select(title, content, options, value) |
고른 값(키), 취소 null. options는 { 값: '표시 글자' } 객체 |
ui.dialog.open({ kind, title, content, options, value }) |
{ id, result, close } |
ui.dialog.confirm('도전', '가위바위보 할까요?').then(function (ok) {
if (ok) room.action.send('도전!');
});
var handle = ui.dialog.open({
kind: 'confirm',
title: '도전',
content: '10초 안에 답해 주세요.',
options: { backdropDismiss: false },
});
handle.result.then(function (ok) { /* true, false, 또는 close로 준 값 */ });
setTimeout(function () { handle.close(null); }, 10000);
kind는alert,confirm,prompt,password,select중 하나예요. 다른 값은 오류를 던져요.title,content는 텍스트로 표시해요(HTML 아님).options.backdropDismiss: false이면 바깥을 눌러도 닫히지 않아요.close(value)는 그 값으로result를 끝내고true를 돌려줘요. 이미 닫혔으면false예요.- 대화상자는 한 번에 하나씩 보이고, 나머지는 순서대로 기다려요. 스킨 자체 대화상자가 떠 있으면 그것이 먼저 보여요. 채팅방이 파괴되면 기다리던 대화상자는 모두
null로 끝나요.
영역 붙이기
ui.mount.*에 mount(host, ctx)를 주면 스킨이 빈 요소(host)를 만들어 건네줘요. host 안만 다루고 바깥 DOM은 고치지 마세요.
ui.mount.messageList({
id: 'banner',
position: 'before',
mount: function (host, ctx) {
host.textContent = '오늘 8시에 이벤트가 있습니다!';
return function () { host.replaceChildren(); };
},
});
| 함수 | position |
자리 |
|---|---|---|
header |
before(기본), after |
머리 영역 앞뒤 |
headerTitle |
— | 채팅방 제목 자리 안 |
middle |
before(기본), after |
대화+사용자 목록 영역 앞뒤 |
messageList |
before(기본), after |
대화 영역 앞뒤 |
userList |
before(기본), after |
사용자 목록 앞뒤 |
composer |
above-input(기본, before-input도 같음), after-input |
입력창 위, 입력창 뒤 |
footer |
before(기본), after |
입력 영역 앞뒤 |
wrap |
before(기본), after, left, right |
채팅방 전체의 위·아래·왼쪽·오른쪽 |
toolbar |
— | 입력창 도구 모음 안(버튼 뒤) |
overlay |
— | 화면 위에 겹치기. 아래 겹치는 영역 |
모두 해제 함수를 돌려줘요. ui.mount.removeOverlay(id), ui.mount.removeToolbar(id)로 아이디로 지울 수도 있어요. 지원하지 않는 position은 오류를 던져요.
mount의 ctx
| 키 | 설명 |
|---|---|
room, skin |
room 객체, room.skin |
signal |
AbortSignal. 정리 직전에 abort돼요. addEventListener(..., { signal })에 넘기면 정리가 자동이에요 |
bounds() |
host의 getBoundingClientRect() 값 |
close() |
이 등록 전체를 지워요(해제 함수와 같음) |
requestLayout() |
host 크기를 바꾼 뒤 스킨에 다시 배치를 요청 |
slot |
(영역) 붙은 자리 이름. 예: messageList.before |
layer, scope |
(overlay) 겹침 층과 범위 |
message, author |
(message.extend) 그 메시지와 보낸 사람 |
mount가 함수 또는 { destroy } 객체를 돌려주면 host가 없어지거나 등록을 지울 때 한 번 불려요.
겹치는 영역
ui.mount.overlay({ id, layer, scope, exclusive, mount })는 레이아웃을 밀지 않고 위에 겹쳐요.
| 키 | 값 | 설명 |
|---|---|---|
layer |
background-effect |
뒤쪽 효과. 클릭이 통과해요 |
content-overlay(기본) |
내용 위 정보 표시 | |
modal |
맨 위에서 가리기 | |
scope |
skin(기본) |
채팅방 전체에 겹침 |
message-list |
대화 영역에만 겹침 | |
exclusive |
불리언 | layer: 'modal'과 함께 쓰면 이 영역이 떠 있는 동안 ui.dialog 대화상자를 띄우지 않고 기다리게 해요. scope가 skin일 때만 쓸 수 있고, 아니면 오류를 던져요 |
ui.mount.overlay({
id: 'player',
layer: 'content-overlay',
scope: 'message-list',
mount: function (host, ctx) {
host.textContent = '재생 중';
},
});
상단 공지
| 함수 | 반환 | 설명 |
|---|---|---|
ui.topLayer.add({ id, content, backgroundColor, color, dismissLabel, weight }) |
해제 함수 | 대화 영역 위쪽 공지 줄에 항목을 더해요 |
ui.topLayer.remove(id) |
true/false |
항목 제거 |
content는 필수이며 텍스트로 표시해요.weight가 클수록 위에 놓여요(기본 0).dismissLabel은 보는 사람이 공지를 닫는 버튼의 글자예요.
ui.topLayer.add({ id: 'event', content: '이벤트 진행 중!', backgroundColor: '#fff4cc', color: '#7a4b00' });
메시지에 붙이기
ui.message.extend({ id, position, match, mount })는 일반 메시지와 귓속말 줄마다 영역을 붙여요. 지난 메시지와 새 메시지 모두에 적용돼요.
| 키 | 설명 |
|---|---|
position |
before(줄 맨 앞, 닉네임 앞), inline(내용 바로 뒤), after(기본, inline 뒤) |
match(ctx) |
붙일 메시지를 골라요. ctx는 { message, author, room, skin }. 생략하면 모든 메시지 |
mount(host, ctx) |
mount의 ctx에 message, author가 더해져요 |
message의 주요 값은 kind('message' 또는 'whisper'), author(보낸 사람), content, uid, created, mine(내가 보냈으면 true)이에요. ctx.close()는 그 메시지 하나가 아니라 등록 전체를 지워요.
ui.message.extend({
id: 'admin-badge',
position: 'inline',
match: function (ctx) { return ctx.author && ctx.author.auth >= 2; },
mount: function (host) { host.textContent = ' [관리자]'; },
});
닉네임 꾸미기
ui.nickname.resolver.add({ id, resolve }), remove(id), refresh().
| 항목 | 설명 |
|---|---|
resolve(ctx) 의 ctx |
{ user, location, message, backgroundColor, room, skin, base, current }. location은 'message'(대화) 또는 'user-list'(사용자 목록), message는 대화일 때만 |
| 돌려줄 값 | { style, nickcon, iconsBefore, iconsAfter, addBefore, addAfter, tag, gradient, classes, vars } 중 바꿀 것만. null이면 건너뛰어요 |
style |
아래 허용 style |
gradient |
닉네임 글자를 왼쪽에서 오른쪽으로 칠하는 색 2~8개 배열(예: ['#e65c00', '#d4145a']). 허용 style의 색 형식만 받고, null이면 지워요. 칭호·아이콘에는 칠하지 않아요 |
classes |
닉네임 전체([data-nickname-display])에 붙일 클래스 이름 배열. 플러그인 CSS로 등록한 규칙을 거는 데 써요. 덧붙이기만 하고(앞 resolver의 클래스를 지우지 못해요) 최대 16개예요. 영문자·숫자·_·-로 된 이름만 받아요 |
vars |
닉네임 전체에 넣을 CSS 변수({ '--my-color': '#f00' }). 사람마다 다른 값을 CSS에 넘길 때 써요. 이름은 --로 시작하고, 값에 ; { }가 있으면 버려요. null이면 그 변수를 지워요 |
nickcon |
{ src, alt }(닉네임 대신 이미지) 또는 null(닉콘 지우기) |
iconsBefore, iconsAfter |
[{ src, alt }]. 닉네임 앞·뒤 아이콘 목록을 통째로 바꿔요. 더하려면 ctx.current.iconsAfter.concat(...) |
addBefore, addAfter |
닉네임 앞·뒤에 덧붙이는 조각 [{ text, image, color, background, border, title, classes }]. 한쪽 최대 6개, 글자는 16자까지. classes(최대 8개)로 플러그인 CSS의 모양을 걸 수 있어요 |
이미지 src는 https://, http://, //, /로 시작하는 주소를 써요. 공백이 든 주소나 형식이 맞지 않는 주소는 버려요.
ui.nickname.resolver.add({
id: 'admin-nick',
resolve: function (ctx) {
if (!ctx.user || ctx.user.auth < 2) return null;
return {
style: { fontWeight: 'bold', color: '#c2410c' },
iconsAfter: ctx.current.iconsAfter.concat({ src: 'https://example.com/star.png', alt: '관리자' }),
};
},
});
메시지 꾸미기
ui.message.resolver.add({ id, resolve }), remove(id), refresh(). 일반 메시지와 귓속말에만 적용돼요.
| 항목 | 설명 |
|---|---|
resolve(ctx) 의 ctx |
{ message, author, backgroundColor, room, skin, base, current }. message는 읽기 전용 |
| 돌려줄 값 | { style, row, classes, vars } 중 바꿀 것만. null이면 건너뛰어요 |
style |
내용 글자와 줄 배경. 아래 허용 style |
row.spacing |
'normal' 또는 'relaxed'(줄 여백을 넓힘) |
row.hideNickname |
true면 닉네임을 숨겨요. 일반 메시지에만 적용되고 귓속말은 숨기지 않아요 |
classes |
메시지 한 줄 전체에 붙일 클래스 이름 배열. 플러그인 CSS로 등록한 규칙을 걸어요. 덧붙이기만 하고 최대 16개예요. 내용 글자는 그 안의 .message-content예요 |
vars |
메시지 줄에 넣을 CSS 변수({ '--my-accent': '#f00' }). 규칙은 닉네임의 vars와 같아요 |
ui.message.resolver.add({
id: 'notice-highlight',
resolve: function (ctx) {
if (ctx.message.content.indexOf('공지') < 0) return null;
return { style: { backgroundColor: '#fff4cc' }, row: { spacing: 'relaxed' } };
},
});
resolver는 order(기본 0) 순서, 같으면 등록 순서로 적용되고 앞의 결과에 덧씌워요. 조건이 바뀌어 다시 계산해야 하면 refresh()를 부르세요.
허용 style
닉네임·메시지 resolver의 style은 아래 키만 받아요. 다른 키와 형식이 틀린 값은 조용히 버려요. 그 밖의 CSS는 플러그인 CSS로 등록하고 닉네임의 classes로 거세요.
| 키 | 허용 값 |
|---|---|
color, backgroundColor |
#rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), rgba(), hsl(), hsla() |
fontWeight |
normal, bold, 100~900(100 단위) |
fontStyle |
normal, italic, oblique |
textDecoration |
none, underline, overline, line-through |
플러그인 CSS
ui.style.add({ id, css })는 해제 함수를, ui.style.remove(id)는 true/false를 돌려줘요.
css글자를 그대로 채팅방 화면의<style>로 넣어요. 내용은 거르지 않아요. 플러그인이 꺼지거나 스킨이 정리되면 함께 빠져요.- 이 CSS는 그 채팅방 화면 전체에 적용돼요. 다른 부분을 건드리지 않게 클래스 이름 앞에 플러그인 이름을 붙이세요.
- 닉네임 글자는 항상
[data-nickname-label]안에 있어요. 그라데이션을 쓰면 같은 요소에[data-nickname-gradient]도 붙어요.
ui.style.add({
id: 'myPlugin.flow',
css: '.my-flow [data-nickname-gradient]{background-size:200% 100%;animation:my-flow 4s linear infinite}'
+ '@keyframes my-flow{to{background-position:-200% 0}}',
});
ui.nickname.resolver.add({
id: 'myPlugin.nick',
resolve: function (ctx) {
if (!ctx.user || ctx.user.auth < 2) return null;
return { gradient: ['#538eff', '#f61ecf', '#538eff'], classes: ['my-flow'] };
},
});
그 밖의 기능
| 함수 | 반환 | 설명 |
|---|---|---|
ui.composer.guard.add({ id, shouldBlock, onBlocked }) |
해제 함수 | 입력창 사용을 막아요. shouldBlock(ctx)가 참이면 막고 onBlocked(ctx)를 불러요 |
ui.composer.guard.remove(id) |
true/false |
가드 제거 |
ui.message.removeByUser(nick) |
지운 개수 | 그 닉네임의 메시지를 내 화면에서 지워요 |
ui.scrollToBottom(force) |
true/false |
대화 맨 아래로 이동. force가 없으면 사용자가 메시지의 닉네임이나 링크 위에 마우스를 올려 두었거나 자동 스크롤을 끄고 위를 보는 중일 때 움직이지 않고 false |
가드의 ctx는 { action, draft, event, room, skin }이고 action은 'send'(보내기) 또는 'activate'(입력창을 누르거나 포커스)예요.
ui.composer.guard.add({
id: 'login-first',
shouldBlock: function (ctx) { return ctx.room.my.auth < 1; },
onBlocked: function (ctx) { ctx.skin.dialog.alert('안내', '로그인한 회원만 채팅할 수 있습니다.'); },
});
봐도 잘 이해가 되지 않나요? 문의 게시판에 질문을 남겨 주세요.