보일러플레이트 코드 보완법 1 – 위젯 배치와 개행 들여쓰기 활용

Posted on

프로그래밍은 크게 절차형 프로그래밍과 객체지향형 프로그래밍으로 구분해볼 수 있습니다. 절차형 프로그래밍은 구현하려는 기능을 담은 함수가 중심이 되고 객체지향형 프로그래밍은 구현하려는 기능을 담지하는 객체가 중심이 됩니다. 함수를 실행하는 순서에 맞추어 코드가 올 수 있는게 절차형이고, 객체들이 정의한 속성에 맞게 인스턴스화해서 객체들끼리의 통신에 주안점을 두는게 차이점인데요. 객체지향형의 경우에는 보다 큰 지향점이 있게 됩니다. 프로그램을 구성하는 클래스들을 잘 정의해야 하고 클래스들끼리 상호작용하는 방식을 형식과 내용을 잘 갖추어 활용해야 한다는 것입니다.

의존성 주입이라느니, 싱글톤 패턴이라느니, 클린 아키텍처와 같은 말이 바로 이런 클래스들끼리의 상호작용 방식을 지정해주는 개념들입니다. 무엇보다도 중요한 것은 절차형에서 보여지는 함수 내용이 덕지덕지 붙여진 중구난방되기 쉬운 코드보다 이미 검증된 간결한 코드를 중심으로 클래스들을 활용할 구조를 잘 짜는 것이 필수입니다.

보다 더 이해잘 되고 보다 더 유지보수가 쉽고 보다 더 구조적인 측면이 살려지게 됩니다.

프로그램이 작동하려면 중요한 것이 코드의 실행 흐름입니다. 실행 흐름을 잘 알면 코드화가 가능해지겠죠. 그런데 때로는 프로그램을 구성하는 구문들이 너무나도 장황하게 길고 알아보기 어렵게 되기도 합니다. 이 경우에 타인이 내 코드를 보고 협업을 해야한다거나, 10년후에 자기가 만든 코드를 살펴봐야 한다면 큰 장애요소가 됩니다. 이를 보일러플레이트 코드라고 하는데요. 작동은 기가 막히게 잘하지만, 식별이나 이해가 잘 안되게 만들어진 것이 되어 프로그램의 가치가 많이 떨어지게 됩니다.

플러터에서도 그러한데요. 클래스 없이 함수만 지닌 절차형으로도 가능한 언어인 다트를 쓸때 특별한 고찰없이 프로그램을 작성하다보면 간결하지 않게 되는 경우가 많습니다. FutureBuilder() 와 같은 위젯을 쓰면 편리하게 되지만, 기재해야 하는 속성의 수가 증가하게 되고 이렇게 되면 경우에 따라 소스코드가 매우 지저분하게 됩니다.

수학에서 보면 수식을 기술할때 변수를 직접 지정해서 해도 이해가 될 수 있는데요. 너무 과도하게 변수의 기호를 바꾼다든지, 라마누잔처럼 비표준적 기술을 많이 쓰면 이후에 가치가 인정되는 부분이 있더라도 공식 이해에 초견상 안좋은 영향을 줍니다. 그래서 널리 쓰이는 상수나 공식은 최초로 입안한대로 쓰기도 하고 한눈에 알아보기 쉽게 기술하는게 불문율인데 프로그래밍도 그러하네요.

디자인 패턴이나 클린 아키텍처와 같은 정식화된 기능도 프로그램 코드의 형식을 정식화해서 쉽게 알아볼 수 있는 간결한 코드 작성을 돕고 이를 구조화한 장점으로 하는 체제입니다.

이러한 방법론도 배워두면 좋구요. 이런 방법론 이전에 해둘법한 리팩토링 상의 노하우를 일단 소개해보겠습니다.

예를 들면 아래처럼 하는 것이

@override
Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
         children: [
             BackgroundImageWork("md", 'assets/images/idols_first_page.gif', "assets/docs/intro.greeting.md"),
         ],
       ),
      bottomNavigationBar: BottomNavigationBarUtil(navIndex),
    );
  }

아래처럼 하는 것보다 간결합니다.

@override
Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundColorBrown(),
      body: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: FadeIn(
              duration: const Duration(milliseconds: 2050), 
              curve: Curves.easeIn,
                  child: Column(
                      children: [
                          BackgroundImageWork("md", 'assets/images/idols_first_page.gif', "assets/docs/intro.greeting.md"),
                    ],
                  ),
            ),
      ),
      bottomNavigationBar: BottomNavigationBarUtil(navIndex),
    );
  }

이 경우 스크롤을 해야 하기에 SingleChildScrollView()를 도입했고, 이를 작동시키기 위한 속성을 기재했습니다. 그리고 페이드인 효과를 주기 위해 FadeIn() 을 도입해서 역시 속성을 기재했네요. 위젯들은 기본 작동을 위해 반드시 기재해야 할 속성이 필수로 들어가야 하는데요. 어떤 경우 속성이 주렁주렁 달리면 한눈에 보기에 지저분해지게 되기도 하구요, 다른 라인과의 길이를 맞추거나 하는 일이 있게 됩니다. 이 경우 프로그래밍 방법론 적용은 아니지만 아래처럼 수정하면 좋아보이기도 합니다. (다른 라인과 길이를 맞추거나 하는 것)

@override
Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundColorBrown(),
      body: SingleChildScrollView(scrollDirection: Axis.vertical,
          child: FadeIn(duration: const Duration(milliseconds: 2050), curve: Curves.easeIn,
              child: Column(
                    children: [
                          BackgroundImageWork("md", 'assets/images/idols_first_page.gif', "assets/docs/intro.greeting.md"),
                    ],
                  ),
            ),
      ),
      bottomNavigationBar: BottomNavigationBarUtil(navIndex),
    );
  }

어느 정도 보기가 좋습니다. 형태적으로 대칭처럼 보여서입니다. 속성 라인이 길게 이어질 수밖에 없을때 사용해볼만하죠.

그리고 FutureBuilder()를 쓰면 조금 복잡해집니다. 아래와 같이 최소 구조를 유지한 상태에서 프로그램을 짜려는 마음 가짐으로 임하면 좋습니다.

@override
Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: Text('Future Demo Page'),
        ),
        body: FutureBuilder(
          builder: (ctx, snapshot) {
            // Checking if future is resolved or not
            if (snapshot.connectionState == ConnectionState.done) {
              // If we got an error
              if (snapshot.hasError) {
                return Center(
                  child: Text(
                    '${snapshot.error} occurred',
                    style: TextStyle(fontSize: 18),
                  ),
                );

                // if we got our data
              } else if (snapshot.hasData) {
                // Extracting data from snapshot object
                final data = snapshot.data as String;
                return Center(
                  child: Text(
                    '$data',
                    style: TextStyle(fontSize: 18),
                  ),
                );
              }
            }

            // Displaying LoadingSpinner to indicate waiting state
            return Center(
              child: CircularProgressIndicator(),
            );
          },

          // Future that needs to be resolved
          // inorder to display something on the Canvas
          future: getData(),
        ),
      ),
    );
  }

때로는 코드에 미려한 UI 처리를 주먹구구식으로 하다보면 Center() 외에 Container(), Column(), Row(), Align(), DecorationBox(), Padding() 등을 무자비하게 (?) 중첩시키기도 합니다. 아래 예를 보세요.

@override
Widget build(BuildContext context) {
    return Scaffold(
      body: Card(
      ...
        Center(
        ...
            Padding(
            ...
               Container(
               ...
                  Column(
                  ...
                  ),
              ),
           ),
        ),
    );
}

이런 구조 자체는 문제가 없어보이는데, 각각의 위젯안에 속성을 기재하다보면 코드가 지저분해집니다.

위와 같은 코드는 위젯 위계 질서를 지켜 불필요한 위젯을 줄이고, 반복되는 코드는 최대한 코드를 분리시켜 함수나 클래스로 만들어 간략하게 해서 보여주면 좋습니다. 이 경우 위젯을 배치하는 코드는 Column()과 Row()를 타위젯과 겹쳐두는 깊이를 최대한 작게 하면서 구성하면 좋습니다. 특히 Column() 이나 Row() 처럼 children: [ ] 속성을 줄 수 있는 위젯을 최대한 간결하게 쓰는게 좋습니다.

@override
Widget build(BuildContext context) {
    return Column(children: [
      Text(title),
      CustomWidget()
    ]);
  }

이 경우 CustomWidget() 은 함수로 구현해도 되고 클래스로 구현해서 위젯을 리턴하게 해서 쓰기도 합니다.

Widget homeView(String title) => Scaffold(
        body: Center(
          child: Column(mainAxisSize: MainAxisSize.min, children: [
            HeaderSection(title, 'FilledStacks'),
            const DecoratedContainer(),
        ]),
    ));

Widget headerSection(String title, String name) => Column(children: [
      Text(title),
      Text('All posts from $name'),
    ]);

Widget decoratedContainer() => Container(
      width: 100,
      height: 100,
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(10.0),
          color: Colors.red,
          boxShadow: [
            BoxShadow(color: Colors.red, blurRadius: 16.0),
          ]),
    );

(윗 코드는 직접 짜는게 귀찮아서 타저자 것을 가져왔습니다)

그리고 GridView 구현시 컬럼을 여러개 두는 코드를 위해 FutureBuilder() 를 쓰면 편리한 경우에도 위젯을 중첩시켜서 달다보면 가독성이 매우 떨어집니다. FutureBuilder()를 GridView와 쓰면 화면이 큰 해상도인 경우 GridView를 두개의 컬럼으로 자동으로 보여주는 기능이 있어서 잘 쓰게 되는데요. 이 경우 Delegate를 설정하는 부분이나 기타 다른 부분에 위에 해설한 위젯 무분별 중첩에 의해 코드가 지저분해집니다. 그래서 대칭적인 구현을 위해 아래처럼 직접 투컬럼을 지정해보는 방법도 있습니다.

@override
Widget build(BuildContext context) {
    List dataList1 = data1.values.toList();
    List dataList2 = data2.values.toList();
    return Container(
      padding: screenCnd == 0
          ? const EdgeInsets.fromLTRB(10, 80, 10, 20)
          : const EdgeInsets.fromLTRB(20, 195, 20, 20),
      height: MediaQuery.of(context).size.height,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          Expanded(
            child: Column(
              children: dataList1
                  .map<Widget>(
                    (e) => ListTile(
                      onTap: () {
                        Navigator.push(
                            context,
                            MaterialPageRoute(
                                builder: (context) =>
                                    ArticleDisplayPage(e['content'])));
                      },
                      title: Text(e["title"], style: textBlack(18.0),),
                      subtitle: Text(e["category"], style: textBlack54(15.0),),
                    ),
                  )
                  .toList(),
            ),
          ),
          Expanded(
            child: Column(
              children: dataList2
                  .map<Widget>(
                    (e) => ListTile(
                      onTap: () {
                        Navigator.push(
                            context,
                            MaterialPageRoute(
                                builder: (context) =>
                                    ArticleDisplayPage(e['content'])));
                      },
                      title: Text(e["title"], style: textBlack(18.0),),
                      subtitle: Text(e["category"], style: textBlack54(15.0),),
                    ),
                  )
                  .toList(),
            ),
          ),
        ],
      ),
    );
}

이 경우 아래 코드에서 부득이하게 확장해서 만든 코드로 기본적인 형태를 갖춘 상태에서 확장하는 예입니다.

@override
Widget build(BuildContext context) {
    List dataList = data.values.toList();
    return Column(
        children: dataList
                  .map<Widget>(
                    (e) => ListTile(
                      onTap: () {
                        Navigator.push(
                            context,
                            MaterialPageRoute(
                                builder: (context) =>
                                    ArticleDisplayPage(e['content'])));
                      },
                      title: Text(e["title"], style: textBlack(18.0),),
                      subtitle: Text(e["category"], style: textBlack54(15.0),),
                    ),
                  )
              .toList(),
            ),
          ),
    );
}

일단 객체지향 방법론인 디자인 패턴이나 클린 아키텍처를 모르더라도 잘 쓸 수 있는 소스코드 정렬 방법입니다. 개행이나 들여쓰기와 같은 것을 잘 정리해두어도 가독성이 큽니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다