본문 바로가기

frond-end

웹페이지 로딩 지연 시 사용자 경험 개선을 위한 Skeleton UI 구현

반응형

웹 페이지에 접근할 때,  긴 로딩 시간을 해결할 가장 좋은 방법은 시간을 단축하는 것이지만 그럴 수 없는 경우  디자인적으로 개선 할 수 있습니다.
사용자 경험을 개선하기 위한 웹사이트와 애플리케이션에서 가장 일반적인 시스템 피드백 형태 중 하나는 대기 애니메이션 진행 표시기 입니다.

 

Designing for the appearance of speed

[참고: 원글] https://juneuprising.medium.com/designing-for-the-appearance-of-speed-aaabc7f568c2

 

Designing for the Appearance of Speed

Interface design for an impatient world

juneuprising.medium.com

"느린 사이트는 유저의 감정적 차원을 극적으로 줄였다" 

2013년 Luke Wroblewski 화면 경험에서 로딩 스피너를 사용하는 것의 단점에 대해 처음으로 논의했고, 그가 "스켈레톤 화면" 이라고 부르는 것을 지지했습니다.

 

 

 

같은 글의 저자 Bill Chung 은 아래와 같이 스켈레톤 디자인에 대해 연구를 하고 글을 작성하였습니다.

[참고] https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a

 

Everything you need to know about skeleton screens

How do we know that skeleton screens actually work?

uxdesign.cc

 

위의 글에서 말하는 "어느 스켈레톤 디자인이 로딩 시간이 더 짧게  느껴졌는가" 은 아래와 같이 말합니다.

  1. 정적 vs 애니메이션 스켈레톤 👉 애니메이션 스켈레톤
  2. 펄싱 애니메시연 vs 웨이브 애니메이션 👉 웨이브 애니메이션
  3. 빠른 파동 vs 느리고 안정적인 파동  👉 느리고 안정적인 파동
  4. 왼쪽에서 오른쪽으로의 파동 애니매이션 vs 오른쪽에서 왼쪽으로의 파동 애니메이션  👉 왼쪽에서 오른쪽으로의 파동 애니매이션

 

 

 

스켈레톤 화면 이란?

https://primevue.org/skeleton/

 

https://primevue.org/skeleton/

 

스켈레톤 화면은 화면을 로딩하는 시간 동안 유저에게 나타나는 와이어프레임 형태의 화면입니다.

페이스북, 링크드인, 구글 드라이브, 유튜브 등에서 사용하는 것을 볼 수 있는데요.

로딩 중인 화면을 스켈레톤 말고도 스피너, 자체 디자인 된 로딩 애미메이션 등등으로 사용하는 것을 봤을 텐데요.

 

대부분 많이 사용하는 스피너UI 와 스켈레톤UI 가 어느 경우에 사용해야 하는지, 각각의 장단점은 무엇인지 확실히 알고 쓰임새에 맞게 사용하면 더욱 사용자경험 개선에 좋을 것 같아 공부해봤습니다.

 

 

 

 

 

스피너(Spinner)와 스켈레톤(Skeleton) UI 비교

[참고] https://www.nngroup.com/articles/progress-indicators/

 

Progress Indicators Make a Slow System Less Insufferable

Users are more satisfied and will wait longer when a site uses wait animations such as percent-done bars and spinners to explain >1 s response times delays.

www.nngroup.com

 

 

스피너 : 로딩 중임을 나타내는 애니메이션 아이콘
   사용자의 요청이나 액션에 따라 발생하는 진행 중인 작업을 보여줄 때 사용
    예로, 버튼을 클릭하여 내용을 불러오거나 어떤 작업을 수행하는 동안 사용

스켈레톤
   콘텐츠가 생기면 점진적으로 채워지는 빈 페이지를 사용
   초기 로딩 상황에서 전체 페이지를 로드할 때 대기시간 감축 효과를 위해 사용

 

 

스피너 (Spinner)      

         스켈레톤 UI (Skeleton)

사용 시기 
  • 짧은 로딩 시간 (1-2초 이내)
  • 전체 페이지 새로고침
  • 작은 컴포넌트의 단일 액션 (버튼 클릭, 폼 제출 등)
  • 로딩 완료 시점을 명확히 표시해야 할 때
  • 긴 로딩 시간 (2초 이상)
  • 복잡한 데이터 구조 (리스트, 테이블, 카드 등)
  • 점진적 로딩이 필요한 경우
  • 사용자 경험이 중요한 메인 콘텐츠
장점
  • 구현이 간단
  • 명확한 로딩 상태 전달
  • 적은 리소스 사용
  • 더 빠르게 로딩되는 것처럼 느껴짐
  • 실제 콘텐츠 구조 미리 파악 가능
  • 부분적 인터랙션 가능
  • 더 부드러운 전환 경험
단점
  • 사용자가 대기 시간을 길게 느낌
  • 전체 화면 블로킹 시 사용자 상호작용 제한
  • 실제 콘텐츠 구조 파악 불가
  • 구현이 복잡
  • 추가적인 디자인 리소스 필요
  • 레이아웃이 자주 변경되는 경우 유지보수 어려움

 

권장 사용

1. 혼합 사용 
   - 전체 페이지 로딩 👉 스켈레톤
   - 부분 데이터 업데이트 👉 스피너

2. 컨텍스트 기반 선택 
   - 데이터 중심 페이지 👉 스켈레톤
   - 단순 액션 피드백 👉 스피너

 

3. 사용자 경험 기준 
   - 긴 대기 시간 예상 👉 스켈레톤
   - 즉각적 피드백 필요 👉 스피너

 

 

 

 


적용 [Vue.js]

기존 문제 상황 :  긴 로딩 시간 전체 화면 스피너 적용

프로젝트의 특정 페이지에 접근할 때, 테이블의 데이터를 완전히 불러오는데 5~6초 정도 걸립니다. 기존 구현은 전체 화면 스피너로 데이터가 로드 될 때까지 화면이 블로킹되는 형태였습니다.

그래서 사용자는 어떤 액션도 취할 수 없는 상태가 지속이 되고, 로딩 중의 상태에 대한 피드백이 부족하여 자칫 화면이 렉이 걸린 듯한 느낌을 줬습니다.

 

👉 Server resone Time :  3.88s (대략 4초)

화면이 뜨기까지 체감시간은 5~6 초 되는 것 같습니다. 화면 전체가 스피너가 돌면서 블락이 되기 때문에  그동안 사용자는 가만히 기다려야만 했는데요.

 

대부분의 스피너 로딩은 짧은 로딩 시간 (1~2초) 에 사용이 되기 때문에 3초 이상으로 로딩이 지속되는 곳에 스피너 로딩을 사용하게 된다면 아무런 정보 없이 계속 반복되는 것을 보고 사용자가 오류라고 받아들일 수 있게 됩니다.

그래서 시각적으로 정보가 보여지는 로딩인 스켈레톤은 빈 화면 대신에 다음 화면에 나올 것을 미리 보여줘서 긴 로딩시간에 스켈레톤UI로 바꾸는게 맞다고 판단했습니다.

 

 

 

해결방법 : 테이블 스켈레톤 UI 적용

테이블에 스켈레톤 UI를 적용하여 부분적으로 로딩을 처리하였습니다. 다른 UI 요소(필터, 다른 버튼 들..)들은 즉시 렌더링 되어 사용 가능하도록 하여 사용자의 답답함을 해소하였습니다.

 

 

스켈레톤 UI 적용

 

 

다크모드 UI 적용

 

 

기존 : 전체 페이지 로딩바 의 문제점  변경 : 점진적 렌더링(Progressive Rendering) -> 테이블에 스켈레톤 UI 적용
  • 사용자 상호작용 차단
  • 전체 페이지 로딩은 웹이 느리다는 인상을 줌
  • 사용자가 기다려야 하는 '데드 타임'을 만듭니다.
  • 페이지 구조와 인터랙티브 요소에 즉시 액세스할 수 있습니다
  • 사용자는 작업 준비를 시작할 수 있습니다(필터 설정, 검색 준비)
  • 완전한 차단이 아닌 부분적인 완성감을 만들어 냅니다
  • 시각적 피드백을 통해 인식된 로딩 시간 단축
  • 체감 성능 향상

 

 

 

 

코드 구현

 

1. 컴포넌트 구조

- 테이블 형태의 스켈레톤 UI 구현
- 동적으로 계산된 행(computedRows) 만큼 반복하여 로딩 플레이스홀더 생성

// Skeleton.vue


<template>
  <div class="skeleton-table">
    <div v-for="n in computedRows" class="skeleton-row">
      <div class="skeleton-box" />
    </div>
  </div>
</template>

 

`.skeleton-row `

테이블 배경 색상 css 

.skeleton-row {
    padding: 16px;
    display: flex;
    justify-content: center;
    background-color: var(--color-theme); //색상 지정
  }

 

 

`skeleton-box`

테이블 row 모양, 웨이브 그라데이션 색상 효과 

.skeleton-box {
  // 기본 스타일링
  height: 20px;
  width: 100%;
  border-radius: 5px;
  display: inline-block;
  position: relative;
  vertical-align: middle;
  overflow: hidden;
  background-color: var(--scales-3);
  
  // 애니메이션 효과
  &::after {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    transform: translateX(-100%);
    background-image: linear-gradient(
      90deg,
      rgba(#fff, 0) 0,
      rgba(#fff, 0.3) 20%,
      rgba(#fff, 0.8) 50%,
      rgba(#fff, 0.3) 80%,
      rgba(#fff, 0)
    );
    animation: shimmer 4s infinite; //4초 주기로 무한 반복, 왼쪽에서 오른쪽으로 이동하는 애니메이션
    content: '';
  }
}

//반짝이는 효과
@keyframes shimmer {
  100% {
    transform: translateX(100%);
  }
}

 

 

2. Props & Data

- height: 컴포넌트 높이 설정 prop (기본값 100%)
- parentHeight: 부모 요소 높이 저장용 데이터

props: {
  height: {
    default: '100%',
    type: String,
  }
},
data: {
  parentHeight: 500 // 기본 높이
}

 

 

3. 높이 계산 로직

- 각 행의 높이를 계산 (content: 20px + padding: 16px * 2)
- 전체 높이를 행 높이로 나누어 필요한 행 수 계산
- 소수점 올림으로 행 수 보정

computed: {
  computedRows() {
    const rowHeight = 20 + 16 * 2; // 한 행의 높이 (content + padding)
    if (this.height === '100%') {
      return Math.ceil(this.parentHeight / rowHeight);
    }
    const containerHeight = parseInt(this.height);
    return Math.ceil(containerHeight / rowHeight);
  }
}

 

`table-body`(부모 요소인 테이블) 의 높이가 지정되어 있으면, row 수 계산하여 처리

 

 

4. 반응형 처리

- 마운트 시 부모 요소 높이 계산
- 리사이즈 이벤트 리스너 등록
- 메모리 누수 방지를 위한 이벤트 리스너 정리

mounted() {
  if (this.height === '100%') {
    this.updateParentDimensions();
    window.addEventListener('resize', this.handleResize);
  }
},
methods: {
  updateParentDimensions() {
    this.parentHeight = this.$el?.parentElement?.clientHeight || 500;
  }
}

 

 

 

 

 

 

 

 


 

참고


New Relic 이 페이지 로드 시간을 측정하는 방법

[원글] https://docs.newrelic.com/kr/docs/browser/new-relic-browser/page-load-timing-resources/page-load-timing-process/

 

New Relic이 페이지 로드 시간을 측정하는 방법 | New Relic Documentation

Request queuing 해당 계정에 브라우저 모니터링 및 APM이 연결되어 있는 경우 로드 시간 차트에 표시됩니다. 뉴렐릭에서 요청 큐잉은 요청이 프로덕션 시스템에 입력된 후 해당 요청이 도달하는 사

docs.newrelic.com

 

Skeleton Loading Animation with Vue.js

[원글] https://markus.oberlehner.net/blog/skeleton-loading-animation-with-vue/

 

Skeleton Loading Animation with Vue.js - Markus Oberlehner

Learn how to build a pure CSS skeleton loading animation with a pure CSS shimmer animation.

markus.oberlehner.net

 

728x90
반응형