[Flutter] 부드러운 로딩 처리 - Skeleton Loader

S_Soo100·2024년 8월 21일
0

flutter

목록 보기
15/19

스켈레톤 로딩?

  • 좋은 서비스에 들어갈 때, 콘텐츠는 안보이고 틀만 흐릿하게 보이다가 콘텐츠가 나타나는 경험을 했을 것이다.
  • 화면의 기본 구조를 먼저 보여주고, 필요한 데이터는 나중에 로딩하여 효율적인 리소스 사용과 더 나은 사용자 경험을 제공하는 것이 바로 스켈레톤 로딩이다.
  • 쉽게 말해 사용자가 지루하거나 앱이 돌아가는지 멈췄는지 고민하지 않도록 해준다.

라이브러리 소개

skeleton_loader | Flutter Package
skeleton_loader는 likes도 적고 조금 마이너한 느낌인데,
Shimmer 패키지를 사용해서 만들어진 간단한 위젯이다.
사용하기 쉽고 몇몇 장점이 있어서 간단히 소개해보고자 한다.

목적

1) 신규 플랫폼 런칭시 더 좋은 UI 제공
2) CircularProgressIndicator보다 더 나은 UX 제공
3) Front-End 개발자로써 라이브러리 학습

준비

  • 우선 플러터에 올라온 공식문서를 확인해보고, 내 패키지에 skeleton_loader를 설치했다.

    skeleton_loader | Flutter Package

  • 패키지 안에는 2개의 위젯이 들어있다.
    1. SkeletonLoader (콘텐츠의 갯수 등 정해져 있을 때)
    2. SekeletonGridLoader
    (그리드형, 콘텐츠의 갯수 등이 정해져 있지 않을 때)

    둘 다 어렵지 않으니 둘 다 만들어 보도록 하자.

프로젝트 사용

  • 프로젝트 안에 예제 페이지를 만든다.
  • TabBar를 써서 첫 탭에는 콜럼 타입, 다른 탭에는 그리드 타입 스켈레톤 로딩을 보여주도록 한다.
  • 원 모양 리프레쉬 버튼을 누르면 상태가 로딩으로 변했다가 3초 후에 다시 로드됨으로 돌아온다.

import 'package:flutter/material.dart';
import 'package:skeleton_loader/skeleton_loader.dart';

enum States { loading, loaded }

class SkeletonPracticeView extends StatefulWidget {
  const SkeletonPracticeView({Key? key}) : super(key: key);

  
  State<SkeletonPracticeView> createState() => _SkeletonPracticeViewState();
}

class _SkeletonPracticeViewState extends State<SkeletonPracticeView> {
  final String title = 'Skeleton Loader Demo';
  States screenState = States.loaded;

  
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: 2,
        initialIndex: 0,
        child: Scaffold(
          appBar: AppBar(
          // 앱바 설정
          ),
          body: TabBarView(
            children: [
              _columnTap(), // 스켈레톤 로더
              _gridTap(), // 그리드형 스켈레톤 로더
            ],
          ),
          floatingActionButton: FloatingActionButton( 
            onPressed: () {
              setState(() {
                screenState = States.loading;
              });
              Future.delayed(const Duration(seconds: 3), () {
                setState(() {
                  screenState = States.loaded;
                });
              });
            },
            child: const Icon(Icons.refresh_rounded),
          ),
        ));
  }
  ...
  ...
  ...
  • 자 이제 사용법에 대해서 훑어보고 가자

손쉬운 사용법

1. SkeletonLoader 타입

  • SkeletonLoader 위젯이 받는 builder는 Widget 타입이다.
    저기에 뭘 넣는지에 따라 로딩 될 때 보여지는 위젯이 다르다.

    Widget _columnTap() {
      return screenState == States.loading
          ? SkeletonLoader(
              baseColor: const Color.fromRGBO(240, 240, 240, 1),
              highlightColor: Colors.amber,
              period: const Duration(seconds: 1),
              builder: _columnItemArray(),
            )
          : _columnItemArray();
    }
    
    Widget _columnItemArray() {
      return Column(
        children: List.generate(3, (index) => _columnItem()),
      );
    }
    
    Widget _columnItem() {
      return Center(
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.all(10),
            child: Row(
              children: [
                // Container(color: Colors.blue, child: Text("data")),
                Container(
                    width: 100,
                    height: 100,
                    color: Colors.blue.shade100,
                    margin: const EdgeInsets.all(10),
                    child: const Icon(
                      Icons.support_agent_sharp,
                      size: 100,
                    )),
                SizedBox(
                  width: 220,
                  height: 120,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      Container(
                        width: 200,
                        height: 30,
                        color: Colors.blue.shade100,
                        child: const Text('Name'),
                      ),
                      Container(
                        width: 200,
                        height: 30,
                        color: Colors.blue.shade100,
                      ),
                    ],
                  ),
                )
              ],
            ),
          )
        ]),
      );
    }
  • 여기서 짚고 넘어갈 포인트는,
    SkeletonLoader는 내 자식 위젯중 가장 상위에 있는 '색상'을 가진 놈을 보여주고 그 하위 자식 위젯들은 모두 가려서 보여준다.

    Shimmer.fromColors 를 사용하고 있기 때문이다.

  • 지금 _columnItem은 Container에 색을 칠하고 그 안에 아이콘과 텍스트를 집어넣고 있다.
    그리고 Container의 부모 위젯들은 모두 색이 없다.
    그러므로 Container가 로딩이 되는 것이다.

  • 그러면 Container의 색상을 없애보자.

 Container(
    width: 100,
    height: 100,
    // color: Colors.blue.shade100,
    margin: const EdgeInsets.all(10),
    child: const Icon(
        Icons.support_agent_sharp,
        size: 100,
    ),
),

  • 이제 Container가 아니라 그 자식인 아이콘이 로딩이 걸리는걸 알 수 있으며, 텍스트도 마찬가지로 부모 Container가 색상이 없으면 글씨 자체가 로딩이 걸린다.
  • 이를 잘 활용해서 본인이 원하는 로딩을 구현하기 바란다.

2. SekeletonGridLoader 타입

  • 그리드는 GridView.builder 의 구성방식을 그대로 답습하고 있다.
  • 그리고 스켈레톤 로더 자체에 그리드를 넣어서, 내가 보여주고자 하는 콘텐츠와 로딩되는 콘텐츠의 갯수를 다르게 조절할 수 있다.

(지금은 9개로 서로 맞춰두었다)

 Widget _gridTap() {
   return screenState == States.loading
       ? SkeletonGridLoader(
           baseColor: const Color.fromRGBO(240, 240, 240, 1),
           highlightColor: Colors.amber,
           period: const Duration(seconds: 1),
           builder: _gridItem(), // 단일 아이템
           items: 9, // 갯수
           itemsPerRow: 3, //1 줄에 배치하는 갯수
           childAspectRatio: 1,
           mainAxisSpacing: 10,
           crossAxisSpacing: 10,
         )
       : _gridItemGrid();
 }
  • 대신 items라던지, itemsPerRow같은 용어가 GridView.builder에서는 itemCount, crossAxisCount로 사용했을 것이다.

     Widget _gridItemGrid() {
       return GridView.builder(
         shrinkWrap: true,
         itemCount: 9,
         gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
           crossAxisCount: 3,
           childAspectRatio: 1,
           mainAxisSpacing: 10,
           crossAxisSpacing: 10,
         ),
         itemBuilder: (BuildContext context, int index) {
           // return Text(index.toString());
           return _gridItem();
         },
       );
     }
    
     Widget _gridItem() {
       return Container(
           width: 100,
           height: 100,
           color: Colors.blue.shade100,
           margin: const EdgeInsets.all(10),
           child: const Icon(
             Icons.support_agent_sharp,
             size: 100,
           ));
     }
  • 이 위젯의 경우 그리드 타입으로 데이터를 불러와야 할 때,
    특히 서버 통신으로 데이터를 불러온다고 하면 총 갯수가 얼마인지 fetching 단계에서는 알 수 없다.

  • 그러므로 SkeletonGridLoader가 적정 수를 보여주도록 세팅하여서 적절한 로딩 UI를 구성할 수 있다.

  • 이를 이용해서 단지 원이 빙글빙글 도는 것 보다 조금 더 나은 사용자 경험을 줘 보도록 하자.

profile
플러터, 리액트

0개의 댓글