스킨 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는 이미 등록된 항목만 다뤄요. 숨기기, 순서 바꾸기, 표시 값 바꾸기, 비활성으로 만들기를 할 수 있고 새 항목을 만들거나 비활성 항목을 켤 수는 없어요.

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('안내', '로그인한 회원만 채팅할 수 있습니다.'); },
});

봐도 잘 이해가 되지 않나요? 문의 게시판에 질문을 남겨 주세요.

상호 UCHAT · 대표 김경욱 · 사업자등록번호 141-05-32832
사업장소재지 경기도 파주시 와석순환로 61 · 통신판매업신고번호 2013-경기파주-4485호
대표번호 010-8677-8633 · 이메일 support@uchat.io

ⓒ 2013–2026 UCHAT. All rights reserved.