컴포넌트/아바타, 페르소나

AvatarGroup (아바타그룹)

VanillaFront 2026. 9. 20. 16:21

Va.AvatarGroup — 아바타 그룹 컨테이너

여러 아바타를 나란히 또는 겹쳐 표시하고, 지정 개수 초과 시 자동으로 +N 팝업으로 접어주는 컨테이너입니다. 협업 UI에서 프로젝트 참여자, 채팅 참가자, 팀 멤버 목록 등을 콤팩트하게 표시할 때 사용됩니다.

 

클래스 정보

Va.AvatarGroup extends Va.Component
tagName: 'avatarGroup' (실제 렌더링은 <div>)
isContainer: true
파일: va_avatar.js (Va.Avatar와 같은 모듈)

기본 사용법

{
    tagName: 'avatarGroup',
    stackSize: 4,
    layout: 'stack',
    tags: [
        { tagName: 'avatar', image: user1.photo },
        { tagName: 'avatar', image: user2.photo },
        { tagName: 'avatar', image: user3.photo },
        { tagName: 'avatar', image: user4.photo },
        { tagName: 'avatar', image: user5.photo },  // 5명째부터 '+N' 팝업
        { tagName: 'avatar', image: user6.photo }
    ]
}

또는 코드로:

let group = new Va.AvatarGroup({ stackSize: 4, layout: 'stack' });
users.forEach(user => {
    group.append(new Va.Avatar({ image: user.photo }));
});
parent.append(group);

핵심 동작 — 자동 오버플로우

stackSize를 초과하는 아바타는 자동으로 팝업으로 이동:

[👤][👤][👤][👤][+3]   ← stackSize: 4, 총 7명일 때
                    ↓ 클릭
                  +3 팝업:
                  [👤]
                  [👤]
                  [👤]
  • 처음 stackSize(기본 4)개는 화면에 나란히 표시
  • 나머지는 "+N" 오버플로우 아바타로 접힘
  • "+N" 클릭 → 팝오버로 나머지 표시
  • 화면 밖 클릭 시 팝오버 자동 닫힘

속성

속성타입기본값설명

stackSize Number 4 화면에 표시할 최대 개수 (초과분은 팝업으로)
layout String 'spread' 'spread'(펼침) 또는 'stack'(겹침)
popWidth Number 오버플로우 팝업 너비
gap String 아바타 사이 간격
stopPropagation Boolean true 이벤트 전파 차단
size String 자식 아바타에 자동 전파되는 크기

layout 두 가지 모드

'spread' (기본) — 나란히 배치

[👤] [👤] [👤] [👤] [+3]
  • 아바타끼리 간격을 두고 나란히
  • 이름이 옆에 붙는 리스트에 적합

'stack' — 겹치게 배치

[👤][👤][👤][👤][+3]
   (뒷 것이 앞을 살짝 덮음)
  • 아바타끼리 겹쳐서 콤팩트한 표시
  • 좁은 공간에 여러 명 표시 (참여자 요약 등)
  • Slack, Notion 같은 협업 툴의 표준 패턴

이벤트

이벤트발생 시점콜백 인자

expand "+N" 팝업 열림 (sender, element, evt)
collapse "+N" 팝업 닫힘 (sender, element, evt)

각 자식 아바타의 이벤트는 아바타 자체에 개별적으로 바인딩합니다.

메서드

  • append(avatar) — 아바타 추가 (초과 시 자동으로 팝업으로)
  • add(avatar)  append의 별칭
  • arrange() — 화면/팝업 사이 아바타 재정렬 (제거 후 균형 맞춤)
  • removeKey(key) — key로 아바타 찾아 제거 후 자동 arrange

내부 구조

<div class="va-avatar-group stack">
    <span class="content" elname="content">
        <!-- 처음 stackSize개 아바타 -->
        <span class="va-avatar circular">...</span>
        <span class="va-avatar circular">...</span>
        ...
    </span>
    <span class="va-avatar" cpname="avatarPopOver">
        <!-- "+3" 오버플로우 트리거 -->
    </span>
    <div elname="pop" class="va-avatar-group-pop" style="display:none; position:absolute">
        <!-- 초과 아바타들 (팝업 열릴 때만 표시) -->
    </div>
</div>

사용 시점 — 언제 쓰나

  • 프로젝트 참여자 표시 — 협업 툴, 이슈 트래커
  • 채팅방 참가자 — 그룹 채팅 헤더
  • 팀 멤버 요약 — 팀/부서 카드
  • 문서 편집자 목록 — 실시간 협업 문서
  • 최근 방문자 — 페이지 조회자 표시

사용하지 말아야 할 때

  • 단일 사용자 표시  Va.Avatar 단독 사용
  • 아바타 + 이름/역할 조합 리스트  Va.Persona 반복
  • 정형적 사용자 리스트 (테이블)  Va.Grid

자주 쓰는 조합 예시

협업 프로젝트 참여자 (겹침 배치)

{
    tagName: 'avatarGroup',
    stackSize: 5,
    layout: 'stack',
    size: 'small',
    tags: project.members.map(member => ({
        tagName: 'avatar',
        image: member.photo,
        initial: member.initial,
        backgroundColor: member.color
    }))
}

채팅방 헤더

{
    tagName: 'div',
    layout: 'ds-flex fd-row ai-center gap-m',
    tags: [{
        tagName: 'avatarGroup',
        stackSize: 3,
        layout: 'stack',
        size: 'small',
        tags: room.participants.map(p => ({
            tagName: 'avatar',
            image: p.photo
        }))
    },{
        tagName: 'div',
        tags: [
            { tagName: 'title', innerHTML: room.name },
            { tagName: 'subTitle', innerHTML: `${room.participants.length}명 참여` }
        ]
    }]
}

팀 카드

{
    tagName: 'div',
    style: { padding: '15px', border: '1px solid var(--colorNeutralStroke)' },
    tags: [
        { tagName: 'title', innerHTML: '개발팀' },
        { tagName: 'subTitle', innerHTML: `${team.length}명` },
        {
            tagName: 'avatarGroup',
            stackSize: 6,
            layout: 'stack',
            size: 'medium',
            style: { marginTop: '10px' },
            tags: team.map(member => ({
                tagName: 'avatar',
                image: member.photo,
                initial: member.name.substring(0, 1),
                onClick: 'onClickMember'
            }))
        }
    ]
}

동적으로 참여자 추가/제거

config(){
    return {
        ref: 'refGroup',
        tagName: 'avatarGroup',
        stackSize: 4,
        layout: 'stack'
    };
}
addUser(user){
    let avatar = new Va.Avatar({
        key: user.id,
        image: user.photo
    });
    this.getRef('refGroup').append(avatar);
}
removeUser(userId){
    this.getRef('refGroup').removeKey(userId);
}

아바타 클릭 이벤트

{
    tagName: 'avatarGroup',
    stackSize: 4,
    layout: 'stack',
    onExpand: 'onExpandGroup',
    onCollapse: 'onCollapseGroup',
    tags: users.map(u => ({
        tagName: 'avatar',
        image: u.photo,
        key: u.id,
        onClick: 'onClickAvatar'
    }))
}

// View 안:
onClickAvatar(sender){
    console.log('아바타 클릭:', sender.key);
    this.showUserProfile(sender.key);
}
onExpandGroup(sender){
    console.log('오버플로우 팝업 열림');
}

실전 예시 — 이슈 트래커 카드

config(){
    return {
        tagName: 'section',
        layout: 'ds-flex fd-column gap-s',
        tags: this.issues.map(issue => ({
            tagName: 'div',
            layout: 'ds-flex fd-row ai-center gap-m',
            style: {
                padding: '15px',
                border: '1px solid var(--colorNeutralStroke)',
                borderRadius: 'var(--sizeRadiusM)'
            },
            tags: [{
                tagName: 'div',
                style: { flex: 1 },
                tags: [
                    { tagName: 'title', innerHTML: issue.title },
                    { tagName: 'subTitle', innerHTML: `#${issue.id} · ${issue.status}` }
                ]
            },{
                tagName: 'avatarGroup',
                stackSize: 3,
                layout: 'stack',
                size: 'small',
                tags: issue.assignees.map(user => ({
                    tagName: 'avatar',
                    image: user.photo,
                    initial: user.initial,
                    key: user.id
                }))
            }]
        }))
    };
}

주의사항

  • 자식은 반드시 Va.Avatar — 다른 컴포넌트를 넣으면 팝업/카운트 로직이 오작동
  • stackSize가 클수록 화면 폭 차지 — 반응형 UI에선 화면 크기별로 stackSize 조정 필요
  • layout: 'stack'의 겹침 마진은 CSS 통제 — 각 아바타에 style로 marginLeft override 시 룩 어긋날 수 있음
  • key 지정 권장  removeKey()로 특정 아바타 제거하려면 각 아바타에 key 필수
  • 팝업은 화면 위쪽에 나타남 — 아바타가 화면 상단에 붙어 있으면 잘림 가능성 있음
  • size 속성은 자식에 자동 전파 — 개별 아바타의 size를 명시하지 않은 경우만
  • 팝업 안 아바타도 크기/스타일 상속 — 별도 처리 불필요
  • arrange() 자동 호출 시점  removeKey() 후는 자동. append()는 자동 배치되지만, DOM 직접 조작 후엔 수동 arrange() 필요
  • autoHide 로직 사용 — 팝업 열려있을 때 다른 곳 클릭 시 자동 닫힘 (Va.autoHideEl 시스템 활용)

AvatarGroup vs Avatar 반복

AvatarGroup 사용:

  • 5명 이상 표시 시 오버플로우 처리 자동
  • Slack/Notion 스타일 겹침 배치 기본 지원
  • 팝업 로직 내장

Avatar 반복 (Div + Avatar들):

  • 완전 커스터마이징 필요할 때
  • 각 아바타에 이름 표시 등 추가 정보 있을 때 → Va.Persona 반복이 더 나음

대안 비교

상황추천

여러 사용자 콤팩트 표시 Va.AvatarGroup
개별 사용자 표시 Va.Avatar
아바타 + 이름/역할 리스트 Va.Persona 반복
정형 사용자 테이블 Va.Grid
태그/뱃지 그룹 Va.Tag 반복

참고

'컴포넌트 > 아바타, 페르소나' 카테고리의 다른 글

Persona (페르소나)  (0) 2026.09.20