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

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