본문 바로가기
Dart/Flutter

[Flutter] 플러터 개발시 모범사례 확인하기

by 검은냥냥이 2023. 4. 9.

플러터(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` 선언을 사용하지 않는 것이 좋으며, 데이터의 의미를 이해하도록 노력해야 합니다. 그리고 타입을 생략할 수 있는 경우에도 타입을 지정하는 것이 좋습니다.

 

참고 링크

 

Top 7 best practices in Flutter

Best practices to build better, less expensive in the long-term, and more robust applications with Flutter

medium.com

 

728x90
사업자 정보 표시
레플라 | 홍대기 | 경기도 부천시 부일로 519 화신오피스텔 1404호 | 사업자 등록번호 : 726-04-01977 | TEL : 070-8800-6071 | Mail : support@reafla.co.kr | 통신판매신고번호 : 호 | 사이버몰의 이용약관 바로가기