Flutter 모범 사례

Flutter 모범 사례

프로그래밍 여정에서 사용하는 기술에 관계없이 수많은 애플리케이션을 구축하게 되는데, 이러한 작업을 완료하는 것도 중요하지만 올바른 방법으로 수행하는 것이 더 중요합니다. 아래는 Flutter를 사용할때 주요 관점이 될 법한 사례들을 간단히 정리해봤습니다. 

👉 가능하면 const 키워드 사용
👉 쉼표를 사용하여 코드 의도 전달
👉 명명 규칙 따르기
👉 올바른 위젯 사용
👉 가능하면 private 변수/메서드 사용
👉 상태 관리의 적절한 사용
👉 딥 트리를 사용하지 않고, 별도의 위젯 생성
👉 위젯 내에서 메서드 구현을 피하고 대신 별도의 메서드 생성
👉 nullable이 아닌 한 nullable 사용 피하기
👉 cascade(..) 사용
👉 spread 연산자(...) 사용
👉 테마/경로를 별도의 파일에 정의하기
👉 하드코딩된 문자열 사용 피하기
👉 하드코딩된 스타일 사용하지 않기
👉 앱 내에서 패키지 가져올때 가급적 상대경로로 가져오기
👉 가능하면 stateless 위젯 사용
👉 Flutter lint 사용

아래 예제에서는 동일한 로직을 구현하는 가장 좋고 올바른 방법을 표현해보았습니다.


가능하면 const 키워드 사용

const 키워드를 사용하면 위젯을 다시 빌드하지 않아도 됩니다.

❌ 기존 코드
Column(
  children: [
    Text('One'),
    Text('Two'),
  ],
)

✅ 개선 코드
Column(
  children: const [
    Text('One'),
    Text('Two'),
  ],
)


쉼표를 사용하여 코드 의도 전달

쉼표를 최대한 많이 추가하여 애플리케이션 코드의 의도를 올바르게 전달할 수 있습니다.

❌ 기존 코드
@override
Widget build(BuildContext context) {
  return const Scaffold(body: Center(child: Text('One')));
}

✅ 개선 코드
@override
Widget build(BuildContext context) {
  return const Scaffold(
    body: Center(
      child: Text(
        'One'
      ),
    ),
  );
}


이름 지정 규칙 따르기

다트에서는 다음과 같이 이름을 지을 때 명심해야 할 사항이 있습니다.
👉 파일 이름은 항상 작은 대소문자를 포함한 카멜 케이스를 사용합니다 (ex. home_page.dart, file_manager.dart 등)
👉 클래스 이름은 대문자로 시작합니다 (ex. HomePage, FileManager 등)
👉 메서드 이름은 소문자로 시작합니다 (ex. getData())
👉 필드, 메서드 또는 클래스가 비공개인 경우 밑줄을 사용합니다 (ex. String _privateValue)

❌ 기존 코드
1. 파일명
Home_Page.dart
File_manager.dart
2. 클래스명
class myApp
class myapp
3. 메서드명
void getdata() {}
void GetData() {}
4. Private 변수
String privateVariable

✅ 개선 코드
1. 파일명
home_page.dart
file_manager.dart
2. 클래스명
class MyApp
3. 메서드명
void getData() {}
4. Private 변수
String _privateVariable


올바른 위젯 사용

Flutter는 위젯에 관한 것이지만 올바른 위젯을 사용하는 것이 중요할 때도 있습니다. 예를 들어, 패딩을 얻기 위해 컨테이너 위젯을 사용하는 것은 의미가 없습니다. 대신 패딩 위젯을 사용하세요.

❌ 기존 코드
Container(
  padding: const EdgeInsets.all(8),
  child: const Text("Hello"),
),
Container(height: 10,),

✅ 개선 코드
const Padding(
  padding: EdgeInsets.all(8),
  child: Text("Hello"),
),
const SizedBox(height: 10,)


가능하면 private 변수/메서드 사용

필요한 경우가 아니라면 가급적 private 키워드를 사용하세요.

❌ 기존 코드
class Student {
  String name;
  String address;

  Student({
    required this.name,
    required this.address,
  });
}

✅ 개선 코드
class Student{
  String _name;
  String _address;

  Student({
    required String name,
    required String address,
  })  : _name = name,
        _address = address;
}


상태 관리의 적절한 사용

상태 관리 도구를 사용하지 않는 경우 setState를 사용하여 위젯 트리를 다시 빌드해야 할 수 있지만 올바른 방법으로 사용하고 있는지 확인하세요. 단일 텍스트 필드를 업데이트한다고 해서 전체 위젯을 다시 빌드하지 말고 별도의 위젯을 만든 다음 해당 새 위젯만 다시 빌드하세요. 이를 상태 올리기(lifting the state up)라고도 합니다.

❌ 기존 코드
Column(
  children: [
    Text(
      _textValue
    ), 
    ElevatedButton(
      onPressed: (){
        setState((){
          _textValue="new value";
        });
      },
    child: Text(
      'Update'
    ),),
 ],
)

✅ 개선 코드
Column(
  children: [
    NewWidgetWithTextAndButton(),
 ],
)


딥 트리를 사용하지 않고, 별도의 위젯 생성

저도 이 문제에 여러 번 직면한 적이 있습니다. 수천 줄의 코드로 이루어진 하나의 위젯을 계속 보고 싶지는 않을 겁니다. 대신 별도의 위젯을 만들어 보세요. 깔끔하고 리팩터링하기 쉬울 거에요!

❌ 기존 코드
Column(
  children: [
    Container(
      ...
    ),
    Container(
      ...
    ),
  ],
)

✅ 개선 코드
Column(
  children: [
    FirstLengthyCodeWidget(),
    SecondLengthyCodeWidget(),
  ],
)


위젯 내에서 메소드 구현을 피하고 대신 별도의 메서드 생성

어떤 버튼의 onPressed() 내에 50줄의 코드가 있나요? 좋은 방법은 아닙니다. 위젯 외부의 메서드로 감싸세요.

❌ 기존 코드
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: ElevatedButton(
      onPressed: () {
        ...
      },
      child: const Text('Do some task'),
    ),
  );
}

✅ 개선 코드
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: ElevatedButton(
      onPressed: _callTheListener,
      child: const Text('Do some task'),
    ),
  );
}
void _callTheListener(){
  ...
}


nullable이 아닌 한 nullable 사용 피하기

다트의 한 가지 좋은 점은 Java와 달리 null safety를 지원한다는 것입니다. 이전 프로젝트에서는 애플리케이션에서 null safety을 제대로 사용하지 않았기 때문에 많은 null exception이 발생했습니다. 따라서 꼭 필요한 경우가 아니라면 nullable을 사용하지 마세요.

❌ 기존 코드
TextEditingController? textEditingController;@override
initState() {
  super.initState();
  textEditingController = TextEditingController();
}

✅ 개선 코드
late TextEditingController textEditingController;
@override
initState() {
  super.initState();
  textEditingController = TextEditingController();
}


Cascade(..) 사용

플러터를 처음 시작하는 경우 이 연산자를 사용하지 않았을 수도 있지만 동일한 객체에 대해 일부 작업을 수행하려는 경우 매우 유용합니다.

❌ 기존 코드
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

✅ 개선 코드
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

Spread 연산자 (...) 사용

이것은 다트가 제공하는 또 다른 멋진 연산자입니다. 이 연산자를 사용하여 if-else, 목록에 합류 등과 같은 많은 작업을 간단히 수행할 수 있습니다.

❌ 기존 코드
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        isTrue ? const Text('One') : Container(),
        isTrue ? const Text('Two') : Container(),
        isTrue ? const Text('Three') : Container(),
      ],
    ),
  );
}

✅ 개선 코드
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        if(isTrue)...[
          const Text('One'),
          const Text('Two'),
          const Text('Three')
        ]
      ],
    ),
  );
}


테마/경로를 별도의 파일에 정의하기

애플리케이션에 여러 개의 테마가 필요하고 앱에 많은 경로가 필요할 수 있으므로 경로와 테마를 별도의 파일에 정의하는 것이 좋습니다.

❌ 기존 코드
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
      textTheme: TextTheme(),
      primaryColor: Colors.green,
    ),
    home: const HomePage(),
  );
}

✅ 개선 코드
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: appTheme,
    home: const HomePage(),
  );
}

var appTheme = ThemeData(
  primarySwatch: Colors.blue,
  textTheme: const TextTheme(),
  primaryColor: Colors.green,
);


하드코딩된 문자열 사용 피하기

도움이 되지 않을 것 같지만 앱이 성장하기 시작하면 결국 많이 찾아보고, 계속 교체를 하게 될 것입니다.

❌ 기존 코드
Text(
  'one',
),

✅ 개선 코드
Text(
  AppLocalizations.of(context)!.one,
),


하드코딩된 스타일 사용하지 않기

애플리케이션에서 하드코딩된 스타일, 장식 등을 사용 중이거나 나중에 해당 스타일을 변경하기로 결정한 경우. 하나씩 수정해야 합니다.


❌ 기존 코드
Column(
  children: const [
    Text(
      'One',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
    Text(
      'Two',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
  ],
)

✅ 개선 코드
Column(
  children: [
    Text(
      'One',
      style: Theme.of(context).textTheme.subtitle1,
    ),
    Text(
      'Two',
      style: Theme.of(context).textTheme.subtitle1,
    ),
  ],
),

앱 내에서 패키지 가져올때 가급적 상대경로로 가져오기

애플리케이션 클래스 또는 파일을 가져오려는 경우 파일에서 경로를 가져올 때 패키지를 직접 가져오기보다 상대경로로 가져오는 것이 좋습니다.


❌ 기존 코드
import 'package:flutter/material.dart';
import 'package:test_project/student.dart';

✅ 개선 코드
import 'package:flutter/material.dart';
import 'model/student.dart';


가능하면 stateless 위젯 사용

가능한 한 상태 저장 위젯을 사용하지 않는 것이 좋으며, 이렇게 하면 setState를 사용하여 위젯을 반복해서 빌드할 필요가 없습니다.


❌ 기존 코드
class TestWidget extends StatefulWidget {
  const TestWidget({Key? key}) : super(key: key);
  
  @override
  State<TestWidget> createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  @override
  Widget build(BuildContext context) {
    return Image.asset('assets/men_walking.png');
  }
}


✅ 개선 코드
class TestWidget extends StatelessWidget {
  const TestWidget({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Image.asset('assets/men_walking.png');
  }
}


Flutter lint 사용

요즘 플러터는 코드에 대한 규칙 집합을 정의할 수 있는 플러터 린트를 기본으로 제공합니다. 애플리케이션에서 반드시 사용하세요.

👉 Flutter lint: https://pub.dev/packages/lint

Previous Post Next Post