Key 란?
위젯 트리에서 위젯이 움직일 때마다 현 상태를 보존하는 역할을 하는 Key입니다.
Key를 사용해서 이전에 스크롤 한 위치를 기억해서 다른 페이지를 갔다가 다시 빌드 될 때 해당 스크롤 위치로 다시 갈 수 있고 컬렉션을 수정한 상태를 보존할 수 있습니다. 이렇게 들어서는 이해가 안 되니 아래에서 추가 설명하겠습니다.
아래에서 Tree에 대해서 보고 오면 더욱 쉽게 이해할 수 있습니다.
Key 사용해보기
해당 코드는 Key를 사용하지는 않았습니다. 버튼을 누르면 색상 박스의 위치가 변하는 앱입니다.
해당 사진과 같이 Widget Tree 와 Element Tree가 생성됐을 겁니다. 색상 박스인 StlColorTile 위젯을 StatelessWidget으로 만들었기 때문에 "빨강" "노랑" 은 위젯 트리에 보관됩니다. 버튼을 눌러서 색상 박스의 위치를 바꾸면 트리는 아래와 같이 형성될 것입니다.
Element Tree는 Widget Tree가 바뀌면 다시 Widget Tree 와 1대1 매칭을 시작합니다. 이때 같은 타입인지 확인하고 키가 있으면 같은 키인지 확인합니다. 하지만 여기에서는 Key를 사용하지 않았으니 같은 타입인지만 확인합니다.
위처럼 같은 트리 위치에서 타입을 확인합니다. 타입은 모두 같기때문에 정상적으로 Element Tree는 RenderObjectTree에게 새로운 위젯으로 업데이트 해달라고 요청하고 다시 새로운 화면을 보이게 됩니다. 이처럼 Stateful 위젯 안에서 화면변경이 일어난 Stateless 위젯은 키를 사용하지 않아도 화면을 변경시킬 수 있기 때문에 일반적으로 Key를 사용하지 않습니다.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [
const StfColorTile(),
const StfColorTile(),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(children: tiles),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
},
child: const Icon(Icons.sentiment_very_satisfied),
),
);
}
}
class StfColorTile extends StatefulWidget {
const StfColorTile({Key? key}) : super(key: key);
@override
State<StfColorTile> createState() => _StfColorTileState();
}
class _StfColorTileState extends State<StfColorTile> {
late Color color;
@override
void initState() {
super.initState();
color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
}
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: const Padding(
padding: EdgeInsets.all(60),
),
);
}
}
이제 StatefulWidget으로 예시를 들겠습니다. 기존의 Stateless Widget 이었던 StlColorTile을 Stateful Widget으로 변경하고 이름을 StfColorTile로 변경하였습니다.
Stateless Widget 일 때는 변경됐던 게 Stateful Widget으로 바꾸니 아무리 버튼을 눌러도 타일의 위치가 변하지 않습니다. 그 이유는 여기서 찾아볼 수 있습니다.
해당 사진은 Stateful 로 바뀐 위젯의 Tree 모습 입니다. Stateful Widget이기 때문에 상태를 Elemente Tree에서 관리하는 걸 볼 수 있습니다.
우리가 버튼을 눌러서 타일의 위치를 변경시키는 행동을 했으니 위젯 트리는 위와 같이 변경됐습니다. 그리고 Widget Tree가 변경되면 Element Tree는 Widget Tree 와 1대1 매칭을 시작합니다. 이때 같은 타입인지 확인하고 키가 있으면 같은 키인지 확인합니다.
위처럼 같은 트리 위치에서 타입을 확인합니다. 그리고 키가 있으면 키가 같은지 확인하지만 현재는 Key를 주지 않은 상태입니다. 따라서 타입만 같은지 확인하고 문제가 없으면 다시 화면을 그려서 우리에게 보여줍니다. 하지만 실제 Flutter가 화면을 그리기 위해 참조하는 RenderObjectTree에서는 Element Tree를 참조 합니다. 하지만 State는 변경되지 않았기 때문에 화면을 다시 그려도 우리는 바뀐 화면을 볼 수 없습니다. Widget Tree는 화면을 다시 그리는 데에는 아무런 영향도 주지 않기 때문이죠. 따라서 Element Tree에 있는 State까지도 같이 바뀌어야 바뀐 화면을 볼 수 있을 겁니다. 이것을 해결할 수 있는 대안이 바로 Key입니다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [
StfColorTile(key: UniqueKey()), // Key 추가
StfColorTile(key: UniqueKey()), // Key 추가
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(children: tiles),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
},
child: const Icon(Icons.sentiment_very_satisfied),
),
);
}
}
class StfColorTile extends StatefulWidget {
const StfColorTile({Key? key}) : super(key: key);
@override
State<StfColorTile> createState() => _StfColorTileState();
}
class _StfColorTileState extends State<StfColorTile> {
late Color color;
@override
void initState() {
super.initState();
color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
}
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: const Padding(
padding: EdgeInsets.all(60),
),
);
}
}
const StfColorTile({Key? key}) : super(key: key);
우리가 항상 위젯을 생성할 때 보던 생성자입니다. 그렇습니다 모든 위젯은 키를 파라미터로 받아서 사용할 수 있습니다.
StfColorTile(key: UniqueKey())
그래서 이렇게 StfColorTile을 생성할 때파라미터로 키를넘길 수 있습니다. 파라미터로 키를 넘기면 위젯이 생성될 때 해당 키로 초기화 작업을 합니다. 유니크 키는 말 그대로 자동으로 유니크한 키를 생성해 줍니다. 이제 우리는 이 키로 위젯을 구별할 수 있습니다.
키를 주었기 때문에 버튼을 누르면 타일의 위치가 바뀌는 것을 알 수 있습니다. 아직 이해가 안 되시죠?
이번 예시에는 Key를 사용했습니다. 그러면 어떻게 될까요? 버튼을 눌러서 위젯 트리를 변경시켰기 때문에 Element Tree는 Widget Tree 와 1 대 1 매칭을 시작합니다. 이때 같은 타입인지 확인하고 키가 있으면 같은 키인지 확인합니다. 여기서 타입은 같지만 Key 값이 다른 걸 ElementTree에서 확인합니다. 1 대 1 매칭이 Key 값이 달라서 실패했기 때문에 Element Tree는 Key 값과 맞는 위젯으로 찾아 갑니다.
이렇게 타입과 Key 값이 맞는 위젯을 찾아 1 대 1 매칭을 합니다. 매칭이 끝났으면 Widget Tree와의 Tree 구조를 맞추어야 하기 때문에 Element Tree의 Tree가 재구성됩니다.
이렇게 Element Tree가 재 구성되고 화면을 그리면 State의 위치도 같이 변했기 때문에 우리는 바뀐 타일을 화면으로 볼 수 있게 되는 것입니다. 이렇게 위젯 트리 간에 상태를 유지하고 싶을 때 Key를 사용합니다.
Key 는 어디에 넣어야할까?
tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StfColorTile(key: UniqueKey()), // Key 추가
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StfColorTile(key: UniqueKey()), // Key 추가
),
];
위의 예시에서 해당 부분만 StfColorTile를 Padding으로 감싸봤습니다.
결과는 충격적이게 타일의 위치가 바뀌는 게 아닌 새로운 타일이 계속 생성되는 모습을 확인할 수 있습니다. 그 이유는 WidgetTree가 변경되고 ElementTree가 1 대 1 매칭을 할 때 특정 레벨에서만 비교를 한다고 했습니다. 쉽게 말해서 같은 노드에 있는 거끼리 만 비교합니다.
버튼을 눌렀다면 현재 트리는 이런 상태 일 것입니다. 그리고 1 대 1 매칭을 시작하겠지요.
이렇게 같은 노드에 있는 거부터 확인합니다. Padding에는 키가 없기 때문에 같은 타입인이지만 확인합니다. 이상이 없기 때문에 다음 노드끼리 확인을 시작합니다.
Padding의 노드는 따로 떨어져 있기 때문에 이렇게 하나씩만 1 대 1 매칭을 하게 됩니다. 매칭을 하는데 키값이 달라서 해당 키를 가진 요소를 찾아야 하는데 해당 노드에는 해당 키를 가진 타일 요소가 없으니 새것을 생성해버립니다. 그래서 위에 결과처럼 버튼을 누를 때마다 새로운 타일이 생성되는 것입니다. 이 예시를 통해 키를 어디에 넣어야 할지 알 수 있습니다.
tiles = [
Padding(
key: UniqueKey(), // Key 추가
padding: const EdgeInsets.all(8.0),
child: const StfColorTile(),
),
Padding(
key: UniqueKey(), // Key 추가
padding: const EdgeInsets.all(8.0),
child: const StfColorTile(),
),
];
이렇게 탑노드에 키를 넣어줘야 합니다.
같은 노드 선상에서 있으니깐 저렇게 변경 작업이 이루어지고 우리가 원하는 타일의 위치를 변경할 수 있게 됩니다.
Key의 종류
UniqueKey : 마땅히 키로 사용할 값이 없지만 확실하게 위젯을 구별할 때 사용하는 키
- 자동으로 유니크한 키가 생성되기 때문에 위젯을 확실하게 구별할 수 있습니다.
ValueKey : 하나의 정보에서 생성되는 키
- 텍스트나 숫자등 구별이 가능한 하나의 정보를 키에 넣어서 사용한다.
- 하루에 한 번씩 쓰는 일기장에서는 날짜를 키로 사용할 수 있다.
ObjectKey : 객체를 이용해 구별하는 키
- 일기장을 다시 예시로 들면
- 하루에 한 번씩 쓰는 일기장이 아니라고 한다면 날짜로 키를 사용하면 중복 키값이 들어갈 수 있다.
- 이때는 날짜 + 일지 제목으로 합쳐서 하나의 Object를 만들어서 구별하는 것이다.
- 같은 날짜 같은 제목으로 일기를 쓰지는 않으니깐 키로 사용해도 좋겠죠
PageStorageKey : 사용자의 스크롤 위치를 기억하는 데 사용되는 키
GlobalKey : 전체 앱에서고유하게 사용할 수 있는 키
- 서로 다른 2개의 스크린에서 같은 위젯을 띄우고 상태는 똑같게 유지해야 하는경우 등에 사용
'Flutter > 기본' 카테고리의 다른 글
[Flutter] StreamSubscription 그리고 listen (0) | 2023.02.12 |
---|---|
[Flutter] 알아두면 좋은 정보들 / 면접 준비에 좋은 질문 답변 (0) | 2023.02.09 |
[Flutter] Widget Tree / Tree 란 무엇인가? (0) | 2023.02.09 |
[Flutter] StateNotifier 란? (1) (1) | 2023.01.31 |
[Flutter] .g.dart 파일 정리하기. (0) | 2023.01.25 |