플러터(Flutter)로 앱을 개발할 때, 일관성 없는 코드 작성이나 특정 컨벤션을 지키지 않는 경우 성능 이슈나 재사용성의 저하 등의 문제가 발생할 수 있습니다. 이러한 문제를 방지하기 위해서는 다음과 같은 모범 사례를 따르는 것이 좋습니다.
뷰단 삼항연산자
플러터(Flutter)는 UI(View)와 비즈니스 로직(Model, Controller)을 구분하지 않고 코드를 작성할 수 있기 때문에, 뷰단에서도 코드를 자유롭게 혼합할 수 있습니다. 이러한 점은 개발자들이 빠르게 프로토타이핑을 할 수 있도록 도와주지만, 코드를 장기적으로 유지하기 어렵거나 가독성이 떨어질 수 있다는 단점도 존재합니다.
따라서, 개발자는 코드의 가독성을 높이기 위해 모델과 컨트롤러 등을 적절히 분리하고, 코드의 일관성을 유지하는 노력이 필요합니다.
이전 코드
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Page'),
),
body: Center(
child: isAuthorized
? AuthorizedUserWidget()
: NonAuthorizedUserWidget(),
),
);
}
개선된 코드
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Page'),
),
body: Center(
child: _buildUserWidget(),
),
);
}
Widget _buildUserWidget() {
return isAuthorized
? AuthorizedUserWidget()
: NonAuthorizedUserWidget();
}
코드 재사용성
위에서 `build` 안에서 `widget`을 반환하는 내부 함수로 사용하게 될 경우 `build` 메서드가 호출될 때마다 위젯이 호출될 수 도 있습니다. 물론, 이전과 상태가 동일한 위젯에 경우는 제외됩니다.
하지만, 코드 재사용성을 위한 위젯을 생성한다면 별도의 `class`로 분리하는것이 좋을 것 같습니다.
이전 코드
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Page'),
),
body: Center(
child: _MyWidget(),
),
);
}
Widget _MyWidget() {
return Text('My Widget');
}
}
개선된 코드
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Page'),
),
body: Center(
child: MyWidget(),
),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return Text('My Widget');
}
}
`build` 메서드 내부 로직에 유의하기
`build` 메서드는 프레임마다 호출되기 때문에, 메서드 내부에서 비즈니스 로직이나 무거운 작업을 수행하면 앱이 느려지고 사용자 경험에 악영향을 줄 수 있습니다. 이를 방지하기 위해서는 다음과 같은 방법을 사용할 수 있습니다.
1. 비즈니스 로직을 `build` 메서드 외부로 이동시키기 (관심사 분리)
2. `const` 키워드를 사용하여 위젯을 효율적으로 리빌드하기
3. API 호출 등의 작업은 build 메서드 외부에서 수행하기 (FutureBuilder를 사용하여 효율적으로 처리 가능)
FutureBuilder에서 Future를 직접 호출하지 않기
`FutureBuilder`는 `Future`의 다양한 상태(waiting, successful, error 등)에 따라 특정 위젯을 렌더링 하는 데 사용되는 위젯입니다. 하지만 FutureBuilder는 위젯이기 때문에 build 메서드 내부에 존재하며, 앱 화면 수명 주기 동안 여러 번 호출되기 때문에 매번 미래(Future)를 호출합니다.
이는 API가 본인의 것이면 서버 비용을 증가시키거나, 타사 API라면 각 엔드포인트 호출에 대한 비용이 발생할 수 있습니다. 또한, 이로 인해 앱이 느려지고 사용자 경험이 나빠질 수 있습니다.
해결책은 간단합니다. `Future`를 `build` 메서드 외부에서 호출하고,` FutureBuilder`에서는 해당 `Future`의 결과만 검색하면 됩니다. 이렇게 하면 여전히 다른 상태를 가지지만, `Future`는 한 번만 해결됩니다.
setState() 메서드 내부에서 무거운 로직 피하기
`Stateful Widgets`은 코드에서 무언가가 변경되었음을 알리고, 자신을 다시 빌드해야 한다는 것을 알리기 위해 `setState()` 메서드를 사용할 수 있습니다.
변수를 수정하고 UI에 변경 사항을 알리려면 `setState` 내부에서 작업해야 하지만, `setState`는 위젯이 자신을 다시 빌드하도록 알리는 역할을 하기 때문에, 변경된 내용만 있어야 합니다. `setState`는 동기 메서드이기 때문에 UI가 느려지고 느려질 수 있습니다.
여기서 가장 좋은 방법은 `setState()` 메서드 내부에서 무거운 로직을 피하고, 대신 무거운 작업을 외부에서 처리하고 `setState()`를 통해 UI에 정확한 변경 사항만 알리는 것입니다.
setState(() {
isLoading = true;
});
// do some heavy logic
setState(() {
isLoading = false;
result = heavyLogicResult;
});
위 코드에서, `isLoading` 변수가 변경되어 UI가 업데이트되어야 하지만, 무거운 로직이 `setState` 내부에 있으면 UI가 느려질 수 있습니다. 대신, `isLoading`을 먼저 변경하고, 무거운 로직을 외부에서 처리한 후, 결과를 `setState`를 사용하여 UI에 반영합니다. 이렇게 하면 UI가 더 빠르고 부드럽게 업데이트됩니다.
Container 위젯 남용하지 않기
웹 개발에서 `div`를 사용하는 것과 같이, `Flutter`에서 `Container` 위젯을 사용하는 것이 익숙한 웹 개발자들이 많습니다. 하지만 `Container`는 내부적으로 많은 것을 처리해야 하기 때문에 무거우며, 다른 위젯을 감싸는 용도로 사용하면 FPS가 감소하여 앱이 느려질 수 있습니다.
물론 `Container`는 많은 경우에 필요하고 존재하는 이유가 있지만, 코드를 불필요하게 복잡하게 만들지 않기 위해서는 필요한 경우에만 사용하는 것이 좋습니다.
이전 코드
Container(
color: Colors.red,
child: Text("Hello World"),
);
Container(
padding: const EdgeInsets.all(20),
child: Text("Hello World"),
);
개선된 코드
SizedBox(
height: 50,
child: Text("Hello World"),
);
Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
height: 50,
child: Text("Hello World"),
);
);
항상 데이터 타입 사용하기
`Dart`는 `var`를 사용하여 동적 데이터 타입을 선언할 수 있습니다. 이 때, `dynamic` 데이터 타입은 그 값을 처리합니다. (보통 API에서 데이터를 가져올 때 데이터 타입을 모를 때 사용됩니다.) 그러나 `dynamic` 데이터 타입은 코드를 런타임 오류에 민감하게 만들 수 있기 때문에 코드를 복잡하게 만들 수 있습니다. 적절한 데이터 타입을 사용하면 컴파일러와 IDE에서 도움을 받을 수 있습니다.
이에 대한 조언으로는 `dynamic` 데이터 타입과 `var` 선언을 사용하지 않는 것이 좋으며, 데이터의 의미를 이해하도록 노력해야 합니다. 그리고 타입을 생략할 수 있는 경우에도 타입을 지정하는 것이 좋습니다.
참고 링크