Study Record

[Flutter] key? 본문

Flutter

[Flutter] key?

초코초코초코 2023. 3. 2. 18:26
728x90

key?

key 는 위젯 트리에서 위젯이 움직일 때마다 현 상태를 보존하는 역할을 한다. 현 스크롤의 위치를 기억하거나 수정 상태를 보존하는 것이 될 수 있다. 

 

 

😶 언제 Key 를 사용해야 하나?

대부분 key 를 사용할 필요 없지만 어떤 상태를 유지하고 있는 같은 종류의 위젯 컬렉션을 더하거나, 제거하거나 정렬해야 할 때 필요하다.

 

 

무슨 말인지 모르겠으니 예를 들어,

랜덤한 색상을 가진 Container() 가 두 개 있고 버튼을 누르면 두 개의 Container 가 서로 위치가 바뀌는 앱이 있다. Container() 는 StatelessWidget 으로 설계되어 있고 titles 라는 리스트에 저장되고 플러팅 버튼을 누르면 titles 의 위젯들 위치만 서로 바뀐다.

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

void main() => runApp(MaterialApp(home: _HomeScreen()));

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

  @override
  State<_HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<_HomeScreen> {
  late List<Widget> titles;

  @override
  void initState() {
    super.initState();
    titles = [
      StatelessColorWidget(),
      StatelessColorWidget(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(child: Row(children: titles)),
      floatingActionButton: 
        FloatingActionButton(child: Icon(Icons.swap_horiz), onPressed: swapTitles)
    );
  }

  swapTitles() {
    setState(() {
      titles.insert(1, titles.removeAt(0));
    });
  }
}

class StatelessColorWidget extends StatelessWidget {
  final color = randomColor();
  StatelessColorWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Container(color: color, width: 100, height: 100);
}


ColorSwatch<int> randomColor() {
  final colorList = [
    Colors.red, Colors.greenAccent, Colors.blueAccent, Colors.deepOrangeAccent,
    Colors.brown, Colors.purple, Colors.amberAccent
  ];

  return colorList[Random().nextInt(7)];
}

이 앱의 실행과정을 설명하기전, 플러터에는 Widget Tree 와 Element Tree 가 있다. Widget Tree 는 말 그대로 현재 앱의 위젯들의 정보와 상하관계를 나타낸 트리이고 Element Tree 는 Widget Tree 의 위젯을 참조하는 정보(위젯 타입 정보)와 부속 정보(ex. State)를 알고 있는 간단한 플러터의 뼈대와 같은 트리이다. 

 

setState() 로 Container 들의 위치를 바뀌면 Widget Tree 의 container 들은 순서가 바뀌며, 플러터는 Element Tree 가 Widget Tree 와 뼈대가 같은지 확인한다. 비교할 때 Element Tree 에서 참조하는 Widget 의 위젯 타입과 key 정보를 확인한다. 비교해서 뼈대가 같지 않으면 Element Tree 를 업데이트한다. 

 

예시 앱에서는 key 에 대한 정보는 없고 위젯의 오리지널 정보를 담고 있는 Widget Tree 는 StatelessWidget 이며 Color 정보를 자체적으로 가지고 있다. 따라서 Element Tree 에서 참조하는 위젯이 다르다는 것을 깨닫고 업데이트되면서 우리가 보이는 것처럼 순서가 바뀐 것을 확인할 수 있다.

 

 

 

😶 StatefulWidget 이라면?

만약 예시 앱에서 Container 가 StatefulWidget 라면 어떻게 될까? 색상 정보를 어디다 선언하는지에 따라 차이가 있을 수 있다.

 

1. StatefulWidget 에 color 선언할 경우

...
titles = [
  StatefulColorWidget(),
  StatefulColorWidget(),
]
...

class StatefulColorWidget extends StatefulWidget {
  ColorSwatch<int> color = randomColor();
  StatefulColorWidget({Key? key}) : super(key: key);

  @override
  State<StatefulColorWidget> createState() => _StatefulColorWidgetState();
}

class _StatefulColorWidgetState extends State<StatefulColorWidget> {

  @override
  Widget build(BuildContext context) => Container(color: widget.color, width: 100, height: 100);

}

이 경우에도 StatelessWidget 에서처럼 key 는 없지만 StatefulWidget 자체에 color 정보가 들어있으니 Widget Tree 와 Element Tree 의 뼈대를 비교할 때 다르다는 것을 발견하고 업데이트되면서 정상 작동할 것이다.

 

 

 

2. State 에 color 정보가 있을 경우

...
titles = [
  StatefulColorWidget(),
  StatefulColorWidget(),
]
...

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

  @override
  State<StatefulColorWidget> createState() => _StatefulColorWidgetState();
}

class _StatefulColorWidgetState extends State<StatefulColorWidget> {
  final color = randomColor();
  
  @override
  Widget build(BuildContext context) => Container(color: color, width: 100, height: 100);

}

 

이 경우엔 State 에 Color 정보가 담겨있고 StatefulWidget 자체에는 정보가 없다. key 정보도 없다. 따라서 Container 들의 위치가 서로 바뀌고 Element Tree 와 비교될 때 같은 타입의 위젯이라는 점만 확인된다. 따라서 Element Tree 의 엡데이트를 하지 않는다. 이 경우의 실행 결과도 아무런 작동이 없는 것처럼 보인다.

 

 

바로 이런 경우 Key 를 활용하면 정상 작동할 수 있다.

 

 

 

3. key + State 에 color 가 있는 경우

...
titles = [
  StatefulColorWidget(key: UniqueKey()),
  StatefulColorWidget(key: UniqueKey()),
]
...

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

  @override
  State<StatefulColorWidget> createState() => _StatefulColorWidgetState();
}

class _StatefulColorWidgetState extends State<StatefulColorWidget> {
  final color = randomColor();
  
  @override
  Widget build(BuildContext context) => Container(color: color, width: 100, height: 100);

}

Widget Tree 에 Key 값이 추가되면 Widget Tree 의 Container 위젯에 key 값도 같이 저장된다. 따라서 순서가 바뀌고 Row 의 children 들과 비교할 때, 이전에 참조했던 위젯 타입과 key 정보를 확인해보니 순서가 바뀌었다는 것을 깨닫고 Element Tree 의 위젯들이 업데이트되어 정상적으로 보일 것이다.

 

 

Widget Tree 와 Element Tree 를 비교하는 것은 트리 레벨단위로 하게 된다. Row 끼리 한번 비교하고 그의 children 인 Container 들을 비교한다. 이것을 잘못 이해하면 예상치 못한 결과가 나올 수 있다.

 

 

😶 Padding 위젯 - Container + key

...
titles = [
  Padding(
    padding: const EdgeInsets.all(8.0),
    child: StatefulColorWidget(key: UniqueKey()),
  ),
  Padding(
    padding: const EdgeInsets.all(8.0),
    child: StatefulColorWidget(key: UniqueKey()),
  ),
]
...

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

  @override
  State<StatefulColorWidget> createState() => _StatefulColorWidgetState();
}

class _StatefulColorWidgetState extends State<StatefulColorWidget> {
  final color = randomColor();
  
  @override
  Widget build(BuildContext context) => Container(color: color, width: 100, height: 100);

}

 

실행하면 Container 들의 순서를 바꾸는 것이 아니라 랜덤으로 다시 생성되는 것처럼 보인다. 이러한 결과가 나온 이유는 Widget Tree 와 Element Tree 를 비교하는 과정이 레벨 단위이기 때문이다. 

순서를 바꾸면(StatefulWidget key1이 자식인 Padding 과 StatefulWidgt key2 가 자식인 Padding 의 순서를 바꾼다),

1번으로 Row 를 확인하고 Row 의 Children 인 Padding 들을 2번째로 비교한다. 2번까지의 과정에서 위젯들의 정보를 비교했을 때 바뀐 점이 없으므로 아무런 변화도 일어나지 않는다. 3번의 과정에서는 이전에 참조했던 key 값이 달라진 걸 알고 아예 새로운 위젯을 생성한다. 그 과정에서 색상도 바뀔 것이다. 4번도 3번의 과정과 같이 새로운 위젯이 생성되고 색상도 바뀐다. 이것을 방지하려면 Padding 위젯에 key 를 추가하면 된다.

 

 

😶 Padding + key

...
titles = [
  Padding(
    key: UniqueKey(),
    padding: const EdgeInsets.all(8.0),
    child: StatefulColorWidget(),
  ),
  Padding(
    key: UniqueKey(),
    padding: const EdgeInsets.all(8.0),
    child: StatefulColorWidget(),
  ),
]
...

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

  @override
  State<StatefulColorWidget> createState() => _StatefulColorWidgetState();
}

class _StatefulColorWidgetState extends State<StatefulColorWidget> {
  final color = randomColor();
  
  @override
  Widget build(BuildContext context) => Container(color: color, width: 100, height: 100);

}

2번 과정에서 Padding 위젯들이 서로 비교될 때 key 값을 보고 서로 위젯이 바뀌었다는 것을 발견하면 Element Tree 에서 Padding 단위끼리 순서를 바꾸고(padding 의 하위 위젯까지 같이 통째로 이동한다.), 3번, 4번에서 서로 같은 위젯 타입(StatefulColorWidget)인 것만 비교해 업데이트하지 않아도 된다고 판단할 것이다. 따라서, 버튼을 누르면 위젯의 위치만 바뀌는 정상 작동을 하게 된다.

 

 

😶 GlobalKey

Globalkey 는 상태를 잃지 않으면서 앱 어디서나 위젯이 상관요소를 바꾸는 것을 가능하게 하고 위젯 트리의 전혀 다른 부분에서 다른 위젯의 정보에 접근할 수 있게 해준다. 예를 들어, 두 개의 다른 스크린에 같은 위젯을 띄우는데 서로 같은 상태를 유지해야 할 경우가 될 수 있다.

 

 

 

Key class - foundation library - Dart API

A Key is an identifier for Widgets, Elements and SemanticsNodes. A new widget will only be used to update an existing element if its key is the same as the key of the current widget associated with the element. Keys must be unique amongst the Elements with

api.flutter.dev

 

728x90