LoadingRegion | CoUI
๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
LogoCoUI

LoadingRegion

์•„์ง ๋‚ด์šฉ์ด ์˜ค์ง€ ์•Š์€ ์˜์—ญ์˜ "๋กœ๋”ฉ ์ค‘" ์„ ํ•œ ๋ฒˆ๋งŒ ์•Œ๋ฆฌ๋Š” ๋ž˜ํผ

๋ชฉ์ฐจ

ํ•œ ์˜์—ญ์„ ์ฑ„์šฐ๋Š” ์Šค์ผˆ๋ ˆํ†ค๋“ค์„ ๊ฐ์‹ธ "๋กœ๋”ฉ ์ค‘" ์„ ์˜์—ญ ๋‹จ์œ„๋กœ ํ•œ ๋ฒˆ ์•Œ๋ฆฝ๋‹ˆ๋‹ค. ์•ˆ์ชฝ ์Šค์ผˆ๋ ˆํ†ค์€ ์ž๊ธฐ๊ฐ€ ์˜์—ญ ์•ˆ์— ์žˆ์Œ์„ ์•Œ๊ณ  ์ž๊ธฐ ์•Œ๋ฆผ์„ ๋•๋‹ˆ๋‹ค โ€” ์Šค์ผˆ๋ ˆํ†ค ์—ด ๊ฐœ๊ฐ€ ์•Œ๋ฆผ ์—ด ๋ฒˆ์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด์— ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์€ ์—†์Šต๋‹ˆ๋‹ค. ๋ ˆ์ด์•„์›ƒ ์ƒ์ž(flex ยท grid ยท flow)๋Š” child ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค โ€” Web ์—์„œ ๋ถ€๋ชจ์˜ ํ˜•์ œ ์„ ํƒ์ž ๊ธฐ๋ฐ˜ ๊ฐ„๊ฒฉ(space-y-* ยท divide-*)๋งŒ ์˜์—ญ์„ ์ง€๋‚˜ child ์— ๋‹ฟ์ง€ ์•Š์œผ๋ฏ€๋กœ, ์˜์—ญ์˜ ๋ถ€๋ชจ์—๋Š” gap ์„ ์“ฐ์„ธ์š”.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• (Basic Usage)#

LoadingRegion(
  semanticLabel: '์ฃผ๋ฌธ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘',
  child: Column(
    spacing: CoreSpace.space8,
    children: [
      Skeleton(height: 20),
      Skeleton(height: 20),
      Skeleton(height: 20),
    ],
  ),
)

๋กœ๋”ฉ์ด ๋๋‚˜๋ฉด ํ˜ธ์ถœํ•˜๋Š” ์ชฝ ์กฐ๊ฑด์œผ๋กœ ์˜์—ญ์งธ ์‹ค์ œ ๋‚ด์šฉ์œผ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.

isLoading
    ? LoadingRegion(child: orderSkeletons)
    : OrderList(orders: orders)

Live Preview#

Web
Loading orders
Flutter
Loading Flutter...
์ฝ”๋“œ ๋ณด๊ธฐ
class LoadingRegionDefaultExample extends StatefulComponent {
  const LoadingRegionDefaultExample({super.key});

  @override
  State<LoadingRegionDefaultExample> createState() => _LoadingRegionDefaultExampleState();
}

class _LoadingRegionDefaultExampleState extends State<LoadingRegionDefaultExample> {
  @override
  Component build(BuildContext context) {
    return LoadingRegion(
      semanticLabel: 'Loading orders',
      child: div(classes: 'flex flex-col gap-${CoreSpace.scale.space8}', [
        Skeleton(width: '200px', height: '20px'),
        Skeleton(width: '150px', height: '20px'),
        Skeleton(width: '180px', height: '20px'),
      ]),
    );
  }
}
class LoadingRegionDefaultExample extends StatefulWidget {
  const LoadingRegionDefaultExample({super.key});

  @override
  State<LoadingRegionDefaultExample> createState() => _LoadingRegionDefaultExampleState();
}

class _LoadingRegionDefaultExampleState extends State<LoadingRegionDefaultExample> {
  @override
  Widget build(BuildContext context) {
    return LoadingRegion(
      semanticLabel: 'Loading orders',
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        spacing: Theme.of(context).geometry.space(CoreSpace.space8),
        children: [
          Skeleton(width: 200, height: 20),
          Skeleton(width: 150, height: 20),
          Skeleton(width: 180, height: 20),
        ],
      ),
    );
  }
}

์‚ฌ์šฉ ์‹œ๊ธฐ (When to Use)#

์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

  • ๋ชฉ๋กยทํ‘œยท์นด๋“œ ๋ฌถ์Œ์ฒ˜๋Ÿผ ์Šค์ผˆ๋ ˆํ†ค ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ํ•œ ์˜์—ญ์„ ์ฑ„์šธ ๋•Œ
  • ํ™”๋ฉด์— ๋กœ๋”ฉ ์ค‘์ธ ์˜์—ญ์ด ์—ฌ๋Ÿฟ์ด๋ผ ๋ฌด์—‡์ด ๋กœ๋”ฉ ์ค‘์ธ์ง€ ๋งํ•ด์•ผ ํ•  ๋•Œ (semanticLabel: '์ฃผ๋ฌธ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘')

๋Œ€์‹  ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

  • Skeleton ํ•˜๋‚˜๋งŒ ๋‹จ๋…์œผ๋กœ ์“ธ ๋•Œ โ€” ๋‹จ๋… ์Šค์ผˆ๋ ˆํ†ค์€ ์Šค์Šค๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค
  • Loading: ์ž๋ฆฌ ๋ชจ์–‘์„ ํ‰๋‚ด ๋‚ผ ํ•„์š” ์—†์ด "์ž‘์—… ์ค‘" ๋งŒ ๋ณด์—ฌ ์ค„ ๋•Œ

Props / Parameters#

LoadingRegion#

์†์„ฑํƒ€์ž…๊ธฐ๋ณธ๊ฐ’์„ค๋ช…
child W ํ•„์ˆ˜ ๋กœ๋”ฉ ์ค‘์ธ ์˜์—ญ โ€” ๋ณดํ†ต ๋‚ด์šฉ ์ž๋ฆฌ๋ฅผ ์ฑ„์šฐ๋Š” ์Šค์ผˆ๋ ˆํ†ค๋“ค. ์˜์—ญ์ด ์•Œ๋ฆฌ๋Š” ๋™์•ˆ ๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ์ˆจ๊ฒจ์ง‘๋‹ˆ๋‹ค
semanticLabel String? null ์•Œ๋ฆด ๋ฌธ๊ตฌ. null ์ด๋ฉด ๋ Œ๋” ์‹œ์  ๋กœ์ผ€์ผ์˜ ๋กœ๋”ฉ ๋ฌธ๊ตฌ(์˜์–ด "Loading", ํ•œ๊ตญ์–ด "๋กœ๋”ฉ ์ค‘")๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค

๋ณด์ด๋Š” ํฌ๋กฌ์ด ์—†์–ด Style ยท Theme ์Šฌ๋กฏ์ด ์—†์Šต๋‹ˆ๋‹ค.

๋™์ž‘ ์ŠคํŽ™ (Behavior)#

  • ์˜์—ญ์ด status ์—ญํ• ์˜ ๋ผ์ด๋ธŒ ์˜์—ญ ํ•˜๋‚˜๊ฐ€ ๋˜์–ด semanticLabel(๋˜๋Š” ๋กœ์ผ€์ผ ๋ฌธ๊ตฌ)์„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค. ์ •์ค‘ํ•œ(polite) ์•Œ๋ฆผ์ด๋ผ ์ฝ๋˜ ๋ฌธ์žฅ์„ ๋Š์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์•ˆ์ชฝ Skeleton ์€ ์˜์—ญ ์•ˆ์— ์žˆ์œผ๋ฉด ์ž๊ธฐ ์•Œ๋ฆผ์„ ๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์˜์—ญ ๋ฐ–์˜ ์Šค์ผˆ๋ ˆํ†ค์€ ์ง€๊ธˆ์ฒ˜๋Ÿผ ์Šค์Šค๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค โ€” Flutter ์—์„œ๋Š” ์˜์—ญ ์•ˆ์—์„œ ์—ฐ ์˜ค๋ฒ„๋ ˆ์ด๋„ ์˜์—ญ ๋ฐ–์— ๊ทธ๋ ค์ง€๋ฏ€๋กœ ๊ทธ ์•ˆ์˜ ์Šค์ผˆ๋ ˆํ†ค์ด ์Šค์Šค๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
  • ์˜์—ญ ์•ˆ์— ์˜์—ญ์„ ๋‹ค์‹œ ๋‘๋ฉด ๋ฐ”๊นฅ ์˜์—ญ๋งŒ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
  • child ๋Š” ์˜์—ญ์ด ์•Œ๋ฆฌ๋Š” ๋™์•ˆ ๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ์ˆจ๊ฒจ์ง‘๋‹ˆ๋‹ค. ๋กœ๋”ฉ ์ค‘์— ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋‚ด์šฉ์€ ์˜์—ญ ๋ฐ–์— ๋‘ก๋‹ˆ๋‹ค.
  • ๋™์ž‘ ์ค„์ด๊ธฐ ์„ค์ •์—์„œ๋„ ์•Œ๋ฆผ์€ ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค(์Šค์ผˆ๋ ˆํ†ค ๋งฅ๋™๋งŒ ๋ฉˆ์ถฅ๋‹ˆ๋‹ค).

์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋ผ์ธ (Usage Guidelines)#

โœ… Do#

์˜์—ญ๋งˆ๋‹ค ๋ฌด์—‡์ด ๋กœ๋”ฉ ์ค‘์ธ์ง€ ๋งํ•˜๊ธฐ

LoadingRegion(semanticLabel: '์ฃผ๋ฌธ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘', child: orderSkeletons)
LoadingRegion(semanticLabel: '์ถ”์ฒœ ์ƒํ’ˆ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘', child: productSkeletons)

ํ™”๋ฉด์— ๋กœ๋”ฉ ์˜์—ญ์ด ๋‘˜ ์ด์ƒ์ด๋ฉด ๊ธฐ๋ณธ ๋ฌธ๊ตฌ("๋กœ๋”ฉ ์ค‘")๋งŒ์œผ๋กœ๋Š” ์–ด๋А ์ชฝ์ธ์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฌธ๊ตฌ๋Š” ์•ฑ ์ฝ˜ํ…์ธ ๋ผ ํ˜ธ์ถœ์ž๊ฐ€ ๋„˜๊น๋‹ˆ๋‹ค.


โŒ Don't#

์ง€๊ธˆ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋‚ด์šฉ์„ ์˜์—ญ ์•ˆ์— ๋„ฃ์ง€ ์•Š๊ธฐ

// โŒ ํ•„ํ„ฐ ๋ฒ„ํŠผ์ด ๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ์‚ฌ๋ผ์ง„๋‹ค
LoadingRegion(
  child: Column(children: [filterBar, orderSkeletons]),
)

์˜์—ญ์€ child ์ „์ฒด๋ฅผ ๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ์ˆจ๊น๋‹ˆ๋‹ค. ๋กœ๋”ฉ๊ณผ ์ƒ๊ด€์—†์ด ์“ธ ์ˆ˜ ์žˆ๋Š” ์ปจํŠธ๋กค์€ ์˜์—ญ ๋ฐ–์— ๋‘๊ณ , ์Šค์ผˆ๋ ˆํ†ค๋งŒ ๊ฐ์‹ธ์„ธ์š”.

์ ‘๊ทผ์„ฑ (Accessibility)#

์—ญํ• #

  • Flutter: CoUISemantics(role: status, container: true) โ€” ๋ผ์ด๋ธŒ ์˜์—ญ ๋…ธ๋“œ ํ•˜๋‚˜์— ๋ฌธ๊ตฌ๊ฐ€ ๋ถ™์Šต๋‹ˆ๋‹ค. container ๋ผ ๋ผ๋ฒจ์ด ๋ถ™์€ ๋ถ€๋ชจ ๋…ธ๋“œ์— ์„ž์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • Web: ์‹œ๊ฐ์ ์œผ๋กœ ์ˆจ๊ธด(sr-only) role="status" ์š”์†Œ ํ•˜๋‚˜๊ฐ€ ๋ฌธ๊ตฌ๋ฅผ ํ…์ŠคํŠธ๋กœ ๋‹ด์Šต๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ ์˜์—ญ์€ ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ ๋‚ด์šฉ์„ ์ฝ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

ํ‚ค๋ณด๋“œ#

์—†์Šต๋‹ˆ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์–ด๋–ค ํ‚ค๋„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์Šคํฌ๋ฆฐ ๋ฆฌ๋”#

  • ์˜์—ญ ํ•˜๋‚˜์— ์•Œ๋ฆผ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ์•ˆ์ชฝ ์Šค์ผˆ๋ ˆํ†ค์€ ์•Œ๋ฆผ์„ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค(์–‘ ํ”Œ๋žซํผ ๋ชจ๋‘ LoadingRegionScope ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค).
  • Flutter: child ๋Š” ExcludeSemantics ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
  • Web: child ๋Š” aria-hidden="true" ์ธ display: contents ์š”์†Œ ์•ˆ์— ์žˆ์Šต๋‹ˆ๋‹ค โ€” ๋ ˆ์ด์•„์›ƒ ์ƒ์ž๋ฅผ ๋”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํฌ์ปค์Šค ๊ด€๋ฆฌ#

ํฌ์ปค์Šค๋ฅผ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. child ์•ˆ์˜ ํฌ์ปค์Šค ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋Š” ๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ์ˆจ๊ฒจ์ง„ ์ฑ„ ํฌ์ปค์Šค๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋กœ๋”ฉ ์ค‘์— ์“ธ ์ปจํŠธ๋กค์€ ์˜์—ญ ๋ฐ–์— ๋‘ก๋‹ˆ๋‹ค.

์•Œ๋ ค์ง„ ์ œ์•ฝ#

  • Web โ€” ์‚ฝ์ž… ์ˆœ๊ฐ„์— ์ด๋ฏธ ๋ฌธ๊ตฌ๊ฐ€ ์ฐฌ ์ƒํƒœ๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. Toast ์™€ ๊ฐ™์€ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์‚ฝ์ž…๋œ status ๋ฅผ ๋ฐ”๋กœ ์ฝ๋Š”์ง€๋Š” ์Šคํฌ๋ฆฐ๋ฆฌ๋”๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์–ด ์‹ค๊ธฐ ํ™•์ธ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.
  • Web โ€” display: contents ์š”์†Œ์˜ aria-hidden. ํ˜„ํ–‰ ๋ธŒ๋ผ์šฐ์ €๋Š” ๋”ฐ๋ฅด์ง€๋งŒ ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๋ฌด์‹œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Web โ€” ๋ถ€๋ชจ์˜ ํ˜•์ œ ์„ ํƒ์ž ๊ธฐ๋ฐ˜ ๊ฐ„๊ฒฉ์€ child ์— ๋‹ฟ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์˜์—ญ์€ ๋ถ€๋ชจ ์•ˆ์— ์ˆจ๊ธด ์•ˆ๋‚ด ์š”์†Œ์™€ display: contents ๋ž˜ํผ, ๋‘˜์„ ํ˜•์ œ๋กœ ๋ผ์›๋‹ˆ๋‹ค. space-y-* ยท divide-* ยท :first-child ๋Š” ๊ทธ ๋‘˜์— ๊ฑธ๋ ค child ์—๋Š” ๊ฐ„๊ฒฉ์ด ์ƒ๊ธฐ์ง€ ์•Š๊ณ , ์˜์—ญ์ด ๋น ์ง€๋ฉด ๊ฐ„๊ฒฉ์ด ์ƒ๊ฒจ ์ฝ˜ํ…์ธ ๊ฐ€ ์ ํ”„ํ•ฉ๋‹ˆ๋‹ค. flex ยท grid ์˜ gap ์€ ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.
  • ์˜์—ญ์€ ๋‚ด์šฉ์„ ํ†ต์งธ๋กœ ์ˆจ๊น๋‹ˆ๋‹ค. ์ผ๋ถ€๋งŒ ๋กœ๋”ฉ ์ค‘์ธ ์˜์—ญ์ด๋ผ๋ฉด ๋กœ๋”ฉ ์ค‘์ธ ๋ถ€๋ถ„๋งŒ ๊ฐ์‹ธ์„ธ์š”.

ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ฐจ์ด์  (Platform Differences)#

ํ•ญ๋ชฉFlutterWeb
ํด๋ž˜์Šค๋ช…LoadingRegionLoadingRegion
child ํƒ€์ž…WidgetComponent
์•Œ๋ฆผ CoUISemantics(role: status) ๋ผ์ด๋ธŒ ์˜์—ญ ๋…ธ๋“œ ์ˆจ๊ธด role="status" ์š”์†Œ์˜ ํ…์ŠคํŠธ
๋‚ด์šฉ ์ˆจ๊น€ ExcludeSemantics aria-hidden="true" + display: contents
์Šค์ผˆ๋ ˆํ†ค ์นจ๋ฌต LoadingRegionScope(InheritedWidget) LoadingRegionScope(InheritedComponent)
  • Skeleton: ์˜์—ญ ์•ˆ์—์„œ ๋‚ด์šฉ ์ž๋ฆฌ๋ฅผ ์ฑ„์šฐ๋Š” ์ž๋ฆฌํ‘œ์‹œ
  • Loading: ์ž๋ฆฌ ๋ชจ์–‘ ์—†์ด "์ž‘์—… ์ค‘" ๋งŒ ๋ณด์—ฌ ์ฃผ๋Š” ํ‘œ์‹œ